From 0a9d53e5bd46a5be00dad9faa246d5a69e5dafd6 Mon Sep 17 00:00:00 2001 From: mohammad Date: Thu, 29 May 2025 10:48:12 +0300 Subject: [PATCH 01/58] Refactor ConditionToggle widget to display icons with corresponding conditions --- .../routines/widgets/condition_toggle.dart | 53 ++++++++++++++----- 1 file changed, 41 insertions(+), 12 deletions(-) diff --git a/lib/pages/routines/widgets/condition_toggle.dart b/lib/pages/routines/widgets/condition_toggle.dart index 99ea2f04..b86ba0b3 100644 --- a/lib/pages/routines/widgets/condition_toggle.dart +++ b/lib/pages/routines/widgets/condition_toggle.dart @@ -12,22 +12,51 @@ class ConditionToggle extends StatelessWidget { }); static const _conditions = ["<", "==", ">"]; + static const _icons = [ + Icons.chevron_left, + Icons.drag_handle, + Icons.chevron_right + ]; @override Widget build(BuildContext context) { - return ToggleButtons( - onPressed: (index) => onChanged(_conditions[index]), - borderRadius: const BorderRadius.all(Radius.circular(8)), - selectedBorderColor: ColorsManager.primaryColorWithOpacity, - selectedColor: Colors.white, - fillColor: ColorsManager.primaryColorWithOpacity, - color: ColorsManager.primaryColorWithOpacity, - constraints: const BoxConstraints( - minHeight: 40.0, - minWidth: 40.0, + final selectedIndex = _conditions.indexOf(currentCondition ?? "=="); + + return Container( + height: 80, + decoration: BoxDecoration( + color: ColorsManager.grayColor, + borderRadius: BorderRadius.circular(50), + ), + clipBehavior: Clip.antiAlias, + child: Row( + mainAxisSize: MainAxisSize.min, + children: List.generate(_conditions.length, (index) { + final isSelected = index == selectedIndex; + return Expanded( + child: GestureDetector( + onTap: () => onChanged(_conditions[index]), + child: AnimatedContainer( + duration: const Duration(milliseconds: 180), + curve: Curves.ease, + decoration: BoxDecoration( + color: isSelected ? ColorsManager.blue1 : Colors.transparent, + ), + child: Center( + child: Icon( + _icons[index], + size: 38, + color: isSelected + ? ColorsManager.whiteColors + : ColorsManager.blackColor, + weight: isSelected ? 700 : 500, + ), + ), + ), + ), + ); + }), ), - isSelected: _conditions.map((c) => c == (currentCondition ?? "==")).toList(), - children: _conditions.map((c) => Text(c)).toList(), ); } } From a44d4231f1bd391dc2c6568541cda162f50a2569 Mon Sep 17 00:00:00 2001 From: mohammad Date: Thu, 29 May 2025 14:26:24 +0300 Subject: [PATCH 02/58] Add new grey color constant and new icons for settings in assets Update CreateNewRoutineView to use const constructor Add SubSpaceModel class for device settings Add DefaultContainer widget for web layout Add events and states for device settings bloc Update API endpoints for device settings --- assets/icons/close_settings_icon.svg | 3 + assets/icons/edit_name_icon_settings.svg | 3 + lib/pages/common/custom_table.dart | 4 + .../view/device_managment_page.dart | 2 +- .../widgets/device_managment_body.dart | 90 ++++-- .../bloc/device_info_model.dart | 182 ++++++++++++ .../bloc/setting_bloc_bloc.dart | 149 ++++++++++ .../bloc/setting_bloc_event.dart | 50 ++++ .../bloc/setting_bloc_state.dart | 69 +++++ .../device_setting/bloc/sub_space_model.dart | 35 +++ .../device_setting/device_settings_panel.dart | 267 ++++++++++++++++++ lib/services/devices_mang_api.dart | 65 ++++- lib/services/space_mana_api.dart | 88 ++++-- lib/utils/color_manager.dart | 3 + lib/utils/constants/api_const.dart | 15 +- lib/utils/constants/assets.dart | 5 + lib/web_layout/default_container.dart | 45 +++ 17 files changed, 1031 insertions(+), 44 deletions(-) create mode 100644 assets/icons/close_settings_icon.svg create mode 100644 assets/icons/edit_name_icon_settings.svg create mode 100644 lib/pages/device_managment/device_setting/bloc/device_info_model.dart create mode 100644 lib/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart create mode 100644 lib/pages/device_managment/device_setting/bloc/setting_bloc_event.dart create mode 100644 lib/pages/device_managment/device_setting/bloc/setting_bloc_state.dart create mode 100644 lib/pages/device_managment/device_setting/bloc/sub_space_model.dart create mode 100644 lib/pages/device_managment/device_setting/device_settings_panel.dart create mode 100644 lib/web_layout/default_container.dart diff --git a/assets/icons/close_settings_icon.svg b/assets/icons/close_settings_icon.svg new file mode 100644 index 00000000..93e615d8 --- /dev/null +++ b/assets/icons/close_settings_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/edit_name_icon_settings.svg b/assets/icons/edit_name_icon_settings.svg new file mode 100644 index 00000000..54bee0af --- /dev/null +++ b/assets/icons/edit_name_icon_settings.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/pages/common/custom_table.dart b/lib/pages/common/custom_table.dart index 62760a16..0abe075b 100644 --- a/lib/pages/common/custom_table.dart +++ b/lib/pages/common/custom_table.dart @@ -211,6 +211,7 @@ class _DynamicTableState extends State { onChanged: widget.withSelectAll && widget.data.isNotEmpty ? _toggleSelectAll : null, + ), ); } @@ -281,6 +282,7 @@ class _DynamicTableState extends State { padding: EdgeInsets.symmetric( horizontal: index == widget.headers.length - 1 ? 12 : 8.0, vertical: 4), + child: Text( title, style: context.textTheme.titleSmall!.copyWith( @@ -301,6 +303,7 @@ class _DynamicTableState extends State { required int rowIndex, required int columnIndex, }) { + bool isBatteryLevel = content.endsWith('%'); double? batteryLevel; @@ -312,6 +315,7 @@ class _DynamicTableState extends State { if (isSettingsColumn) { return _buildSettingsIcon(rowIndex, size); } + Color? statusColor; switch (content) { diff --git a/lib/pages/device_managment/all_devices/view/device_managment_page.dart b/lib/pages/device_managment/all_devices/view/device_managment_page.dart index fd3a2574..755bc8b7 100644 --- a/lib/pages/device_managment/all_devices/view/device_managment_page.dart +++ b/lib/pages/device_managment/all_devices/view/device_managment_page.dart @@ -95,7 +95,7 @@ class DeviceManagementPage extends StatelessWidget with HelperResponsiveLayout { return const RoutinesView(); } if (state.createRoutineView) { - return CreateNewRoutineView(); + return const CreateNewRoutineView(); } return BlocBuilder( 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 a3c975c1..f4baad0c 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 @@ -6,9 +6,11 @@ import 'package:syncrow_web/pages/common/filter/filter_widget.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/widgets/device_search_filters.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/device_settings_panel.dart'; import 'package:syncrow_web/pages/device_managment/shared/device_batch_control_dialog.dart'; import 'package:syncrow_web/pages/device_managment/shared/device_control_dialog.dart'; import 'package:syncrow_web/pages/space_tree/view/space_tree_view.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/format_date_time.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; import 'package:syncrow_web/utils/style.dart'; @@ -58,7 +60,8 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { 'Low Battery ($lowBatteryCount)', ]; - final buttonLabel = (selectedDevices.length > 1) ? 'Batch Control' : 'Control'; + final buttonLabel = + (selectedDevices.length > 1) ? 'Batch Control' : 'Control'; return Row( children: [ @@ -105,18 +108,23 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { if (selectedDevices.length == 1) { showDialog( context: context, - builder: (context) => DeviceControlDialog( + builder: (context) => + DeviceControlDialog( device: selectedDevices.first, ), ); - } else if (selectedDevices.length > 1) { - final productTypes = selectedDevices - .map((device) => device.productType) - .toSet(); + } else if (selectedDevices.length > + 1) { + final productTypes = + selectedDevices + .map((device) => + device.productType) + .toSet(); if (productTypes.length == 1) { showDialog( context: context, - builder: (context) => DeviceBatchControlDialog( + builder: (context) => + DeviceBatchControlDialog( devices: selectedDevices, ), ); @@ -130,7 +138,9 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { textAlign: TextAlign.center, style: TextStyle( fontSize: 12, - color: isControlButtonEnabled ? Colors.white : Colors.grey, + color: isControlButtonEnabled + ? Colors.white + : Colors.grey, ), ), ), @@ -166,29 +176,40 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { 'Installation Date and Time', 'Status', 'Last Offline Date and Time', + 'Settings' ], data: devicesToShow.map((device) { final combinedSpaceNames = device.spaces != null - ? device.spaces!.map((space) => space.spaceName).join(' > ') + + ? device.spaces! + .map((space) => space.spaceName) + .join(' > ') + (device.community != null ? ' > ${device.community!.name}' : '') - : (device.community != null ? device.community!.name : ''); + : (device.community != null + ? device.community!.name + : ''); return [ device.name ?? '', device.productName ?? '', device.uuid ?? '', - (device.spaces != null && device.spaces!.isNotEmpty) + (device.spaces != null && + device.spaces!.isNotEmpty) ? device.spaces![0].spaceName : '', combinedSpaceNames, - device.batteryLevel != null ? '${device.batteryLevel}%' : '-', - formatDateTime(DateTime.fromMillisecondsSinceEpoch( - (device.createTime ?? 0) * 1000)), + device.batteryLevel != null + ? '${device.batteryLevel}%' + : '-', + formatDateTime( + DateTime.fromMillisecondsSinceEpoch( + (device.createTime ?? 0) * 1000)), device.online == true ? 'Online' : 'Offline', - formatDateTime(DateTime.fromMillisecondsSinceEpoch( - (device.updateTime ?? 0) * 1000)), + formatDateTime( + DateTime.fromMillisecondsSinceEpoch( + (device.updateTime ?? 0) * 1000)), + 'Settings', ]; }).toList(), onSelectionChanged: (selectedRows) { @@ -202,6 +223,10 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { .map((device) => device.uuid!) .toList(), isEmpty: devicesToShow.isEmpty, + onSettingsPressed: (rowIndex) { + final device = devicesToShow[rowIndex]; + showDeviceSettingsSidebar(context, device); + }, ), ), ) @@ -213,4 +238,37 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { }, ); } + + void showDeviceSettingsSidebar(BuildContext context, AllDevicesModel device) { + showGeneralDialog( + context: context, + barrierDismissible: true, + barrierLabel: "Device Settings", + transitionDuration: const Duration(milliseconds: 300), + pageBuilder: (context, anim1, anim2) { + return Align( + alignment: Alignment.centerRight, + child: Material( + child: Container( + width: MediaQuery.of(context).size.width * 0.3, + color: ColorsManager.whiteColors, + child: DeviceSettingsPanel( + device: device, + onClose: () => Navigator.of(context).pop(), + ), + ), + ), + ); + }, + transitionBuilder: (context, anim1, anim2, child) { + return SlideTransition( + position: Tween( + begin: const Offset(1, 0), + end: Offset.zero, + ).animate(anim1), + child: child, + ); + }, + ); + } } diff --git a/lib/pages/device_managment/device_setting/bloc/device_info_model.dart b/lib/pages/device_managment/device_setting/bloc/device_info_model.dart new file mode 100644 index 00000000..65a48508 --- /dev/null +++ b/lib/pages/device_managment/device_setting/bloc/device_info_model.dart @@ -0,0 +1,182 @@ +class DeviceInfoModel { + final int activeTime; + final String category; + final String categoryName; + final int createTime; + final String gatewayId; + final String icon; + final String ip; + final String lat; + final String localKey; + final String lon; + final String model; + final String name; + final String nodeId; + final bool online; + final String ownerId; + final String productName; + final bool sub; + final String timeZone; + final int updateTime; + final String uuid; + final String productUuid; + final String productType; + final String permissionType; + final String macAddress; + final Subspace subspace; + + DeviceInfoModel({ + required this.activeTime, + required this.category, + required this.categoryName, + required this.createTime, + required this.gatewayId, + required this.icon, + required this.ip, + required this.lat, + required this.localKey, + required this.lon, + required this.model, + required this.name, + required this.nodeId, + required this.online, + required this.ownerId, + required this.productName, + required this.sub, + required this.timeZone, + required this.updateTime, + required this.uuid, + required this.productUuid, + required this.productType, + required this.permissionType, + required this.macAddress, + required this.subspace, + }); + + factory DeviceInfoModel.fromJson(Map json) { + return DeviceInfoModel( + activeTime: json['activeTime'], + category: json['category'], + categoryName: json['categoryName'], + createTime: json['createTime'], + gatewayId: json['gatewayId'], + icon: json['icon'], + ip: json['ip'] ?? "", + lat: json['lat'], + localKey: json['localKey'], + lon: json['lon'], + model: json['model'], + name: json['name'], + nodeId: json['nodeId'], + online: json['online'], + ownerId: json['ownerId'], + productName: json['productName'], + sub: json['sub'], + timeZone: json['timeZone'], + updateTime: json['updateTime'], + uuid: json['uuid'], + productUuid: json['productUuid'], + productType: json['productType'], + permissionType: json['permissionType'] ?? '', + macAddress: json['macAddress'], + subspace: Subspace.fromJson(json['subspace']), + ); + } + + Map toJson() { + return { + 'activeTime': activeTime, + 'category': category, + 'categoryName': categoryName, + 'createTime': createTime, + 'gatewayId': gatewayId, + 'icon': icon, + 'ip': ip, + 'lat': lat, + 'localKey': localKey, + 'lon': lon, + 'model': model, + 'name': name, + 'nodeId': nodeId, + 'online': online, + 'ownerId': ownerId, + 'productName': productName, + 'sub': sub, + 'timeZone': timeZone, + 'updateTime': updateTime, + 'uuid': uuid, + 'productUuid': productUuid, + 'productType': productType, + 'permissionType': permissionType, + 'macAddress': macAddress, + 'subspace': subspace.toJson(), + }; + } + + static DeviceInfoModel empty() { + return DeviceInfoModel( + activeTime: 0, + category: '', + categoryName: '', + createTime: 0, + gatewayId: '', + icon: '', + ip: '', + lat: '', + localKey: '', + lon: '', + model: '', + name: '', + nodeId: '', + online: false, + ownerId: '', + productName: '', + sub: false, + timeZone: '', + updateTime: 0, + uuid: '', + productUuid: '', + productType: '', + permissionType: '', + macAddress: '', + subspace: Subspace( + uuid: '', + createdAt: '', + updatedAt: '', + subspaceName: '', + ), + ); + } +} + +class Subspace { + final String uuid; + final String createdAt; + final String updatedAt; + final String subspaceName; + + Subspace({ + required this.uuid, + required this.createdAt, + required this.updatedAt, + required this.subspaceName, + }); + + factory Subspace.fromJson(Map json) { + return Subspace( + uuid: json['uuid'], + createdAt: json['createdAt'], + updatedAt: json['updatedAt'], + subspaceName: json['subspaceName'], + ); + } + + Map toJson() { + return { + 'uuid': uuid, + 'createdAt': createdAt, + 'updatedAt': updatedAt, + 'subspaceName': subspaceName, + }; + } +} diff --git a/lib/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart b/lib/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart new file mode 100644 index 00000000..55e5e74e --- /dev/null +++ b/lib/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart @@ -0,0 +1,149 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/bloc/device_info_model.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/bloc/setting_bloc_state.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/bloc/sub_space_model.dart'; +import 'package:syncrow_web/services/devices_mang_api.dart'; +import 'package:syncrow_web/utils/snack_bar.dart'; +part 'setting_bloc_event.dart'; + +class SettingBlocBloc extends Bloc { + final String deviceId; + SettingBlocBloc({ + required this.deviceId, + }) : super(const SettingBlocInitial()) { + on(fetchDeviceInfo); + on(saveName); + on(_changeName); + on(deleteDevice); + //on(_fetchRoomsAndDevices); + } + static String deviceName = ''; + final TextEditingController nameController = + TextEditingController(text: deviceName); + List roomsList = []; + bool isEditingName = false; + + bool _validateInputs() { + final nameError = fullNameValidator(nameController.text); + if (nameError != null) { + CustomSnackBar.displaySnackBar(nameError); + return true; + } + return false; + } + + String? fullNameValidator(String? value) { + if (value == null) return 'name is required'; + final withoutExtraSpaces = value.replaceAll(RegExp(r"\s+"), ' ').trim(); + if (withoutExtraSpaces.length < 2 || withoutExtraSpaces.length > 30) { + return 'name must be between 2 and 30 characters long'; + } + if (RegExp(r"/[^ a-zA-Z0-9-\']/").hasMatch(withoutExtraSpaces)) { + return 'Only alphanumeric characters, space, dash and single quote are allowed'; + } + return null; + } + + Future saveName( + SaveNameEvent event, Emitter emit) async { + if (_validateInputs()) return; + try { + emit(SettingLoadingState()); + var response = await DevicesManagementApi.putDeviceName( + deviceId: deviceId, deviceName: nameController.text); + add(DeviceSettingInitialInfo()); + CustomSnackBar.displaySnackBar('Save Successfully'); + emit(UpdateSettingState(deviceName: nameController.text)); + } catch (e) { + emit(ErrorState(message: e.toString())); + } finally { + // isSaving = false; + } + } + + DeviceInfoModel deviceInfo = DeviceInfoModel( + activeTime: 0, + category: "", + categoryName: "", + createTime: 0, + gatewayId: "", + icon: "", + ip: "", + lat: "", + localKey: "", + lon: "", + model: "", + name: "", + nodeId: "", + online: false, + ownerId: "", + productName: "", + sub: false, + timeZone: "", + updateTime: 0, + uuid: "", + productUuid: "", + productType: "", + permissionType: "", + macAddress: "", + subspace: Subspace( + uuid: "", + createdAt: "", + updatedAt: "", + subspaceName: "", + ), + ); + + Future fetchDeviceInfo( + DeviceSettingInitialInfo event, Emitter emit) async { + try { + emit(SettingLoadingState()); + var response = await DevicesManagementApi.getDeviceInfo(deviceId); + deviceInfo = DeviceInfoModel.fromJson(response); + nameController.text = deviceInfo.name; + + emit(UpdateSettingState( + deviceName: nameController.text, + deviceInfo: deviceInfo, + )); + } catch (e) { + emit(ErrorState(message: e.toString())); + } + } + + bool editName = false; + final FocusNode focusNode = FocusNode(); + + void _changeName(ChangeNameEvent event, Emitter emit) { + emit(SettingLoadingState()); + editName = event.value!; + if (editName) { + Future.delayed(const Duration(milliseconds: 500), () { + focusNode.requestFocus(); + }); + } else { + add(const SaveNameEvent()); + focusNode.unfocus(); + } + emit(UpdateSettingState(deviceName: deviceName, deviceInfo: deviceInfo)); + } + + void deleteDevice( + DeleteDeviceEvent event, Emitter emit) async { + try { + emit(SettingLoadingState()); + var response = + await DevicesManagementApi.resetDevise(devicesUuid: deviceId); + CustomSnackBar.displaySnackBar('Reset Successfully'); + emit(UpdateSettingState( + deviceName: nameController.text, + deviceInfo: deviceInfo, + )); + } catch (e) { + emit(ErrorState(message: e.toString())); + return; + } + } +} diff --git a/lib/pages/device_managment/device_setting/bloc/setting_bloc_event.dart b/lib/pages/device_managment/device_setting/bloc/setting_bloc_event.dart new file mode 100644 index 00000000..737c8889 --- /dev/null +++ b/lib/pages/device_managment/device_setting/bloc/setting_bloc_event.dart @@ -0,0 +1,50 @@ +part of 'setting_bloc_bloc.dart'; + +abstract class SettingBlocEvent extends Equatable { + const SettingBlocEvent(); + @override + List get props => []; +} + +class SaveDeviceName extends SettingBlocEvent { + final String deviceName; + final String deviceId; + + const SaveDeviceName({required this.deviceName, required this.deviceId}); + + @override + List get props => [deviceName, deviceId]; +} + +class StartEditingName extends SettingBlocEvent {} + +class CancelEditingName extends SettingBlocEvent {} + +class ChangeEditingNameValue extends SettingBlocEvent { + final String value; + const ChangeEditingNameValue(this.value); + + @override + List get props => [value]; +} + +class FetchRoomsEvent extends SettingBlocEvent { + final String deviceId; + + const FetchRoomsEvent(this.deviceId); + + @override + List get props => [deviceId]; +} + +class SaveNameEvent extends SettingBlocEvent { + const SaveNameEvent(); +} + +class DeviceSettingInitialInfo extends SettingBlocEvent {} + +class ChangeNameEvent extends SettingBlocEvent { + final bool? value; + const ChangeNameEvent({this.value}); +} +class DeleteDeviceEvent extends SettingBlocEvent {} diff --git a/lib/pages/device_managment/device_setting/bloc/setting_bloc_state.dart b/lib/pages/device_managment/device_setting/bloc/setting_bloc_state.dart new file mode 100644 index 00000000..65907c67 --- /dev/null +++ b/lib/pages/device_managment/device_setting/bloc/setting_bloc_state.dart @@ -0,0 +1,69 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/bloc/device_info_model.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/bloc/sub_space_model.dart'; + +abstract class DeviceSettingsState extends Equatable { + const DeviceSettingsState(); + + @override + List get props => []; +} + +class SettingBlocInitial extends DeviceSettingsState { + final String deviceName; + final String deviceId; + final bool isEditingName; + final String editingNameValue; + + const SettingBlocInitial({ + this.deviceName = '', + this.deviceId = '', + this.isEditingName = false, + this.editingNameValue = '', + }); + + SettingBlocInitial copyWith({ + String? deviceName, + String? deviceId, + bool? isEditingName, + String? editingNameValue, + }) => + SettingBlocInitial( + deviceName: deviceName ?? this.deviceName, + deviceId: deviceId ?? this.deviceId, + isEditingName: isEditingName ?? this.isEditingName, + editingNameValue: editingNameValue ?? this.editingNameValue, + ); + + @override + List get props => + [deviceName, deviceId, isEditingName, editingNameValue]; +} + +class SettingLoadingState extends DeviceSettingsState {} + +class UpdateSettingState extends DeviceSettingsState { + final String deviceName; + final DeviceInfoModel? deviceInfo; + const UpdateSettingState({required this.deviceName, this.deviceInfo}); + + @override + List get props => [deviceName, deviceInfo]; +} + +class ErrorState extends DeviceSettingsState { + final String message; + + const ErrorState({required this.message}); + @override + List get props => [message]; +} + +class FetchRoomsState extends DeviceSettingsState { + final List roomsList; + + const FetchRoomsState({required this.roomsList}); + + @override + List get props => [roomsList]; +} diff --git a/lib/pages/device_managment/device_setting/bloc/sub_space_model.dart b/lib/pages/device_managment/device_setting/bloc/sub_space_model.dart new file mode 100644 index 00000000..bc68b33e --- /dev/null +++ b/lib/pages/device_managment/device_setting/bloc/sub_space_model.dart @@ -0,0 +1,35 @@ +import 'package:syncrow_web/pages/visitor_password/model/device_model.dart'; + +class SubSpaceModel { + final String? id; + final String? name; + List? devices; + + SubSpaceModel({ + required this.id, + required this.name, + required this.devices, + }); + + Map toJson() { + return { + 'id': id, + 'name': name, + 'devices': devices?.map((device) => device.toJson()).toList(), + }; + } + + factory SubSpaceModel.fromJson(Map json) { + List devices = []; + if (json['devices'] != null) { + for (var device in json['devices']) { + devices.add(DeviceModel.fromJson(device)); + } + } + return SubSpaceModel( + id: json['uuid'], + name: json['subspaceName'], + devices: devices, + ); + } +} diff --git a/lib/pages/device_managment/device_setting/device_settings_panel.dart b/lib/pages/device_managment/device_setting/device_settings_panel.dart new file mode 100644 index 00000000..2415ab90 --- /dev/null +++ b/lib/pages/device_managment/device_setting/device_settings_panel.dart @@ -0,0 +1,267 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.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/device_setting/bloc/device_info_model.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/bloc/setting_bloc_state.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/web_layout/default_container.dart'; + +class DeviceSettingsPanel extends StatelessWidget { + final VoidCallback? onClose; + final AllDevicesModel device; + + const DeviceSettingsPanel({this.onClose, super.key, required this.device}); + + @override + Widget build(BuildContext context) { + final sectionTitle = context.theme.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.bold, + color: ColorsManager.grayColor, + ); + Widget infoRow( + {required String label, required String value, Widget? trailing}) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: context.theme.textTheme.bodyMedium!.copyWith( + fontSize: 14, + color: ColorsManager.grayColor, + ), + ), + Expanded( + child: Text( + value, + textAlign: TextAlign.end, + style: context.theme.textTheme.bodyMedium!.copyWith( + fontSize: 14, + color: ColorsManager.blackColor, + ), + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 8), + trailing ?? const SizedBox.shrink(), + ], + ), + ); + } + + return BlocProvider( + create: (context) => SettingBlocBloc( + deviceId: device.uuid ?? '', + )..add(DeviceSettingInitialInfo()), + child: BlocBuilder( + builder: (context, state) { + final iconPath = + DeviceTypeHelper.getDeviceIconByTypeCode(device.productType); + final _bloc = BlocProvider.of(context); + DeviceInfoModel deviceInfo = DeviceInfoModel.empty(); + if (state is UpdateSettingState) { + deviceInfo = state.deviceInfo!; + } + return Container( + width: MediaQuery.of(context).size.width * 0.3, + color: ColorsManager.grey25, + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24), + child: ListView( + children: [ + /// Header + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + icon: SvgPicture.asset(Assets.closeSettingsIcon), + onPressed: onClose ?? () => Navigator.of(context).pop(), + ), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('Device Settings', + style: context.theme.textTheme.titleLarge!.copyWith( + fontWeight: FontWeight.bold, + color: ColorsManager.primaryColor)), + ], + ), + const SizedBox(height: 24), + + /// Device Name + Icon + DefaultContainer( + child: Row( + children: [ + CircleAvatar( + radius: 40, + backgroundColor: + const Color.fromARGB(177, 213, 213, 213), + child: CircleAvatar( + backgroundColor: ColorsManager.whiteColors, + radius: 36, + child: SvgPicture.asset( + iconPath, + fit: BoxFit.cover, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: TextFormField( + maxLength: 30, + style: const TextStyle( + color: ColorsManager.blackColor, + ), + textAlign: TextAlign.center, + focusNode: _bloc.focusNode, + controller: _bloc.nameController, + enabled: _bloc.editName, + onFieldSubmitted: (value) { + _bloc.add(const ChangeNameEvent(value: false)); + }, + decoration: const InputDecoration( + border: InputBorder.none, + fillColor: Colors.white10, + counterText: '', + ), + ), + ), + const SizedBox(width: 8), + _bloc.editName == true + ? const SizedBox() + : GestureDetector( + onTap: () { + _bloc.add(const ChangeNameEvent(value: true)); + }, + child: SvgPicture.asset( + Assets.editNameIconSettings, + color: ColorsManager.grayColor, + height: 20, + width: 20, + ), + ), + ], + ), + ), + const SizedBox(height: 32), + + /// Device Management + Text('Device Management', style: sectionTitle), + DefaultContainer( + padding: EdgeInsets.zero, + child: Column( + children: [ + const SizedBox( + height: 5, + ), + Padding( + padding: const EdgeInsets.all(10.0), + child: infoRow( + label: 'Sub-Space:', + value: device.subspace!.subspaceName, + trailing: const Icon( + Icons.arrow_forward_ios, + size: 16, + color: ColorsManager.greyColor, + ), + ), + ), + const Divider(color: ColorsManager.dividerColor), + Padding( + padding: const EdgeInsets.all(10.0), + child: infoRow( + label: 'Virtual Address:', + value: deviceInfo.productUuid, + trailing: InkWell( + onTap: () { + Clipboard.setData( + ClipboardData(text: device.productUuid ?? ''), + ); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Virtual Address copied to clipboard'), + ), + ); + }, + child: const Icon( + Icons.copy, + size: 16, + color: ColorsManager.greyColor, + ), + ), + ), + ), + const Divider(color: ColorsManager.dividerColor), + Padding( + padding: const EdgeInsets.all(10.0), + child: infoRow( + label: 'MAC Address:', + value: deviceInfo.macAddress), + ), + const SizedBox( + height: 5, + ), + ], + ), + ), + const SizedBox(height: 32), + + /// Remove Device Button + SizedBox( + width: double.infinity, + child: InkWell( + onTap: () { + _bloc.add(DeleteDeviceEvent()); + }, + child: const DefaultContainer( + padding: EdgeInsets.all(25), + child: Center( + child: Text( + 'Remove Device', + style: TextStyle(color: ColorsManager.red), + ), + ), + ), + ), + ) + ], + ), + ); + })); + } +} + +class DeviceTypeHelper { + static const Map _iconMap = { + 'AC': Assets.ac, + 'GW': Assets.gateway, + 'CPS': Assets.sensors, + 'DL': Assets.doorLock, + 'WPS': Assets.sensors, + '3G': Assets.gangSwitch, + '2G': Assets.twoGang, + '1G': Assets.oneGang, + 'CUR': Assets.curtain, + 'WH': Assets.waterHeater, + 'DS': Assets.doorSensor, + '1GT': Assets.oneTouchSwitch, + '2GT': Assets.twoTouchSwitch, + '3GT': Assets.threeTouchSwitch, + 'GD': Assets.garageDoor, + 'WL': Assets.waterLeakNormal, + 'NCPS': Assets.sensors, + }; + + static String getDeviceIconByTypeCode(String? typeCode) { + if (typeCode == null) return Assets.logoHorizontal; + return _iconMap[typeCode] ?? Assets.logoHorizontal; + } +} diff --git a/lib/services/devices_mang_api.dart b/lib/services/devices_mang_api.dart index b4de6326..97ac95d8 100644 --- a/lib/services/devices_mang_api.dart +++ b/lib/services/devices_mang_api.dart @@ -91,7 +91,8 @@ class DevicesManagementApi { } } - Future deviceBatchControl(List uuids, String code, dynamic value) async { + Future deviceBatchControl( + List uuids, String code, dynamic value) async { try { final body = { 'devicesUuid': uuids, @@ -116,7 +117,8 @@ class DevicesManagementApi { } } - static Future> getDevicesByGatewayId(String gatewayId) async { + static Future> getDevicesByGatewayId( + String gatewayId) async { final response = await HTTPService().get( path: ApiEndpoints.gatewayApi.replaceAll('{gatewayUuid}', gatewayId), showServerMessage: false, @@ -150,7 +152,9 @@ class DevicesManagementApi { String code, ) async { final response = await HTTPService().get( - path: ApiEndpoints.getDeviceLogs.replaceAll('{uuid}', uuid).replaceAll('{code}', code), + path: ApiEndpoints.getDeviceLogs + .replaceAll('{uuid}', uuid) + .replaceAll('{code}', code), showServerMessage: false, expectedResponseModel: (json) { return DeviceReport.fromJson(json['data']); @@ -223,7 +227,8 @@ class DevicesManagementApi { } } - Future addScheduleRecord(ScheduleEntry sendSchedule, String uuid) async { + Future addScheduleRecord( + ScheduleEntry sendSchedule, String uuid) async { try { final response = await HTTPService().post( path: ApiEndpoints.scheduleByDeviceId.replaceAll('{deviceUuid}', uuid), @@ -240,7 +245,8 @@ class DevicesManagementApi { } } - Future> getDeviceSchedules(String uuid, String category) async { + Future> getDeviceSchedules( + String uuid, String category) async { try { final response = await HTTPService().get( path: ApiEndpoints.getScheduleByDeviceId @@ -263,7 +269,9 @@ class DevicesManagementApi { } Future updateScheduleRecord( - {required bool enable, required String uuid, required String scheduleId}) async { + {required bool enable, + required String uuid, + required String scheduleId}) async { try { final response = await HTTPService().put( path: ApiEndpoints.updateScheduleByDeviceId @@ -284,7 +292,8 @@ class DevicesManagementApi { } } - Future editScheduleRecord(String uuid, ScheduleEntry newSchedule) async { + Future editScheduleRecord( + String uuid, ScheduleEntry newSchedule) async { try { final response = await HTTPService().put( path: ApiEndpoints.scheduleByDeviceId.replaceAll('{deviceUuid}', uuid), @@ -335,4 +344,46 @@ class DevicesManagementApi { return false; } } + + static Future> putDeviceName( + {required String deviceId, required String deviceName}) async { + try { + final response = await HTTPService().put( + path: ApiEndpoints.deviceByUuid.replaceAll('{deviceUuid}', deviceId), + body: {"deviceName": deviceName}, + expectedResponseModel: (json) { + return json['data']; + }, + ); + return response; + } catch (e) { + rethrow; + } + } + + static Future getDeviceInfo(String deviceId) async { + final response = await HTTPService().get( + path: ApiEndpoints.deviceByUuid.replaceAll('{deviceUuid}', deviceId), + showServerMessage: false, + expectedResponseModel: (json) { + return json['data'] as Map; + }); + return response; + } + static Future resetDevise({ + String? devicesUuid, + }) async { + final response = await HTTPService().post( + path: ApiEndpoints.resetDevice.replaceAll('{deviceUuid}', devicesUuid!), + showServerMessage: false, + body: { + "devicesUuid": [devicesUuid] + }, + expectedResponseModel: (json) { + return json; + }, + ); + return response; + } + } diff --git a/lib/services/space_mana_api.dart b/lib/services/space_mana_api.dart index 19e219b6..048c7b40 100644 --- a/lib/services/space_mana_api.dart +++ b/lib/services/space_mana_api.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/bloc/sub_space_model.dart'; import 'package:syncrow_web/pages/space_tree/model/pagination_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/create_subspace_model.dart'; @@ -12,14 +13,16 @@ import 'package:syncrow_web/utils/constants/api_const.dart'; class CommunitySpaceManagementApi { // Community Management APIs - Future> fetchCommunities(String projectId, {int page = 1}) async { + Future> fetchCommunities(String projectId, + {int page = 1}) async { try { List allCommunities = []; bool hasNext = true; while (hasNext) { await HTTPService().get( - path: ApiEndpoints.getCommunityList.replaceAll('{projectId}', projectId), + path: ApiEndpoints.getCommunityList + .replaceAll('{projectId}', projectId), queryParameters: { 'page': page, }, @@ -55,8 +58,14 @@ class CommunitySpaceManagementApi { try { bool hasNext = false; await HTTPService().get( - path: ApiEndpoints.getCommunityList.replaceAll('{projectId}', projectId), - queryParameters: {'page': page, 'includeSpaces': true, 'size': 25, 'search': search}, + path: + ApiEndpoints.getCommunityList.replaceAll('{projectId}', projectId), + queryParameters: { + 'page': page, + 'includeSpaces': true, + 'size': 25, + 'search': search + }, expectedResponseModel: (json) { try { List jsonData = json['data'] ?? []; @@ -68,7 +77,10 @@ class CommunitySpaceManagementApi { page = currentPage + 1; paginationModel = PaginationModel( - pageNum: page, hasNext: hasNext, size: 25, communities: communityList); + pageNum: page, + hasNext: hasNext, + size: 25, + communities: communityList); return paginationModel; } catch (_) { hasNext = false; @@ -83,7 +95,8 @@ class CommunitySpaceManagementApi { Future getCommunityById(String communityId) async { try { final response = await HTTPService().get( - path: ApiEndpoints.getCommunityById.replaceAll('{communityId}', communityId), + path: ApiEndpoints.getCommunityById + .replaceAll('{communityId}', communityId), expectedResponseModel: (json) { return CommunityModel.fromJson(json['data']); }, @@ -95,7 +108,8 @@ class CommunitySpaceManagementApi { } } - Future createCommunity(String name, String description, String projectId) async { + Future createCommunity( + String name, String description, String projectId) async { try { final response = await HTTPService().post( path: ApiEndpoints.createCommunity.replaceAll('{projectId}', projectId), @@ -114,7 +128,8 @@ class CommunitySpaceManagementApi { } } - Future updateCommunity(String communityId, String name, String projectId) async { + Future updateCommunity( + String communityId, String name, String projectId) async { try { final response = await HTTPService().put( path: ApiEndpoints.updateCommunity @@ -151,7 +166,8 @@ class CommunitySpaceManagementApi { } } - Future fetchSpaces(String communityId, String projectId) async { + Future fetchSpaces( + String communityId, String projectId) async { try { final response = await HTTPService().get( path: ApiEndpoints.listSpaces @@ -177,7 +193,8 @@ class CommunitySpaceManagementApi { } } - Future getSpace(String communityId, String spaceId, String projectId) async { + Future getSpace( + String communityId, String spaceId, String projectId) async { try { final response = await HTTPService().get( path: ApiEndpoints.getSpace @@ -289,7 +306,8 @@ class CommunitySpaceManagementApi { } } - Future deleteSpace(String communityId, String spaceId, String projectId) async { + Future deleteSpace( + String communityId, String spaceId, String projectId) async { try { final response = await HTTPService().delete( path: ApiEndpoints.deleteSpace @@ -307,15 +325,17 @@ class CommunitySpaceManagementApi { } } - Future> getSpaceHierarchy(String communityId, String projectId) async { + Future> getSpaceHierarchy( + String communityId, String projectId) async { try { final response = await HTTPService().get( path: ApiEndpoints.getSpaceHierarchy .replaceAll('{communityId}', communityId) .replaceAll('{projectId}', projectId), expectedResponseModel: (json) { - final spaceModels = - (json['data'] as List).map((spaceJson) => SpaceModel.fromJson(spaceJson)).toList(); + final spaceModels = (json['data'] as List) + .map((spaceJson) => SpaceModel.fromJson(spaceJson)) + .toList(); return spaceModels; }, @@ -327,15 +347,17 @@ class CommunitySpaceManagementApi { } } - Future> getSpaceOnlyWithDevices({String? communityId, String? projectId}) async { + Future> getSpaceOnlyWithDevices( + {String? communityId, String? projectId}) async { try { final response = await HTTPService().get( path: ApiEndpoints.spaceOnlyWithDevices .replaceAll('{communityId}', communityId!) .replaceAll('{projectId}', projectId!), expectedResponseModel: (json) { - final spaceModels = - (json['data'] as List).map((spaceJson) => SpaceModel.fromJson(spaceJson)).toList(); + final spaceModels = (json['data'] as List) + .map((spaceJson) => SpaceModel.fromJson(spaceJson)) + .toList(); return spaceModels; }, ); @@ -345,4 +367,36 @@ class CommunitySpaceManagementApi { return []; } } + + static Future> getSubSpaceBySpaceId( + String communityId, String spaceId, String projectId) async { + try { + // Construct the API path + final path = ApiEndpoints.listSubspace + .replaceFirst('{communityUuid}', communityId) + .replaceFirst('{spaceUuid}', spaceId) + .replaceAll('{projectUuid}', projectId); + + final response = await HTTPService().get( + path: path, + queryParameters: {"page": 1, "pageSize": 10}, + showServerMessage: false, + expectedResponseModel: (json) { + List rooms = []; + if (json['data'] != null) { + for (var subspace in json['data']) { + rooms.add(SubSpaceModel.fromJson(subspace)); + } + } else { + print("Warning: 'data' key is missing or null in response JSON."); + } + return rooms; + }, + ); + + return response; + } catch (error, stackTrace) { + return []; // Return an empty list if there's an error + } + } } diff --git a/lib/utils/color_manager.dart b/lib/utils/color_manager.dart index 41ceb29a..50170ed9 100644 --- a/lib/utils/color_manager.dart +++ b/lib/utils/color_manager.dart @@ -83,4 +83,7 @@ abstract class ColorsManager { static const Color maxPurpleDot = Color(0xFF5F00BD); static const Color minBlue = Color(0xFF93AAFD); static const Color minBlueDot = Color(0xFF023DFE); + static const Color grey25 = Color(0xFFF9F9F9); + + } diff --git a/lib/utils/constants/api_const.dart b/lib/utils/constants/api_const.dart index 454ec46d..472055bd 100644 --- a/lib/utils/constants/api_const.dart +++ b/lib/utils/constants/api_const.dart @@ -60,9 +60,12 @@ abstract class ApiEndpoints { '/devices/{uuid}/report-logs?code={code}&startTime={startTime}&endTime={endTime}'; static const String scheduleByDeviceId = '/schedule/{deviceUuid}'; - static const String getScheduleByDeviceId = '/schedule/{deviceUuid}?category={category}'; - static const String deleteScheduleByDeviceId = '/schedule/{deviceUuid}/{scheduleUuid}'; - static const String updateScheduleByDeviceId = '/schedule/enable/{deviceUuid}'; + static const String getScheduleByDeviceId = + '/schedule/{deviceUuid}?category={category}'; + static const String deleteScheduleByDeviceId = + '/schedule/{deviceUuid}/{scheduleUuid}'; + static const String updateScheduleByDeviceId = + '/schedule/enable/{deviceUuid}'; static const String factoryReset = '/devices/batch'; //product @@ -124,4 +127,10 @@ abstract class ApiEndpoints { '/projects/{projectId}/communities/{communityId}/spaces/{unitUuid}/automations'; static const String spaceOnlyWithDevices = '/projects/{projectId}/communities/{communityId}/spaces?onlyWithDevices=true'; + + static const String listSubspace = + '/projects/{projectUuid}/communities/{communityUuid}/spaces/{spaceUuid}/subspaces'; + static const String deviceByUuid = '/devices/{deviceUuid}'; + + static const String resetDevice = '/factory/reset/{deviceUuid}'; } diff --git a/lib/utils/constants/assets.dart b/lib/utils/constants/assets.dart index 13d51ea5..515ede28 100644 --- a/lib/utils/constants/assets.dart +++ b/lib/utils/constants/assets.dart @@ -452,4 +452,9 @@ class Assets { 'assets/icons/refresh_status_icon.svg'; static const String energyConsumedIcon = 'assets/icons/energy_consumed_icon.svg'; + static const String closeSettingsIcon = + 'assets/icons/close_settings_icon.svg'; + + static const String editNameIconSettings = + 'assets/icons/edit_name_icon_settings.svg'; } diff --git a/lib/web_layout/default_container.dart b/lib/web_layout/default_container.dart new file mode 100644 index 00000000..e0a71b04 --- /dev/null +++ b/lib/web_layout/default_container.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; + +class DefaultContainer extends StatelessWidget { + const DefaultContainer({ + super.key, + required this.child, + this.height, + this.width, + this.color, + this.boxConstraints, + this.margin, + this.padding, + this.onTap, + this.borderRadius, + }); + + final double? height; + final double? width; + final Widget child; + final BoxConstraints? boxConstraints; + final EdgeInsets? margin; + final EdgeInsets? padding; + final Color? color; + final Function()? onTap; + final BorderRadius? borderRadius; + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(20), + child: Container( + height: height, + width: width, + margin: margin ?? const EdgeInsets.only(right: 3, bottom: 3), + constraints: boxConstraints, + decoration: BoxDecoration( + color: color ?? Colors.white, + borderRadius: borderRadius ?? BorderRadius.circular(20), + ), + padding: padding ?? const EdgeInsets.all(10), + child: child, + ), + ); + } +} From 305d695358bfa707bdf7a8bc53885555b761c05a Mon Sep 17 00:00:00 2001 From: mohammad Date: Sun, 1 Jun 2025 13:12:58 +0300 Subject: [PATCH 03/58] Refactor energy clamp dialog to handle empty functions list gracefully --- .../energy_clamp_dialog.dart | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/lib/pages/routines/widgets/routine_dialogs/power_clamp_enargy/energy_clamp_dialog.dart b/lib/pages/routines/widgets/routine_dialogs/power_clamp_enargy/energy_clamp_dialog.dart index c5bf8828..f736e91d 100644 --- a/lib/pages/routines/widgets/routine_dialogs/power_clamp_enargy/energy_clamp_dialog.dart +++ b/lib/pages/routines/widgets/routine_dialogs/power_clamp_enargy/energy_clamp_dialog.dart @@ -99,7 +99,25 @@ class _EnergyClampDialogState extends State { mainAxisSize: MainAxisSize.min, children: [ const DialogHeader('Energy Clamp Conditions'), - Expanded(child: _buildMainContent(context, state)), + Expanded( + child: _functions.isEmpty + ? SizedBox( + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Center( + child: Text( + 'You Cant add\n the Power Clamp to Then Section', + textAlign: TextAlign.center, + style: context.textTheme.bodyMedium!.copyWith( + color: ColorsManager.red, + fontWeight: FontWeight.w400), + )), + ], + ), + ) + : _buildMainContent(context, state)), _buildDialogFooter(context, state), ], ), From 8916000696c6512768923d8580c6533f839f00cd Mon Sep 17 00:00:00 2001 From: mohammad Date: Sun, 1 Jun 2025 14:11:21 +0300 Subject: [PATCH 04/58] Refactor visibility logic in Energy Clamp Dialog to handle empty functions list more elegantly --- .../energy_clamp_dialog.dart | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/lib/pages/routines/widgets/routine_dialogs/power_clamp_enargy/energy_clamp_dialog.dart b/lib/pages/routines/widgets/routine_dialogs/power_clamp_enargy/energy_clamp_dialog.dart index f736e91d..291abf59 100644 --- a/lib/pages/routines/widgets/routine_dialogs/power_clamp_enargy/energy_clamp_dialog.dart +++ b/lib/pages/routines/widgets/routine_dialogs/power_clamp_enargy/energy_clamp_dialog.dart @@ -100,24 +100,26 @@ class _EnergyClampDialogState extends State { children: [ const DialogHeader('Energy Clamp Conditions'), Expanded( - child: _functions.isEmpty - ? SizedBox( - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Center( - child: Text( - 'You Cant add\n the Power Clamp to Then Section', - textAlign: TextAlign.center, - style: context.textTheme.bodyMedium!.copyWith( - color: ColorsManager.red, - fontWeight: FontWeight.w400), - )), - ], - ), - ) - : _buildMainContent(context, state)), + child: Visibility( + visible: _functions.isNotEmpty, + replacement: SizedBox( + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Center( + child: Text( + 'You Cant add\n the Power Clamp to Then Section', + textAlign: TextAlign.center, + style: context.textTheme.bodyMedium!.copyWith( + color: ColorsManager.red, + fontWeight: FontWeight.w400), + )), + ], + ), + ), + child: _buildMainContent(context, state), + )), _buildDialogFooter(context, state), ], ), From 7c55e8bbf94f04a0694e68b0cba3ed81a750ec99 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 1 Jun 2025 11:27:34 +0300 Subject: [PATCH 05/58] Prepared widgets for the aqi distribution chart. --- .../air_quality/views/air_quality_view.dart | 13 +++++-- .../widgets/aqi_distribution_chart.dart | 10 ++++++ .../widgets/aqi_distribution_chart_box.dart | 29 +++++++++++++++ .../widgets/aqi_distribution_chart_title.dart | 35 +++++++++++++++++++ 4 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart create mode 100644 lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart create mode 100644 lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.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 17ecbc22..b6d403eb 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,5 +1,6 @@ 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/range_of_aqi_chart_box.dart'; class AirQualityView extends StatelessWidget { @@ -23,8 +24,14 @@ class AirQualityView extends StatelessWidget { height: height * 1.2, child: const AirQualityEndSideWidget(), ), - SizedBox(height: height * 0.5, child: const RangeOfAqiChartBox()), - SizedBox(height: height * 0.5, child: const Placeholder()), + SizedBox( + height: height * 0.5, + child: const RangeOfAqiChartBox(), + ), + SizedBox( + height: height * 0.5, + child: const AqiDistributionChartBox(), + ), ], ), ); @@ -46,7 +53,7 @@ class AirQualityView extends StatelessWidget { spacing: 20, children: [ Expanded(child: RangeOfAqiChartBox()), - Expanded(child: Placeholder()), + Expanded(child: AqiDistributionChartBox()), ], ), ), 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 new file mode 100644 index 00000000..254727aa --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class AqiDistributionChart extends StatelessWidget { + const AqiDistributionChart({super.key}); + + @override + Widget build(BuildContext context) { + return const Placeholder(); + } +} diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart new file mode 100644 index 00000000..77eacfa5 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart'; +import 'package:syncrow_web/utils/style.dart'; + +class AqiDistributionChartBox extends StatelessWidget { + const AqiDistributionChartBox({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsetsDirectional.all(30), + decoration: subSectionContainerDecoration.copyWith( + borderRadius: BorderRadius.circular(30), + ), + child: const Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AqiDistributionChartTitle(isLoading: false), + SizedBox(height: 10), + Divider(), + SizedBox(height: 20), + Expanded(child: AqiDistributionChart()), + ], + ), + ); + } +} 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 new file mode 100644 index 00000000..a1272a10 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart'; +import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart'; + +class AqiDistributionChartTitle extends StatelessWidget { + const AqiDistributionChartTitle({required this.isLoading, super.key}); + + final bool isLoading; + + @override + Widget build(BuildContext context) { + return Row( + spacing: 11, + children: [ + const Expanded( + flex: 3, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: AlignmentDirectional.centerStart, + child: ChartTitle( + title: Text('Distribution over Air Quality Index'), + ), + ), + ), + FittedBox( + alignment: AlignmentDirectional.centerEnd, + fit: BoxFit.scaleDown, + child: AqiTypeDropdown( + onChanged: (value) {}, + ), + ), + ], + ); + } +} From 5940e5282679ecb04f17ce24034d9e6336c6309b Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 1 Jun 2025 11:50:34 +0300 Subject: [PATCH 06/58] Implemented an initial version of `AqiDistributionChart`. --- .../models/air_quality_data_model.dart | 31 +++ .../widgets/aqi_distribution_chart.dart | 199 +++++++++++++++++- .../widgets/aqi_distribution_chart_box.dart | 44 +++- 3 files changed, 266 insertions(+), 8 deletions(-) create mode 100644 lib/pages/analytics/models/air_quality_data_model.dart diff --git a/lib/pages/analytics/models/air_quality_data_model.dart b/lib/pages/analytics/models/air_quality_data_model.dart new file mode 100644 index 00000000..639bcb2e --- /dev/null +++ b/lib/pages/analytics/models/air_quality_data_model.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class AirQualityDataModel { + const AirQualityDataModel({ + required this.date, + this.aqi, + this.pm25, + this.pm10, + this.hcho, + this.tvoc, + this.co2, + }); + + final DateTime date; + final double? aqi; + final double? pm25; + final double? pm10; + final double? hcho; + final double? tvoc; + final double? co2; + + static const Map metricColors = { + 'aqi': ColorsManager.goodGreen, + 'pm25': ColorsManager.moderateYellow, + 'pm10': ColorsManager.poorOrange, + 'hcho': ColorsManager.unhealthyRed, + 'tvoc': ColorsManager.severePink, + 'co2': ColorsManager.hazardousPurple, + }; +} 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 254727aa..1038aaa8 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 @@ -1,10 +1,205 @@ +import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.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/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; class AqiDistributionChart extends StatelessWidget { - const AqiDistributionChart({super.key}); + const AqiDistributionChart({super.key, required this.chartData}); + final List chartData; + + static const _rodStackItemsSpacing = 4; @override Widget build(BuildContext context) { - return const Placeholder(); + return BarChart( + BarChartData( + alignment: BarChartAlignment.spaceAround, + barTouchData: _barTouchData(context), + titlesData: _titlesData(context), + gridData: EnergyManagementChartsHelper.gridData( + horizontalInterval: 100, + ), + borderData: EnergyManagementChartsHelper.borderData(), + barGroups: _buildBarGroups(), + groupsSpace: 12, + ), + duration: Duration.zero, + ); + } + + List _buildBarGroups() { + return List.generate(chartData.length, (index) { + final data = chartData[index]; + final stackItems = []; + double currentY = 0; + + if (data.aqi != null) { + stackItems.add( + BarChartRodData( + fromY: currentY, + toY: currentY + data.aqi!, + color: AirQualityDataModel.metricColors['aqi']!, + borderRadius: BorderRadius.circular(10), + width: 20, + ), + ); + currentY += data.aqi! + _rodStackItemsSpacing; + } + + if (data.pm25 != null) { + stackItems.add( + BarChartRodData( + fromY: currentY, + toY: currentY + data.pm25!, + color: AirQualityDataModel.metricColors['pm25']!, + borderRadius: BorderRadius.circular(10), + width: 20, + ), + ); + currentY += data.pm25! + _rodStackItemsSpacing; + } + + if (data.pm10 != null) { + stackItems.add( + BarChartRodData( + fromY: currentY, + toY: currentY + data.pm10!, + color: AirQualityDataModel.metricColors['pm10']!, + borderRadius: BorderRadius.circular(10), + width: 20, + ), + ); + currentY += data.pm10! + 2; + } + + if (data.hcho != null) { + stackItems.add( + BarChartRodData( + fromY: currentY, + toY: currentY + data.hcho!, + color: AirQualityDataModel.metricColors['hcho']!, + borderRadius: BorderRadius.circular(10), + width: 20, + ), + ); + currentY += data.hcho! + 2; + } + + if (data.tvoc != null) { + stackItems.add( + BarChartRodData( + fromY: currentY, + toY: currentY + data.tvoc!, + color: AirQualityDataModel.metricColors['tvoc']!, + borderRadius: BorderRadius.circular(10), + width: 20, + ), + ); + currentY += data.tvoc! + 2; + } + + if (data.co2 != null) { + stackItems.add( + BarChartRodData( + fromY: currentY, + toY: currentY + data.co2!, + color: AirQualityDataModel.metricColors['co2']!, + borderRadius: BorderRadius.circular(10), + width: 20, + ), + ); + currentY += data.co2! + 2; + } + return BarChartGroupData( + x: index, + barRods: stackItems, + groupVertically: true, + ); + }); + } + + BarTouchData _barTouchData(BuildContext context) { + return BarTouchData( + touchTooltipData: BarTouchTooltipData( + getTooltipColor: (_) => ColorsManager.whiteColors, + tooltipBorder: const BorderSide( + color: ColorsManager.semiTransparentBlack, + ), + tooltipRoundedRadius: 16, + tooltipPadding: const EdgeInsets.all(8), + getTooltipItem: (group, groupIndex, rod, rodIndex) { + final data = chartData[group.x.toInt()]; + final stackItems = rod.rodStackItems; + + return BarTooltipItem( + '${data.date.day}/${data.date.month}\n', + context.textTheme.bodyMedium!.copyWith( + color: ColorsManager.blackColor, + fontSize: 14, + ), + children: stackItems.map((item) { + final metricName = AirQualityDataModel.metricColors.entries + .firstWhere((entry) => entry.value == item.color) + .key + .toUpperCase(); + return TextSpan( + text: '$metricName: ${item.toY - item.fromY}\n', + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.blackColor, + fontSize: 12, + ), + ); + }).toList(), + ); + }, + ), + ); + } + + FlTitlesData _titlesData(BuildContext context) { + final titlesData = EnergyManagementChartsHelper.titlesData(context); + + return titlesData.copyWith( + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (value, meta) { + if (value.toInt() >= chartData.length) return const SizedBox.shrink(); + return Padding( + padding: const EdgeInsetsDirectional.only(top: 20.0), + child: Text( + chartData[value.toInt()].date.day.toString(), + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.lightGreyColor, + fontSize: 12, + ), + ), + ); + }, + reservedSize: 32, + ), + ), + leftTitles: titlesData.leftTitles.copyWith( + sideTitles: titlesData.leftTitles.sideTitles.copyWith( + reservedSize: 70, + getTitlesWidget: (value, meta) => Padding( + padding: const EdgeInsetsDirectional.only(end: 12), + child: FittedBox( + alignment: AlignmentDirectional.centerStart, + fit: BoxFit.scaleDown, + child: Text( + value.toInt().toString(), + style: context.textTheme.bodySmall?.copyWith( + fontSize: 12, + color: ColorsManager.lightGreyColor, + ), + ), + ), + ), + ), + ), + ); } } diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart index 77eacfa5..f1ccc0ab 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart'; import 'package:syncrow_web/utils/style.dart'; @@ -13,15 +14,46 @@ class AqiDistributionChartBox extends StatelessWidget { decoration: subSectionContainerDecoration.copyWith( borderRadius: BorderRadius.circular(30), ), - child: const Column( + child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - AqiDistributionChartTitle(isLoading: false), - SizedBox(height: 10), - Divider(), - SizedBox(height: 20), - Expanded(child: AqiDistributionChart()), + const AqiDistributionChartTitle(isLoading: false), + const SizedBox(height: 10), + const Divider(), + const SizedBox(height: 20), + Expanded( + child: AqiDistributionChart( + chartData: [ + AirQualityDataModel( + date: DateTime.now(), + aqi: 50, + pm25: 30, + pm10: 40, + co2: 120, + hcho: 10, + tvoc: 50, + ), + AirQualityDataModel( + date: DateTime.now(), + aqi: 50, + pm25: 25, + pm10: 40, + co2: 120, + hcho: 10, + tvoc: 50, + ), + AirQualityDataModel( + date: DateTime.now(), + aqi: 50, + pm25: 25, + pm10: 40, + co2: 120, + hcho: 10, + tvoc: 50, + ), + ], + )), ], ), ); From 1998a629b637cb3d65f8b9c509de63aa78e91adf Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 1 Jun 2025 12:20:08 +0300 Subject: [PATCH 07/58] added some opacity to metric colors. --- .../analytics/models/air_quality_data_model.dart | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/pages/analytics/models/air_quality_data_model.dart b/lib/pages/analytics/models/air_quality_data_model.dart index 639bcb2e..2f5b3b3b 100644 --- a/lib/pages/analytics/models/air_quality_data_model.dart +++ b/lib/pages/analytics/models/air_quality_data_model.dart @@ -20,12 +20,12 @@ class AirQualityDataModel { final double? tvoc; final double? co2; - static const Map metricColors = { - 'aqi': ColorsManager.goodGreen, - 'pm25': ColorsManager.moderateYellow, - 'pm10': ColorsManager.poorOrange, - 'hcho': ColorsManager.unhealthyRed, - 'tvoc': ColorsManager.severePink, - 'co2': ColorsManager.hazardousPurple, + static final Map metricColors = { + 'aqi': ColorsManager.goodGreen.withValues(alpha: 0.7), + 'pm25': ColorsManager.moderateYellow.withValues(alpha: 0.7), + 'pm10': ColorsManager.poorOrange.withValues(alpha: 0.7), + 'hcho': ColorsManager.unhealthyRed.withValues(alpha: 0.7), + 'tvoc': ColorsManager.severePink.withValues(alpha: 0.7), + 'co2': ColorsManager.hazardousPurple.withValues(alpha: 0.7), }; } From 10f35d37477c8038796410db92b36ed706e8053a Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 1 Jun 2025 12:20:27 +0300 Subject: [PATCH 08/58] added more mock data to `AqiDistributionChart`. --- .../widgets/aqi_distribution_chart_box.dart | 63 ++++++++++--------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart index f1ccc0ab..eb0ab19e 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart @@ -23,37 +23,38 @@ class AqiDistributionChartBox extends StatelessWidget { const Divider(), const SizedBox(height: 20), Expanded( - child: AqiDistributionChart( - chartData: [ - AirQualityDataModel( - date: DateTime.now(), - aqi: 50, - pm25: 30, - pm10: 40, - co2: 120, - hcho: 10, - tvoc: 50, - ), - AirQualityDataModel( - date: DateTime.now(), - aqi: 50, - pm25: 25, - pm10: 40, - co2: 120, - hcho: 10, - tvoc: 50, - ), - AirQualityDataModel( - date: DateTime.now(), - aqi: 50, - pm25: 25, - pm10: 40, - co2: 120, - hcho: 10, - tvoc: 50, - ), - ], - )), + child: AqiDistributionChart( + chartData: [ + AirQualityDataModel( + date: DateTime.now(), + aqi: 20, + pm25: 10, + pm10: 40, + co2: 10, + hcho: 0, + tvoc: 20, + ), + AirQualityDataModel( + date: DateTime.now(), + aqi: 20, + pm25: 10, + pm10: 40, + co2: 10, + hcho: 0, + tvoc: 20, + ), + AirQualityDataModel( + date: DateTime.now(), + aqi: 20, + pm25: 10, + pm10: 40, + co2: 10, + hcho: 0, + tvoc: 20, + ), + ], + ), + ), ], ), ); From 7b31914e1ccd5f12f70a0f2aa0389365f858577f Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 1 Jun 2025 12:20:44 +0300 Subject: [PATCH 09/58] made progress towards aqi distribution chart. --- .../widgets/aqi_distribution_chart.dart | 125 ++++++++++-------- 1 file changed, 67 insertions(+), 58 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 1038aaa8..0575dfff 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 @@ -9,21 +9,22 @@ class AqiDistributionChart extends StatelessWidget { const AqiDistributionChart({super.key, required this.chartData}); final List chartData; - static const _rodStackItemsSpacing = 4; + static const _rodStackItemsSpacing = 0.4; + static const _barWidth = 20.0; + static final _barBorderRadius = BorderRadius.circular(22); @override Widget build(BuildContext context) { return BarChart( BarChartData( - alignment: BarChartAlignment.spaceAround, - barTouchData: _barTouchData(context), - titlesData: _titlesData(context), + maxY: 100.1, gridData: EnergyManagementChartsHelper.gridData( - horizontalInterval: 100, + horizontalInterval: 20, ), borderData: EnergyManagementChartsHelper.borderData(), + barTouchData: _barTouchData(context), + titlesData: _titlesData(context), barGroups: _buildBarGroups(), - groupsSpace: 12, ), duration: Duration.zero, ); @@ -41,8 +42,8 @@ class AqiDistributionChart extends StatelessWidget { fromY: currentY, toY: currentY + data.aqi!, color: AirQualityDataModel.metricColors['aqi']!, - borderRadius: BorderRadius.circular(10), - width: 20, + borderRadius: _barBorderRadius, + width: _barWidth, ), ); currentY += data.aqi! + _rodStackItemsSpacing; @@ -54,8 +55,8 @@ class AqiDistributionChart extends StatelessWidget { fromY: currentY, toY: currentY + data.pm25!, color: AirQualityDataModel.metricColors['pm25']!, - borderRadius: BorderRadius.circular(10), - width: 20, + borderRadius: _barBorderRadius, + width: _barWidth, ), ); currentY += data.pm25! + _rodStackItemsSpacing; @@ -67,11 +68,11 @@ class AqiDistributionChart extends StatelessWidget { fromY: currentY, toY: currentY + data.pm10!, color: AirQualityDataModel.metricColors['pm10']!, - borderRadius: BorderRadius.circular(10), - width: 20, + borderRadius: _barBorderRadius, + width: _barWidth, ), ); - currentY += data.pm10! + 2; + currentY += data.pm10! + _rodStackItemsSpacing; } if (data.hcho != null) { @@ -80,11 +81,11 @@ class AqiDistributionChart extends StatelessWidget { fromY: currentY, toY: currentY + data.hcho!, color: AirQualityDataModel.metricColors['hcho']!, - borderRadius: BorderRadius.circular(10), - width: 20, + borderRadius: _barBorderRadius, + width: _barWidth, ), ); - currentY += data.hcho! + 2; + currentY += data.hcho! + _rodStackItemsSpacing; } if (data.tvoc != null) { @@ -93,11 +94,11 @@ class AqiDistributionChart extends StatelessWidget { fromY: currentY, toY: currentY + data.tvoc!, color: AirQualityDataModel.metricColors['tvoc']!, - borderRadius: BorderRadius.circular(10), - width: 20, + borderRadius: _barBorderRadius, + width: _barWidth, ), ); - currentY += data.tvoc! + 2; + currentY += data.tvoc! + _rodStackItemsSpacing; } if (data.co2 != null) { @@ -106,11 +107,11 @@ class AqiDistributionChart extends StatelessWidget { fromY: currentY, toY: currentY + data.co2!, color: AirQualityDataModel.metricColors['co2']!, - borderRadius: BorderRadius.circular(10), - width: 20, + borderRadius: _barBorderRadius, + width: _barWidth, ), ); - currentY += data.co2! + 2; + currentY += data.co2! + _rodStackItemsSpacing; } return BarChartGroupData( x: index, @@ -143,7 +144,7 @@ class AqiDistributionChart extends StatelessWidget { final metricName = AirQualityDataModel.metricColors.entries .firstWhere((entry) => entry.value == item.color) .key - .toUpperCase(); + .toLowerCase(); return TextSpan( text: '$metricName: ${item.toY - item.fromY}\n', style: context.textTheme.bodySmall?.copyWith( @@ -159,47 +160,55 @@ class AqiDistributionChart extends StatelessWidget { } FlTitlesData _titlesData(BuildContext context) { - final titlesData = EnergyManagementChartsHelper.titlesData(context); + final titlesData = EnergyManagementChartsHelper.titlesData( + context, + leftTitlesInterval: 20, + ); - return titlesData.copyWith( - bottomTitles: AxisTitles( - sideTitles: SideTitles( - showTitles: true, - getTitlesWidget: (value, meta) { - if (value.toInt() >= chartData.length) return const SizedBox.shrink(); - return Padding( - padding: const EdgeInsetsDirectional.only(top: 20.0), - child: Text( - chartData[value.toInt()].date.day.toString(), - style: context.textTheme.bodySmall?.copyWith( - color: ColorsManager.lightGreyColor, - fontSize: 12, - ), - ), - ); - }, - reservedSize: 32, - ), - ), - leftTitles: titlesData.leftTitles.copyWith( - sideTitles: titlesData.leftTitles.sideTitles.copyWith( - reservedSize: 70, - getTitlesWidget: (value, meta) => Padding( - padding: const EdgeInsetsDirectional.only(end: 12), - child: FittedBox( - alignment: AlignmentDirectional.centerStart, - fit: BoxFit.scaleDown, - child: Text( - value.toInt().toString(), - style: context.textTheme.bodySmall?.copyWith( - fontSize: 12, - color: ColorsManager.lightGreyColor, - ), + final leftTitles = titlesData.leftTitles.copyWith( + sideTitles: titlesData.leftTitles.sideTitles.copyWith( + reservedSize: 70, + interval: 20, + maxIncluded: false, + minIncluded: true, + getTitlesWidget: (value, meta) => Padding( + padding: const EdgeInsetsDirectional.only(end: 12), + child: FittedBox( + alignment: AlignmentDirectional.centerStart, + fit: BoxFit.scaleDown, + child: Text( + '${value.toStringAsFixed(0)}%', + style: context.textTheme.bodySmall?.copyWith( + fontSize: 12, + color: ColorsManager.lightGreyColor, ), ), ), ), ), ); + + final bottomTitles = AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (value, _) => FittedBox( + alignment: AlignmentDirectional.bottomCenter, + fit: BoxFit.scaleDown, + child: Text( + chartData[value.toInt()].date.day.toString(), + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.lightGreyColor, + fontSize: 8, + ), + ), + ), + reservedSize: 36, + ), + ); + + return titlesData.copyWith( + leftTitles: leftTitles, + bottomTitles: bottomTitles, + ); } } From ca1feb96009cf2f2962d0b5a486f9f38365d306c Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 1 Jun 2025 12:36:09 +0300 Subject: [PATCH 10/58] made charts based on states and not based on metrics. --- .../models/air_quality_data_model.dart | 36 ++++++------- .../widgets/aqi_distribution_chart.dart | 52 ++++++++++--------- .../widgets/aqi_distribution_chart_box.dart | 42 +++++++-------- 3 files changed, 66 insertions(+), 64 deletions(-) diff --git a/lib/pages/analytics/models/air_quality_data_model.dart b/lib/pages/analytics/models/air_quality_data_model.dart index 2f5b3b3b..d65f1418 100644 --- a/lib/pages/analytics/models/air_quality_data_model.dart +++ b/lib/pages/analytics/models/air_quality_data_model.dart @@ -4,28 +4,28 @@ import 'package:syncrow_web/utils/color_manager.dart'; class AirQualityDataModel { const AirQualityDataModel({ required this.date, - this.aqi, - this.pm25, - this.pm10, - this.hcho, - this.tvoc, - this.co2, + this.good, + this.moderate, + this.poor, + this.unhealthy, + this.severe, + this.hazardous, }); final DateTime date; - final double? aqi; - final double? pm25; - final double? pm10; - final double? hcho; - final double? tvoc; - final double? co2; + final double? good; + final double? moderate; + final double? poor; + final double? unhealthy; + final double? severe; + final double? hazardous; static final Map metricColors = { - 'aqi': ColorsManager.goodGreen.withValues(alpha: 0.7), - 'pm25': ColorsManager.moderateYellow.withValues(alpha: 0.7), - 'pm10': ColorsManager.poorOrange.withValues(alpha: 0.7), - 'hcho': ColorsManager.unhealthyRed.withValues(alpha: 0.7), - 'tvoc': ColorsManager.severePink.withValues(alpha: 0.7), - 'co2': ColorsManager.hazardousPurple.withValues(alpha: 0.7), + 'good': ColorsManager.goodGreen.withValues(alpha: 0.7), + 'moderate': ColorsManager.moderateYellow.withValues(alpha: 0.7), + 'poor': ColorsManager.poorOrange.withValues(alpha: 0.7), + 'unhealthy': ColorsManager.unhealthyRed.withValues(alpha: 0.7), + 'severe': ColorsManager.severePink.withValues(alpha: 0.7), + 'hazardous': ColorsManager.hazardousPurple.withValues(alpha: 0.7), }; } 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 0575dfff..d3cab467 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 @@ -36,83 +36,84 @@ class AqiDistributionChart extends StatelessWidget { final stackItems = []; double currentY = 0; - if (data.aqi != null) { + if (data.good != null) { stackItems.add( BarChartRodData( fromY: currentY, - toY: currentY + data.aqi!, - color: AirQualityDataModel.metricColors['aqi']!, + toY: currentY + data.good!, + color: AirQualityDataModel.metricColors['good']!, borderRadius: _barBorderRadius, width: _barWidth, ), ); - currentY += data.aqi! + _rodStackItemsSpacing; + currentY += data.good! + _rodStackItemsSpacing; } - if (data.pm25 != null) { + if (data.moderate != null) { stackItems.add( BarChartRodData( fromY: currentY, - toY: currentY + data.pm25!, - color: AirQualityDataModel.metricColors['pm25']!, + toY: currentY + data.moderate!, + color: AirQualityDataModel.metricColors['moderate']!, borderRadius: _barBorderRadius, width: _barWidth, ), ); - currentY += data.pm25! + _rodStackItemsSpacing; + currentY += data.moderate! + _rodStackItemsSpacing; } - if (data.pm10 != null) { + if (data.poor != null) { stackItems.add( BarChartRodData( fromY: currentY, - toY: currentY + data.pm10!, - color: AirQualityDataModel.metricColors['pm10']!, + toY: currentY + data.poor!, + color: AirQualityDataModel.metricColors['poor']!, borderRadius: _barBorderRadius, width: _barWidth, ), ); - currentY += data.pm10! + _rodStackItemsSpacing; + currentY += data.poor! + _rodStackItemsSpacing; } - if (data.hcho != null) { + if (data.unhealthy != null) { stackItems.add( BarChartRodData( fromY: currentY, - toY: currentY + data.hcho!, - color: AirQualityDataModel.metricColors['hcho']!, + toY: currentY + data.unhealthy!, + color: AirQualityDataModel.metricColors['unhealthy']!, borderRadius: _barBorderRadius, width: _barWidth, ), ); - currentY += data.hcho! + _rodStackItemsSpacing; + currentY += data.unhealthy! + _rodStackItemsSpacing; } - if (data.tvoc != null) { + if (data.severe != null) { stackItems.add( BarChartRodData( fromY: currentY, - toY: currentY + data.tvoc!, - color: AirQualityDataModel.metricColors['tvoc']!, + toY: currentY + data.severe!, + color: AirQualityDataModel.metricColors['severe']!, borderRadius: _barBorderRadius, width: _barWidth, ), ); - currentY += data.tvoc! + _rodStackItemsSpacing; + currentY += data.severe! + _rodStackItemsSpacing; } - if (data.co2 != null) { + if (data.hazardous != null) { stackItems.add( BarChartRodData( fromY: currentY, - toY: currentY + data.co2!, - color: AirQualityDataModel.metricColors['co2']!, + toY: currentY + data.hazardous!, + color: AirQualityDataModel.metricColors['hazardous']!, borderRadius: _barBorderRadius, width: _barWidth, ), ); - currentY += data.co2! + _rodStackItemsSpacing; + currentY += data.hazardous! + _rodStackItemsSpacing; } + return BarChartGroupData( x: index, barRods: stackItems, @@ -146,7 +147,8 @@ class AqiDistributionChart extends StatelessWidget { .key .toLowerCase(); return TextSpan( - text: '$metricName: ${item.toY - item.fromY}\n', + text: + '$metricName: ${(item.toY - item.fromY).toStringAsFixed(1)}%\n', style: context.textTheme.bodySmall?.copyWith( color: ColorsManager.blackColor, fontSize: 12, diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart index eb0ab19e..ac770a4e 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart @@ -26,31 +26,31 @@ class AqiDistributionChartBox extends StatelessWidget { child: AqiDistributionChart( chartData: [ AirQualityDataModel( - date: DateTime.now(), - aqi: 20, - pm25: 10, - pm10: 40, - co2: 10, - hcho: 0, - tvoc: 20, + date: DateTime(2025, 5, 1), + good: 30, + moderate: 25, + poor: 15, + unhealthy: 10, + severe: 15, + hazardous: 5, ), AirQualityDataModel( - date: DateTime.now(), - aqi: 20, - pm25: 10, - pm10: 40, - co2: 10, - hcho: 0, - tvoc: 20, + date: DateTime(2025, 5, 2), + good: 40, + moderate: 20, + poor: 20, + unhealthy: 10, + severe: 5, + hazardous: 5, ), AirQualityDataModel( - date: DateTime.now(), - aqi: 20, - pm25: 10, - pm10: 40, - co2: 10, - hcho: 0, - tvoc: 20, + date: DateTime(2025, 5, 3), + good: 35, + moderate: 30, + poor: 15, + unhealthy: 10, + severe: 5, + hazardous: 5, ), ], ), From 44c4648941f5c9d6178c759a8e3a3dc889aa16ed Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 1 Jun 2025 12:45:41 +0300 Subject: [PATCH 11/58] made the first element of the bar rods to have only a top sides radius to match the design. --- .../widgets/aqi_distribution_chart.dart | 50 ++++++++++++++++--- .../widgets/aqi_distribution_chart_box.dart | 20 ++++---- 2 files changed, 54 insertions(+), 16 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 d3cab467..aab0a607 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 @@ -35,6 +35,7 @@ class AqiDistributionChart extends StatelessWidget { final data = chartData[index]; final stackItems = []; double currentY = 0; + bool isFirstElement = true; if (data.good != null) { stackItems.add( @@ -42,11 +43,18 @@ class AqiDistributionChart extends StatelessWidget { fromY: currentY, toY: currentY + data.good!, color: AirQualityDataModel.metricColors['good']!, - borderRadius: _barBorderRadius, + borderRadius: isFirstElement + ? const BorderRadius.only( + topLeft: Radius.circular(22), + topRight: Radius.circular(22), + ) + // ignore: dead_code + : _barBorderRadius, width: _barWidth, ), ); currentY += data.good! + _rodStackItemsSpacing; + isFirstElement = false; } if (data.moderate != null) { @@ -55,11 +63,17 @@ class AqiDistributionChart extends StatelessWidget { fromY: currentY, toY: currentY + data.moderate!, color: AirQualityDataModel.metricColors['moderate']!, - borderRadius: _barBorderRadius, + borderRadius: isFirstElement + ? const BorderRadius.only( + topLeft: Radius.circular(22), + topRight: Radius.circular(22), + ) + : _barBorderRadius, width: _barWidth, ), ); currentY += data.moderate! + _rodStackItemsSpacing; + isFirstElement = false; } if (data.poor != null) { @@ -68,11 +82,17 @@ class AqiDistributionChart extends StatelessWidget { fromY: currentY, toY: currentY + data.poor!, color: AirQualityDataModel.metricColors['poor']!, - borderRadius: _barBorderRadius, + borderRadius: isFirstElement + ? const BorderRadius.only( + topLeft: Radius.circular(22), + topRight: Radius.circular(22), + ) + : _barBorderRadius, width: _barWidth, ), ); currentY += data.poor! + _rodStackItemsSpacing; + isFirstElement = false; } if (data.unhealthy != null) { @@ -81,11 +101,17 @@ class AqiDistributionChart extends StatelessWidget { fromY: currentY, toY: currentY + data.unhealthy!, color: AirQualityDataModel.metricColors['unhealthy']!, - borderRadius: _barBorderRadius, + borderRadius: isFirstElement + ? const BorderRadius.only( + topLeft: Radius.circular(22), + topRight: Radius.circular(22), + ) + : _barBorderRadius, width: _barWidth, ), ); currentY += data.unhealthy! + _rodStackItemsSpacing; + isFirstElement = false; } if (data.severe != null) { @@ -94,11 +120,17 @@ class AqiDistributionChart extends StatelessWidget { fromY: currentY, toY: currentY + data.severe!, color: AirQualityDataModel.metricColors['severe']!, - borderRadius: _barBorderRadius, + borderRadius: isFirstElement + ? const BorderRadius.only( + topLeft: Radius.circular(22), + topRight: Radius.circular(22), + ) + : _barBorderRadius, width: _barWidth, ), ); currentY += data.severe! + _rodStackItemsSpacing; + isFirstElement = false; } if (data.hazardous != null) { @@ -107,11 +139,17 @@ class AqiDistributionChart extends StatelessWidget { fromY: currentY, toY: currentY + data.hazardous!, color: AirQualityDataModel.metricColors['hazardous']!, - borderRadius: _barBorderRadius, + borderRadius: isFirstElement + ? const BorderRadius.only( + topLeft: Radius.circular(22), + topRight: Radius.circular(22), + ) + : _barBorderRadius, width: _barWidth, ), ); currentY += data.hazardous! + _rodStackItemsSpacing; + isFirstElement = false; } return BarChartGroupData( diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart index ac770a4e..8d20db94 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart @@ -27,21 +27,21 @@ class AqiDistributionChartBox extends StatelessWidget { chartData: [ AirQualityDataModel( date: DateTime(2025, 5, 1), - good: 30, - moderate: 25, - poor: 15, - unhealthy: 10, - severe: 15, - hazardous: 5, + good: null, + moderate: 35, + poor: 20, + unhealthy: 15, + severe: 20, + hazardous: 10, ), AirQualityDataModel( date: DateTime(2025, 5, 2), - good: 40, + good: null, moderate: 20, poor: 20, - unhealthy: 10, - severe: 5, - hazardous: 5, + unhealthy: null, + severe: 30, + hazardous: 25, ), AirQualityDataModel( date: DateTime(2025, 5, 3), From 286dea3f5128b942c5d09dfa6eb4ebe4c65fb518 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 1 Jun 2025 13:01:53 +0300 Subject: [PATCH 12/58] created a `GetAirQualityDistributionParam`. --- .../params/get_air_quality_distribution_param.dart | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 lib/pages/analytics/params/get_air_quality_distribution_param.dart diff --git a/lib/pages/analytics/params/get_air_quality_distribution_param.dart b/lib/pages/analytics/params/get_air_quality_distribution_param.dart new file mode 100644 index 00000000..f1d3fe9f --- /dev/null +++ b/lib/pages/analytics/params/get_air_quality_distribution_param.dart @@ -0,0 +1,9 @@ +class GetAirQualityDistributionParam { + final DateTime date; + final String spaceUuid; + + const GetAirQualityDistributionParam({ + required this.date, + required this.spaceUuid, + }); +} From 4479ed04b79cc1befe448e6802742abdfc00bee1 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 1 Jun 2025 13:02:11 +0300 Subject: [PATCH 13/58] Created a `AirQualityDistributionService` along with its fake implementation. --- .../air_quality_distribution_service.dart | 8 ++++ ...fake_air_quality_distribution_service.dart | 38 +++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 lib/pages/analytics/services/air_quality_distribution/air_quality_distribution_service.dart create mode 100644 lib/pages/analytics/services/air_quality_distribution/fake_air_quality_distribution_service.dart diff --git a/lib/pages/analytics/services/air_quality_distribution/air_quality_distribution_service.dart b/lib/pages/analytics/services/air_quality_distribution/air_quality_distribution_service.dart new file mode 100644 index 00000000..ef63856a --- /dev/null +++ b/lib/pages/analytics/services/air_quality_distribution/air_quality_distribution_service.dart @@ -0,0 +1,8 @@ +import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart'; +import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart'; + +abstract interface class AirQualityDistributionService { + Future> getAirQualityDistribution( + GetAirQualityDistributionParam param, + ); +} diff --git a/lib/pages/analytics/services/air_quality_distribution/fake_air_quality_distribution_service.dart b/lib/pages/analytics/services/air_quality_distribution/fake_air_quality_distribution_service.dart new file mode 100644 index 00000000..59f4947b --- /dev/null +++ b/lib/pages/analytics/services/air_quality_distribution/fake_air_quality_distribution_service.dart @@ -0,0 +1,38 @@ +import 'dart:math'; + +import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart'; +import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart'; +import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/air_quality_distribution_service.dart'; + +class FakeAirQualityDistributionService implements AirQualityDistributionService { + final _random = Random(); + + @override + Future> getAirQualityDistribution( + GetAirQualityDistributionParam param, + ) async { + return List.generate(30, (index) { + final date = DateTime(2025, 5, 1).add(Duration(days: index)); + + final values = _generateRandomPercentages(); + + return AirQualityDataModel( + date: date, + good: values[0], + moderate: values[1], + poor: values[2], + unhealthy: values[3], + severe: values[4], + hazardous: values[5], + ); + }); + } + + List _generateRandomPercentages() { + final values = List.generate(6, (_) => _random.nextDouble()); + + final sum = values.reduce((a, b) => a + b); + + return values.map((value) => (value / sum * 100).roundToDouble()).toList(); + } +} From 455d9c1f012d9924f9e8952131561db49e7c9cf8 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 1 Jun 2025 13:02:25 +0300 Subject: [PATCH 14/58] Created `AirQualityDistributionBloc`. --- .../air_quality_distribution_bloc.dart | 54 +++++++++++++++++++ .../air_quality_distribution_event.dart | 21 ++++++++ .../air_quality_distribution_state.dart | 23 ++++++++ 3 files changed, 98 insertions(+) create mode 100644 lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart create mode 100644 lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_event.dart create mode 100644 lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_state.dart diff --git a/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart b/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart new file mode 100644 index 00000000..a81724a2 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart @@ -0,0 +1,54 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart'; +import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart'; +import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/air_quality_distribution_service.dart'; + +part 'air_quality_distribution_event.dart'; +part 'air_quality_distribution_state.dart'; + +class AirQualityDistributionBloc + extends Bloc { + final AirQualityDistributionService _service; + + AirQualityDistributionBloc( + this._service, + ) : super(const AirQualityDistributionState()) { + on(_onLoadAirQualityDistribution); + on(_onClearAirQualityDistribution); + } + + Future _onLoadAirQualityDistribution( + LoadAirQualityDistribution event, + Emitter emit, + ) async { + try { + emit( + const AirQualityDistributionState( + status: AirQualityDistributionStatus.loading, + ), + ); + final result = await _service.getAirQualityDistribution(event.param); + emit( + AirQualityDistributionState( + status: AirQualityDistributionStatus.success, + chartData: result, + ), + ); + } catch (e) { + emit( + AirQualityDistributionState( + status: AirQualityDistributionStatus.failure, + errorMessage: e.toString(), + ), + ); + } + } + + Future _onClearAirQualityDistribution( + ClearAirQualityDistribution event, + Emitter emit, + ) async { + emit(const AirQualityDistributionState()); + } +} diff --git a/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_event.dart b/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_event.dart new file mode 100644 index 00000000..2e1d291f --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_event.dart @@ -0,0 +1,21 @@ +part of 'air_quality_distribution_bloc.dart'; + +sealed class AirQualityDistributionEvent extends Equatable { + const AirQualityDistributionEvent(); + + @override + List get props => []; +} + +final class LoadAirQualityDistribution extends AirQualityDistributionEvent { + final GetAirQualityDistributionParam param; + + const LoadAirQualityDistribution(this.param); + + @override + List get props => [param]; +} + +final class ClearAirQualityDistribution extends AirQualityDistributionEvent { + const ClearAirQualityDistribution(); +} diff --git a/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_state.dart b/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_state.dart new file mode 100644 index 00000000..0db95e2d --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_state.dart @@ -0,0 +1,23 @@ +part of 'air_quality_distribution_bloc.dart'; + +enum AirQualityDistributionStatus { + initial, + loading, + success, + failure, +} + +class AirQualityDistributionState extends Equatable { + final AirQualityDistributionStatus status; + final List chartData; + final String? errorMessage; + + const AirQualityDistributionState({ + this.status = AirQualityDistributionStatus.initial, + this.chartData = const [], + this.errorMessage, + }); + + @override + List get props => [status, chartData, errorMessage]; +} From 736e0c3d9c72f9c98138009bd2862ae40e77e9bb Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 1 Jun 2025 13:02:40 +0300 Subject: [PATCH 15/58] Injected `AirQualityDistributionBloc` into `AnalyticsPage`. --- .../analytics/views/analytics_page.dart | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/lib/pages/analytics/modules/analytics/views/analytics_page.dart b/lib/pages/analytics/modules/analytics/views/analytics_page.dart index 68a531c8..6fc0fc5c 100644 --- a/lib/pages/analytics/modules/analytics/views/analytics_page.dart +++ b/lib/pages/analytics/modules/analytics/views/analytics_page.dart @@ -1,5 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +<<<<<<< HEAD +======= +import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/device_location/device_location_bloc.dart'; +>>>>>>> 449e1fd6 (Injected `AirQualityDistributionBloc` into `AnalyticsPage`.) 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/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'; @@ -18,6 +23,7 @@ import 'package:syncrow_web/pages/analytics/services/analytics_devices/remote_en import 'package:syncrow_web/pages/analytics/services/analytics_devices/remote_occupancy_analytics_devices_service.dart'; import 'package:syncrow_web/pages/analytics/services/energy_consumption_by_phases/remote_energy_consumption_by_phases_service.dart'; import 'package:syncrow_web/pages/analytics/services/energy_consumption_per_device/remote_energy_consumption_per_device_service.dart'; +import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/fake_air_quality_distribution_service.dart'; import 'package:syncrow_web/pages/analytics/services/occupacy/remote_occupancy_service.dart'; import 'package:syncrow_web/pages/analytics/services/occupancy_heat_map/remote_occupancy_heat_map_service.dart'; import 'package:syncrow_web/pages/analytics/services/power_clamp_info/remote_power_clamp_info_service.dart'; @@ -101,6 +107,21 @@ class _AnalyticsPageState extends State { FakeRangeOfAqiService(), ), ), +<<<<<<< HEAD +======= + BlocProvider( + create: (context) => DeviceLocationBloc( + RemoteDeviceLocationService( + Dio(BaseOptions(baseUrl: 'https://api.openweathermap.org/data/2.5')), + ), + ), + ), + BlocProvider( + create: (context) => AirQualityDistributionBloc( + FakeAirQualityDistributionService(), + ), + ), +>>>>>>> 449e1fd6 (Injected `AirQualityDistributionBloc` into `AnalyticsPage`.) ], child: const AnalyticsPageForm(), ); From accafb150e808722d70f51396b373a6ab8953ceb Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 1 Jun 2025 14:24:07 +0300 Subject: [PATCH 16/58] . --- .../modules/analytics/views/analytics_page.dart | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/lib/pages/analytics/modules/analytics/views/analytics_page.dart b/lib/pages/analytics/modules/analytics/views/analytics_page.dart index 6fc0fc5c..575aa862 100644 --- a/lib/pages/analytics/modules/analytics/views/analytics_page.dart +++ b/lib/pages/analytics/modules/analytics/views/analytics_page.dart @@ -1,10 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -<<<<<<< HEAD -======= import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart'; -import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/device_location/device_location_bloc.dart'; ->>>>>>> 449e1fd6 (Injected `AirQualityDistributionBloc` into `AnalyticsPage`.) 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/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'; @@ -18,12 +14,12 @@ import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/real import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/total_energy_consumption/total_energy_consumption_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy/occupancy_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy_heat_map/occupancy_heat_map_bloc.dart'; +import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/fake_air_quality_distribution_service.dart'; import 'package:syncrow_web/pages/analytics/services/analytics_devices/analytics_devices_service_delagate.dart'; import 'package:syncrow_web/pages/analytics/services/analytics_devices/remote_energy_management_analytics_devices_service.dart'; import 'package:syncrow_web/pages/analytics/services/analytics_devices/remote_occupancy_analytics_devices_service.dart'; import 'package:syncrow_web/pages/analytics/services/energy_consumption_by_phases/remote_energy_consumption_by_phases_service.dart'; import 'package:syncrow_web/pages/analytics/services/energy_consumption_per_device/remote_energy_consumption_per_device_service.dart'; -import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/fake_air_quality_distribution_service.dart'; import 'package:syncrow_web/pages/analytics/services/occupacy/remote_occupancy_service.dart'; import 'package:syncrow_web/pages/analytics/services/occupancy_heat_map/remote_occupancy_heat_map_service.dart'; import 'package:syncrow_web/pages/analytics/services/power_clamp_info/remote_power_clamp_info_service.dart'; @@ -107,21 +103,11 @@ class _AnalyticsPageState extends State { FakeRangeOfAqiService(), ), ), -<<<<<<< HEAD -======= - BlocProvider( - create: (context) => DeviceLocationBloc( - RemoteDeviceLocationService( - Dio(BaseOptions(baseUrl: 'https://api.openweathermap.org/data/2.5')), - ), - ), - ), BlocProvider( create: (context) => AirQualityDistributionBloc( FakeAirQualityDistributionService(), ), ), ->>>>>>> 449e1fd6 (Injected `AirQualityDistributionBloc` into `AnalyticsPage`.) ], child: const AnalyticsPageForm(), ); From 8dc7d2b3d016e044ed4f25d540e9432876db292b Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 1 Jun 2025 13:02:52 +0300 Subject: [PATCH 17/58] Connected `AirQualityDistributionBloc` into `AqiDistributionChartBox`. --- .../widgets/aqi_distribution_chart_box.dart | 76 +++++++------------ 1 file changed, 28 insertions(+), 48 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart index 8d20db94..8347a15b 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart'; +import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart'; import 'package:syncrow_web/utils/style.dart'; class AqiDistributionChartBox extends StatelessWidget { @@ -9,54 +11,32 @@ class AqiDistributionChartBox extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - padding: const EdgeInsetsDirectional.all(30), - decoration: subSectionContainerDecoration.copyWith( - borderRadius: BorderRadius.circular(30), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const AqiDistributionChartTitle(isLoading: false), - const SizedBox(height: 10), - const Divider(), - const SizedBox(height: 20), - Expanded( - child: AqiDistributionChart( - chartData: [ - AirQualityDataModel( - date: DateTime(2025, 5, 1), - good: null, - moderate: 35, - poor: 20, - unhealthy: 15, - severe: 20, - hazardous: 10, - ), - AirQualityDataModel( - date: DateTime(2025, 5, 2), - good: null, - moderate: 20, - poor: 20, - unhealthy: null, - severe: 30, - hazardous: 25, - ), - AirQualityDataModel( - date: DateTime(2025, 5, 3), - good: 35, - moderate: 30, - poor: 15, - unhealthy: 10, - severe: 5, - hazardous: 5, - ), - ], - ), + return BlocBuilder( + builder: (context, state) { + return Container( + padding: const EdgeInsetsDirectional.all(30), + decoration: subSectionContainerDecoration.copyWith( + borderRadius: BorderRadius.circular(30), ), - ], - ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (state.errorMessage != null) ...[ + AnalyticsErrorWidget(state.errorMessage), + const SizedBox(height: 10), + ], + AqiDistributionChartTitle( + isLoading: state.status == AirQualityDistributionStatus.loading, + ), + const SizedBox(height: 10), + const Divider(), + const SizedBox(height: 20), + Expanded(child: AqiDistributionChart(chartData: state.chartData)), + ], + ), + ); + }, ); } } From c50ed693ae3de23e60213d769d2a1ccee4739035 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 1 Jun 2025 13:10:56 +0300 Subject: [PATCH 18/58] loads and clears aqi distribution in `FetchAirQualityDataHelper`. --- .../fetch_air_quality_data_helper.dart | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) 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 65e62365..55de65d3 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 @@ -1,10 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_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'; import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart'; import 'package:syncrow_web/pages/analytics/params/get_range_of_aqi_param.dart'; @@ -28,6 +30,11 @@ abstract final class FetchAirQualityDataHelper { date: date, aqiType: AqiType.aqi, ); + loadAirQualityDistribution( + context, + spaceUuid: spaceUuid, + date: date, + ); } static void clearAllData(BuildContext context) { @@ -37,7 +44,9 @@ abstract final class FetchAirQualityDataHelper { context.read().add( const RealtimeDeviceChangesClosed(), ); - + context.read().add( + const ClearAirQualityDistribution(), + ); context.read().add(const ClearRangeOfAqiEvent()); } @@ -79,4 +88,16 @@ abstract final class FetchAirQualityDataHelper { ), ); } + + static void loadAirQualityDistribution( + BuildContext context, { + required String spaceUuid, + required DateTime date, + }) { + context.read().add( + LoadAirQualityDistribution( + GetAirQualityDistributionParam(spaceUuid: spaceUuid, date: date), + ), + ); + } } From 2e12d73151dd6d8f3af53b96934a5833cd6bbafa Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 1 Jun 2025 13:11:29 +0300 Subject: [PATCH 19/58] randomize generated fake data in `FakeAirQualityDistributionService`. --- ...fake_air_quality_distribution_service.dart | 55 +++++++++++++++---- 1 file changed, 43 insertions(+), 12 deletions(-) diff --git a/lib/pages/analytics/services/air_quality_distribution/fake_air_quality_distribution_service.dart b/lib/pages/analytics/services/air_quality_distribution/fake_air_quality_distribution_service.dart index 59f4947b..264addab 100644 --- a/lib/pages/analytics/services/air_quality_distribution/fake_air_quality_distribution_service.dart +++ b/lib/pages/analytics/services/air_quality_distribution/fake_air_quality_distribution_service.dart @@ -11,23 +11,54 @@ class FakeAirQualityDistributionService implements AirQualityDistributionService Future> getAirQualityDistribution( GetAirQualityDistributionParam param, ) async { - return List.generate(30, (index) { - final date = DateTime(2025, 5, 1).add(Duration(days: index)); + return Future.delayed( + const Duration(milliseconds: 400), + () => List.generate(30, (index) { + final date = DateTime(2025, 5, 1).add(Duration(days: index)); - final values = _generateRandomPercentages(); + final values = _generateRandomPercentages(); + final nullMask = List.generate(6, (_) => _shouldBeNull()); - return AirQualityDataModel( - date: date, - good: values[0], - moderate: values[1], - poor: values[2], - unhealthy: values[3], - severe: values[4], - hazardous: values[5], - ); + // If all values are null, force at least one to be non-null + if (nullMask.every((isNull) => isNull)) { + nullMask[_random.nextInt(6)] = false; + } + + // Redistribute percentages among non-null values + final nonNullValues = _redistributePercentages(values, nullMask); + + return AirQualityDataModel( + date: date, + good: nullMask[0] ? null : nonNullValues[0], + moderate: nullMask[1] ? null : nonNullValues[1], + poor: nullMask[2] ? null : nonNullValues[2], + unhealthy: nullMask[3] ? null : nonNullValues[3], + severe: nullMask[4] ? null : nonNullValues[4], + hazardous: nullMask[5] ? null : nonNullValues[5], + ); + }), + ); + } + + List _redistributePercentages( + List originalValues, List nullMask) { + // Calculate total of non-null values + double nonNullSum = 0; + for (int i = 0; i < originalValues.length; i++) { + if (!nullMask[i]) { + nonNullSum += originalValues[i]; + } + } + + // Redistribute percentages to maintain 100% total + return List.generate(originalValues.length, (i) { + if (nullMask[i]) return 0; + return (originalValues[i] / nonNullSum * 100).roundToDouble(); }); } + bool _shouldBeNull() => _random.nextDouble() < 0.6; + List _generateRandomPercentages() { final values = List.generate(6, (_) => _random.nextDouble()); From 2be15e648ac7498b97098adc3c639c6e26621521 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 1 Jun 2025 13:13:45 +0300 Subject: [PATCH 20/58] added loading widget to `AqiDistributionChartTitle`. --- .../air_quality/widgets/aqi_distribution_chart_title.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 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 a1272a10..5045316b 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 @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart'; +import 'package:syncrow_web/pages/analytics/widgets/charts_loading_widget.dart'; class AqiDistributionChartTitle extends StatelessWidget { const AqiDistributionChartTitle({required this.isLoading, super.key}); @@ -10,12 +11,12 @@ class AqiDistributionChartTitle extends StatelessWidget { @override Widget build(BuildContext context) { return Row( - spacing: 11, children: [ + ChartsLoadingWidget(isLoading: isLoading), const Expanded( flex: 3, child: FittedBox( - fit: BoxFit.scaleDown, + fit: BoxFit.scaleDown, alignment: AlignmentDirectional.centerStart, child: ChartTitle( title: Text('Distribution over Air Quality Index'), From e28f3c3c0300c0d38a59db6d431ee9e977456873 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 1 Jun 2025 13:16:12 +0300 Subject: [PATCH 21/58] reduced bar width size. --- .../modules/air_quality/widgets/aqi_distribution_chart.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 aab0a607..e23a4424 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 @@ -10,7 +10,7 @@ class AqiDistributionChart extends StatelessWidget { final List chartData; static const _rodStackItemsSpacing = 0.4; - static const _barWidth = 20.0; + static const _barWidth = 13.0; static final _barBorderRadius = BorderRadius.circular(22); @override From 066f967cd1561bf273c5700099c47b7d3ecec708 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 1 Jun 2025 13:27:35 +0300 Subject: [PATCH 22/58] shows tooltip with data. --- .../widgets/aqi_distribution_chart.dart | 71 ++++++++++++++----- 1 file changed, 54 insertions(+), 17 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 e23a4424..89b6dd1d 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 @@ -1,5 +1,6 @@ import 'package:fl_chart/fl_chart.dart'; 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/utils/color_manager.dart'; @@ -171,28 +172,64 @@ class AqiDistributionChart extends StatelessWidget { tooltipPadding: const EdgeInsets.all(8), getTooltipItem: (group, groupIndex, rod, rodIndex) { final data = chartData[group.x.toInt()]; - final stackItems = rod.rodStackItems; + + final List children = []; + + final textStyle = context.textTheme.bodySmall?.copyWith( + color: ColorsManager.blackColor, + fontSize: 12, + ); + + if (data.good != null) { + children.add(TextSpan( + text: '\nGOOD: ${data.good!.toStringAsFixed(1)}%', + style: textStyle, + )); + } + + if (data.moderate != null) { + children.add(TextSpan( + text: '\nMODERATE: ${data.moderate!.toStringAsFixed(1)}%', + style: textStyle, + )); + } + + if (data.poor != null) { + children.add(TextSpan( + text: '\nPOOR: ${data.poor!.toStringAsFixed(1)}%', + style: textStyle, + )); + } + + if (data.unhealthy != null) { + children.add(TextSpan( + text: '\nUNHEALTHY: ${data.unhealthy!.toStringAsFixed(1)}%', + style: textStyle, + )); + } + + if (data.severe != null) { + children.add(TextSpan( + text: '\nSEVERE: ${data.severe!.toStringAsFixed(1)}%', + style: textStyle, + )); + } + + if (data.hazardous != null) { + children.add(TextSpan( + text: '\nHAZARDOUS: ${data.hazardous!.toStringAsFixed(1)}%', + style: textStyle, + )); + } return BarTooltipItem( - '${data.date.day}/${data.date.month}\n', + DateFormat('dd/MM/yyyy').format(data.date), context.textTheme.bodyMedium!.copyWith( color: ColorsManager.blackColor, - fontSize: 14, + fontSize: 16, + fontWeight: FontWeight.w600, ), - children: stackItems.map((item) { - final metricName = AirQualityDataModel.metricColors.entries - .firstWhere((entry) => entry.value == item.color) - .key - .toLowerCase(); - return TextSpan( - text: - '$metricName: ${(item.toY - item.fromY).toStringAsFixed(1)}%\n', - style: context.textTheme.bodySmall?.copyWith( - color: ColorsManager.blackColor, - fontSize: 12, - ), - ); - }).toList(), + children: children, ); }, ), From 78f42dacf61ed0e5adf209d05d2fe43ae936d8e7 Mon Sep 17 00:00:00 2001 From: mohammad Date: Sun, 1 Jun 2025 14:37:42 +0300 Subject: [PATCH 23/58] Adjust ConditionToggle widget dimensions and colors for improved UI consistency --- lib/pages/routines/widgets/condition_toggle.dart | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/pages/routines/widgets/condition_toggle.dart b/lib/pages/routines/widgets/condition_toggle.dart index b86ba0b3..541ad431 100644 --- a/lib/pages/routines/widgets/condition_toggle.dart +++ b/lib/pages/routines/widgets/condition_toggle.dart @@ -23,9 +23,10 @@ class ConditionToggle extends StatelessWidget { final selectedIndex = _conditions.indexOf(currentCondition ?? "=="); return Container( - height: 80, + height: 30, + width: MediaQuery.of(context).size.width * 0.1, decoration: BoxDecoration( - color: ColorsManager.grayColor, + color: ColorsManager.softGray.withOpacity(0.5), borderRadius: BorderRadius.circular(50), ), clipBehavior: Clip.antiAlias, @@ -34,18 +35,19 @@ class ConditionToggle extends StatelessWidget { children: List.generate(_conditions.length, (index) { final isSelected = index == selectedIndex; return Expanded( - child: GestureDetector( + child: InkWell( onTap: () => onChanged(_conditions[index]), child: AnimatedContainer( duration: const Duration(milliseconds: 180), curve: Curves.ease, decoration: BoxDecoration( - color: isSelected ? ColorsManager.blue1 : Colors.transparent, + color: + isSelected ? ColorsManager.vividBlue : Colors.transparent, ), child: Center( child: Icon( _icons[index], - size: 38, + size: 20, color: isSelected ? ColorsManager.whiteColors : ColorsManager.blackColor, From 94847fa93639c9367d2643c83aa60b56cb2275fe Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 1 Jun 2025 15:36:52 +0300 Subject: [PATCH 24/58] SP-1664-Fe-Sider-bar-tree-behavior-issues-on-Analytics-page. --- ...ergy_management_data_loading_strategy.dart | 56 ++++++++++--------- .../occupancy_data_loading_strategy.dart | 12 ++-- .../space_tree/bloc/space_tree_bloc.dart | 17 ++++++ .../space_tree/bloc/space_tree_event.dart | 4 ++ 4 files changed, 57 insertions(+), 32 deletions(-) diff --git a/lib/pages/analytics/modules/analytics/strategies/energy_management_data_loading_strategy.dart b/lib/pages/analytics/modules/analytics/strategies/energy_management_data_loading_strategy.dart index e73b5179..caaf9540 100644 --- a/lib/pages/analytics/modules/analytics/strategies/energy_management_data_loading_strategy.dart +++ b/lib/pages/analytics/modules/analytics/strategies/energy_management_data_loading_strategy.dart @@ -14,24 +14,14 @@ class EnergyManagementDataLoadingStrategy implements AnalyticsDataLoadingStrateg CommunityModel community, List spaces, ) { - context.read().add( - OnCommunitySelected( - community.uuid, - spaces, - ), - ); + final spaceTreeBloc = context.read(); + final isCommunitySelected = + spaceTreeBloc.state.selectedCommunities.contains(community.uuid); - final spaceTreeState = context.read().state; - if (spaceTreeState.selectedCommunities.contains(community.uuid)) { + if (isCommunitySelected) { clearData(context); return; } - - FetchEnergyManagementDataHelper.loadEnergyManagementData( - context, - communityId: community.uuid, - spaceId: spaces.isNotEmpty ? spaces.first.uuid ?? '' : '', - ); } @override @@ -40,21 +30,31 @@ class EnergyManagementDataLoadingStrategy implements AnalyticsDataLoadingStrateg CommunityModel community, SpaceModel space, ) { - context.read().add( - OnSpaceSelected( - community, - space.uuid ?? '', - space.children, - ), - ); + final spaceTreeBloc = context.read(); + final isSpaceSelected = spaceTreeBloc.state.selectedSpaces.contains(space.uuid); + final hasSelectedSpaces = spaceTreeBloc.state.selectedSpaces.isNotEmpty; - final spaceTreeState = context.read().state; - if (spaceTreeState.selectedCommunities.contains(community.uuid) || - spaceTreeState.selectedSpaces.contains(space.uuid)) { - clearData(context); + if (isSpaceSelected) { + final firstSelectedSpace = spaceTreeBloc.state.selectedSpaces.first; + final isTheFirstSelectedSpace = firstSelectedSpace == space.uuid; + if (isTheFirstSelectedSpace) { + clearData(context); + } return; } + if (hasSelectedSpaces) { + clearData(context); + } + + spaceTreeBloc.add( + OnSpaceSelected( + community, + space.uuid ?? '', + space.children, + ), + ); + FetchEnergyManagementDataHelper.loadEnergyManagementData( context, communityId: community.uuid, @@ -68,12 +68,14 @@ class EnergyManagementDataLoadingStrategy implements AnalyticsDataLoadingStrateg CommunityModel community, SpaceModel child, ) { - return onSpaceSelected(context, community, child); + onSpaceSelected(context, community, child); } @override void clearData(BuildContext context) { - context.read().add(const SpaceTreeClearSelectionEvent()); + context.read().add( + const AnalyticsClearAllSpaceTreeSelectionsEvent(), + ); FetchEnergyManagementDataHelper.clearAllData(context); } } diff --git a/lib/pages/analytics/modules/analytics/strategies/occupancy_data_loading_strategy.dart b/lib/pages/analytics/modules/analytics/strategies/occupancy_data_loading_strategy.dart index 5241564c..239e3cd3 100644 --- a/lib/pages/analytics/modules/analytics/strategies/occupancy_data_loading_strategy.dart +++ b/lib/pages/analytics/modules/analytics/strategies/occupancy_data_loading_strategy.dart @@ -26,10 +26,10 @@ class OccupancyDataLoadingStrategy implements AnalyticsDataLoadingStrategy { final spaceTreeBloc = context.read(); final isSpaceSelected = spaceTreeBloc.state.selectedSpaces.contains(space.uuid); - if (isSpaceSelected) { - clearData(context); - return; - } + final hasSelectedSpaces = spaceTreeBloc.state.selectedSpaces.isNotEmpty; + if (hasSelectedSpaces) clearData(context); + + if (isSpaceSelected) return; spaceTreeBloc ..add(const SpaceTreeClearSelectionEvent()) @@ -53,7 +53,9 @@ class OccupancyDataLoadingStrategy implements AnalyticsDataLoadingStrategy { @override void clearData(BuildContext context) { - context.read().add(const SpaceTreeClearSelectionEvent()); + context.read().add( + const AnalyticsClearAllSpaceTreeSelectionsEvent(), + ); FetchOccupancyDataHelper.clearAllData(context); } } diff --git a/lib/pages/space_tree/bloc/space_tree_bloc.dart b/lib/pages/space_tree/bloc/space_tree_bloc.dart index a3a29004..e8c2e015 100644 --- a/lib/pages/space_tree/bloc/space_tree_bloc.dart +++ b/lib/pages/space_tree/bloc/space_tree_bloc.dart @@ -24,6 +24,9 @@ class SpaceTreeBloc extends Bloc { on(_fetchPaginationSpaces); on(_onDebouncedSearch); on(_onSpaceTreeClearSelectionEvent); + on( + _onAnalyticsClearAllSpaceTreeSelectionsEvent, + ); } Timer _timer = Timer(const Duration(microseconds: 0), () {}); @@ -493,6 +496,20 @@ class SpaceTreeBloc extends Bloc { ); } + void _onAnalyticsClearAllSpaceTreeSelectionsEvent( + AnalyticsClearAllSpaceTreeSelectionsEvent event, + Emitter emit, + ) async { + emit( + state.copyWith( + selectedCommunities: [], + selectedCommunityAndSpaces: {}, + selectedSpaces: [], + soldCheck: [], + ), + ); + } + @override Future close() async { _timer.cancel(); diff --git a/lib/pages/space_tree/bloc/space_tree_event.dart b/lib/pages/space_tree/bloc/space_tree_event.dart index 9c2342fc..6e1687af 100644 --- a/lib/pages/space_tree/bloc/space_tree_event.dart +++ b/lib/pages/space_tree/bloc/space_tree_event.dart @@ -112,3 +112,7 @@ class ClearCachedData extends SpaceTreeEvent {} class SpaceTreeClearSelectionEvent extends SpaceTreeEvent { const SpaceTreeClearSelectionEvent(); } + +final class AnalyticsClearAllSpaceTreeSelectionsEvent extends SpaceTreeEvent { + const AnalyticsClearAllSpaceTreeSelectionsEvent(); +} From a56e93d0d79415843dd2fe8088c6dc873bb872d7 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 1 Jun 2025 15:38:14 +0300 Subject: [PATCH 25/58] removed the interface method `onSelectChildSpace`, because all the clients dont use it and instead pass the `onSpaceSelected`, which isn't a good design. --- .../strategies/air_quality_data_loading_strategy.dart | 9 --------- .../strategies/analytics_data_loading_strategy.dart | 5 ----- .../energy_management_data_loading_strategy.dart | 9 --------- .../strategies/occupancy_data_loading_strategy.dart | 9 --------- .../analytics/widgets/analytics_communities_sidebar.dart | 2 +- 5 files changed, 1 insertion(+), 33 deletions(-) diff --git a/lib/pages/analytics/modules/analytics/strategies/air_quality_data_loading_strategy.dart b/lib/pages/analytics/modules/analytics/strategies/air_quality_data_loading_strategy.dart index dc3b1c5e..cd5f4e46 100644 --- a/lib/pages/analytics/modules/analytics/strategies/air_quality_data_loading_strategy.dart +++ b/lib/pages/analytics/modules/analytics/strategies/air_quality_data_loading_strategy.dart @@ -42,15 +42,6 @@ final class AirQualityDataLoadingStrategy implements AnalyticsDataLoadingStrateg ); } - @override - void onChildSpaceSelected( - BuildContext context, - CommunityModel community, - SpaceModel child, - ) { - return onSpaceSelected(context, community, child); - } - @override void clearData(BuildContext context) { context.read().add(const SpaceTreeClearSelectionEvent()); diff --git a/lib/pages/analytics/modules/analytics/strategies/analytics_data_loading_strategy.dart b/lib/pages/analytics/modules/analytics/strategies/analytics_data_loading_strategy.dart index 2c2194ba..654455b2 100644 --- a/lib/pages/analytics/modules/analytics/strategies/analytics_data_loading_strategy.dart +++ b/lib/pages/analytics/modules/analytics/strategies/analytics_data_loading_strategy.dart @@ -13,10 +13,5 @@ abstract class AnalyticsDataLoadingStrategy { CommunityModel community, SpaceModel space, ); - void onChildSpaceSelected( - BuildContext context, - CommunityModel community, - SpaceModel child, - ); void clearData(BuildContext context); } diff --git a/lib/pages/analytics/modules/analytics/strategies/energy_management_data_loading_strategy.dart b/lib/pages/analytics/modules/analytics/strategies/energy_management_data_loading_strategy.dart index caaf9540..757b2a9a 100644 --- a/lib/pages/analytics/modules/analytics/strategies/energy_management_data_loading_strategy.dart +++ b/lib/pages/analytics/modules/analytics/strategies/energy_management_data_loading_strategy.dart @@ -62,15 +62,6 @@ class EnergyManagementDataLoadingStrategy implements AnalyticsDataLoadingStrateg ); } - @override - void onChildSpaceSelected( - BuildContext context, - CommunityModel community, - SpaceModel child, - ) { - onSpaceSelected(context, community, child); - } - @override void clearData(BuildContext context) { context.read().add( diff --git a/lib/pages/analytics/modules/analytics/strategies/occupancy_data_loading_strategy.dart b/lib/pages/analytics/modules/analytics/strategies/occupancy_data_loading_strategy.dart index 239e3cd3..9bffe3b4 100644 --- a/lib/pages/analytics/modules/analytics/strategies/occupancy_data_loading_strategy.dart +++ b/lib/pages/analytics/modules/analytics/strategies/occupancy_data_loading_strategy.dart @@ -42,15 +42,6 @@ class OccupancyDataLoadingStrategy implements AnalyticsDataLoadingStrategy { ); } - @override - void onChildSpaceSelected( - BuildContext context, - CommunityModel community, - SpaceModel child, - ) { - return onSpaceSelected(context, community, child); - } - @override void clearData(BuildContext context) { context.read().add( diff --git a/lib/pages/analytics/modules/analytics/widgets/analytics_communities_sidebar.dart b/lib/pages/analytics/modules/analytics/widgets/analytics_communities_sidebar.dart index b63c6411..ab07737a 100644 --- a/lib/pages/analytics/modules/analytics/widgets/analytics_communities_sidebar.dart +++ b/lib/pages/analytics/modules/analytics/widgets/analytics_communities_sidebar.dart @@ -21,7 +21,7 @@ class AnalyticsCommunitiesSidebar extends StatelessWidget { strategy.onSpaceSelected(context, community, space); }, onSelectChildSpace: (community, child) { - strategy.onChildSpaceSelected(context, community, child); + strategy.onSpaceSelected(context, community, child); }, ), ); From 393a5361f08d125222553e8125425221ccc8338b Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 1 Jun 2025 15:40:12 +0300 Subject: [PATCH 26/58] Apply correct business logic in `AirQualityDataLoadingStrategy`. --- .../air_quality_data_loading_strategy.dart | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/pages/analytics/modules/analytics/strategies/air_quality_data_loading_strategy.dart b/lib/pages/analytics/modules/analytics/strategies/air_quality_data_loading_strategy.dart index cd5f4e46..a8993cc3 100644 --- a/lib/pages/analytics/modules/analytics/strategies/air_quality_data_loading_strategy.dart +++ b/lib/pages/analytics/modules/analytics/strategies/air_quality_data_loading_strategy.dart @@ -26,10 +26,10 @@ final class AirQualityDataLoadingStrategy implements AnalyticsDataLoadingStrateg final spaceTreeBloc = context.read(); final isSpaceSelected = spaceTreeBloc.state.selectedSpaces.contains(space.uuid); - if (isSpaceSelected) { - clearData(context); - return; - } + final hasSelectedSpaces = spaceTreeBloc.state.selectedSpaces.isNotEmpty; + if (hasSelectedSpaces) clearData(context); + + if (isSpaceSelected) return; spaceTreeBloc ..add(const SpaceTreeClearSelectionEvent()) @@ -44,7 +44,9 @@ final class AirQualityDataLoadingStrategy implements AnalyticsDataLoadingStrateg @override void clearData(BuildContext context) { - context.read().add(const SpaceTreeClearSelectionEvent()); + context.read().add( + const AnalyticsClearAllSpaceTreeSelectionsEvent(), + ); FetchAirQualityDataHelper.clearAllData(context); } } From 17f6985dbff59cfcceafafe2eb667438c55bb349 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 1 Jun 2025 15:59:29 +0300 Subject: [PATCH 27/58] enable hot reload on web. --- .vscode/launch.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.vscode/launch.json b/.vscode/launch.json index f81a9deb..4aceb26d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -16,6 +16,7 @@ "3000", "-t", "lib/main_dev.dart", + "--web-experimental-hot-reload", ], "flutterMode": "debug" @@ -35,6 +36,7 @@ "3000", "-t", "lib/main_staging.dart", + "--web-experimental-hot-reload", ], "flutterMode": "debug" @@ -54,6 +56,7 @@ "3000", "-t", "lib/main.dart", + "--web-experimental-hot-reload", ], "flutterMode": "debug" From 2d68fc23a3b69a57370bc946fb9f788b43688222 Mon Sep 17 00:00:00 2001 From: mohammad Date: Sun, 1 Jun 2025 16:21:22 +0300 Subject: [PATCH 28/58] Normalize email to lowercase when logging in --- lib/pages/auth/bloc/auth_bloc.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/auth/bloc/auth_bloc.dart b/lib/pages/auth/bloc/auth_bloc.dart index 35663557..e5de46c9 100644 --- a/lib/pages/auth/bloc/auth_bloc.dart +++ b/lib/pages/auth/bloc/auth_bloc.dart @@ -161,7 +161,7 @@ class AuthBloc extends Bloc { token = await AuthenticationAPI.loginWithEmail( model: LoginWithEmailModel( - email: event.username, + email: event.username.toLowerCase(), password: event.password, ), ); From 62dabf1ce2bb11e2ed94dc7dd3d780780f60c690 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 2 Jun 2025 10:10:50 +0300 Subject: [PATCH 29/58] Made values in `DeviceControlDialog` selectable for a better UX. --- lib/pages/device_managment/shared/device_control_dialog.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/device_managment/shared/device_control_dialog.dart b/lib/pages/device_managment/shared/device_control_dialog.dart index c9cd4648..beb3b52c 100644 --- a/lib/pages/device_managment/shared/device_control_dialog.dart +++ b/lib/pages/device_managment/shared/device_control_dialog.dart @@ -157,7 +157,7 @@ class DeviceControlDialog extends StatelessWidget with RouteControlsBasedCode { ), ), const SizedBox(width: 10), - Text( + SelectableText( value, style: TextStyle( fontSize: 16, From 6f3dfb607ef10f4562593de447798f4b456e86cb Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 2 Jun 2025 10:11:23 +0300 Subject: [PATCH 30/58] Extracted single/batch control services creation into a factory for ease of reusablility for the sake of this migration. --- .../device_bloc_dependencies_factory.dart | 18 ++++++++++++++++++ ...h_mounted_presence_sensor_bloc_factory.dart | 11 +++-------- 2 files changed, 21 insertions(+), 8 deletions(-) create mode 100644 lib/pages/device_managment/factories/device_bloc_dependencies_factory.dart diff --git a/lib/pages/device_managment/factories/device_bloc_dependencies_factory.dart b/lib/pages/device_managment/factories/device_bloc_dependencies_factory.dart new file mode 100644 index 00000000..1c75c38b --- /dev/null +++ b/lib/pages/device_managment/factories/device_bloc_dependencies_factory.dart @@ -0,0 +1,18 @@ +import 'package:syncrow_web/services/batch_control_devices_service.dart'; +import 'package:syncrow_web/services/control_device_service.dart'; + +abstract final class DeviceBlocDependenciesFactory { + const DeviceBlocDependenciesFactory._(); + + static ControlDeviceService createControlDeviceService() { + return DebouncedControlDeviceService( + decoratee: RemoteControlDeviceService(), + ); + } + + static BatchControlDevicesService createBatchControlDevicesService() { + return DebouncedBatchControlDevicesService( + decoratee: RemoteBatchControlDevicesService(), + ); + } +} diff --git a/lib/pages/device_managment/flush_mounted_presence_sensor/factories/flush_mounted_presence_sensor_bloc_factory.dart b/lib/pages/device_managment/flush_mounted_presence_sensor/factories/flush_mounted_presence_sensor_bloc_factory.dart index 49fb517f..e842f36b 100644 --- a/lib/pages/device_managment/flush_mounted_presence_sensor/factories/flush_mounted_presence_sensor_bloc_factory.dart +++ b/lib/pages/device_managment/flush_mounted_presence_sensor/factories/flush_mounted_presence_sensor_bloc_factory.dart @@ -1,6 +1,5 @@ +import 'package:syncrow_web/pages/device_managment/factories/device_bloc_dependencies_factory.dart'; import 'package:syncrow_web/pages/device_managment/flush_mounted_presence_sensor/bloc/flush_mounted_presence_sensor_bloc.dart'; -import 'package:syncrow_web/services/batch_control_devices_service.dart'; -import 'package:syncrow_web/services/control_device_service.dart'; abstract final class FlushMountedPresenceSensorBlocFactory { const FlushMountedPresenceSensorBlocFactory._(); @@ -10,12 +9,8 @@ abstract final class FlushMountedPresenceSensorBlocFactory { }) { return FlushMountedPresenceSensorBloc( deviceId: deviceId, - controlDeviceService: DebouncedControlDeviceService( - decoratee: RemoteControlDeviceService(), - ), - batchControlDevicesService: DebouncedBatchControlDevicesService( - decoratee: RemoteBatchControlDevicesService(), - ), + controlDeviceService: DeviceBlocDependenciesFactory.createControlDeviceService(), + batchControlDevicesService: DeviceBlocDependenciesFactory.createBatchControlDevicesService(), ); } } From b60c6744960341488c35f3a4c238c153dd012a08 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 2 Jun 2025 10:12:46 +0300 Subject: [PATCH 31/58] Created a factory for the `WaterHeaterBloc`, and injected the necessary dependenices. --- .../water_heater/bloc/water_heater_bloc.dart | 202 ++++-------------- .../factories/water_heater_bloc_factory.dart | 18 ++ .../view/water_heater_batch_control.dart | 10 +- .../view/water_heater_device_control.dart | 9 +- 4 files changed, 76 insertions(+), 163 deletions(-) create mode 100644 lib/pages/device_managment/water_heater/factories/water_heater_bloc_factory.dart diff --git a/lib/pages/device_managment/water_heater/bloc/water_heater_bloc.dart b/lib/pages/device_managment/water_heater/bloc/water_heater_bloc.dart index 18a0787f..38c7f2d6 100644 --- a/lib/pages/device_managment/water_heater/bloc/water_heater_bloc.dart +++ b/lib/pages/device_managment/water_heater/bloc/water_heater_bloc.dart @@ -10,6 +10,8 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/device_sta 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/batch_control_devices_service.dart'; +import 'package:syncrow_web/services/control_device_service.dart'; import 'package:syncrow_web/services/devices_mang_api.dart'; import 'package:syncrow_web/utils/format_date_time.dart'; @@ -17,7 +19,17 @@ part 'water_heater_event.dart'; part 'water_heater_state.dart'; class WaterHeaterBloc extends Bloc { - WaterHeaterBloc() : super(WaterHeaterInitial()) { + late WaterHeaterStatusModel deviceStatus; + final String deviceId; + final ControlDeviceService controlDeviceService; + final BatchControlDevicesService batchControlDevicesService; + Timer? _countdownTimer; + + WaterHeaterBloc({ + required this.deviceId, + required this.controlDeviceService, + required this.batchControlDevicesService, + }) : super(WaterHeaterInitial()) { on(_fetchWaterHeaterStatus); on(_controlWaterHeater); on(_batchFetchWaterHeater); @@ -29,7 +41,6 @@ class WaterHeaterBloc extends Bloc { on(_updateSelectedTime); on(_updateSelectedDay); on(_updateFunctionOn); - on(_getSchedule); on(_onAddSchedule); on(_onEditSchedule); @@ -38,10 +49,6 @@ class WaterHeaterBloc extends Bloc { on(_onStatusUpdated); } - late WaterHeaterStatusModel deviceStatus; - Timer? _countdownTimer; - // Timer? _inchingTimer; - FutureOr _initializeAddSchedule( InitializeAddScheduleEvent event, Emitter emit, @@ -116,13 +123,11 @@ class WaterHeaterBloc extends Bloc { countdownRemaining: countdownRemaining, )); - if (!currentState.isCountdownActive! && - countdownRemaining > Duration.zero) { + if (!currentState.isCountdownActive! && countdownRemaining > Duration.zero) { _startCountdownTimer(emit, countdownRemaining); } } else if (event.scheduleMode == ScheduleModes.inching) { - final inchingDuration = - Duration(hours: event.hours, minutes: event.minutes); + final inchingDuration = Duration(hours: event.hours, minutes: event.minutes); emit(currentState.copyWith( scheduleMode: ScheduleModes.inching, @@ -141,21 +146,18 @@ class WaterHeaterBloc extends Bloc { if (state is WaterHeaterDeviceStatusLoaded) { final currentState = state as WaterHeaterDeviceStatusLoaded; - final oldValue = _getValueByCode(event.code); - _updateLocalValue(event.code, event.value); emit(currentState.copyWith( status: deviceStatus, )); - final success = await _runDebounce( - deviceId: event.deviceId, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - isBatch: false, + final success = await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status( + code: event.code, + value: event.value, + ), ); if (success) { @@ -182,15 +184,11 @@ class WaterHeaterBloc extends Bloc { } } else if (event.code == "switch_inching") { final inchingDuration = Duration(seconds: event.value); - //if (inchingDuration.inSeconds > 0) { - // _startInchingTimer(emit, inchingDuration); - // } else { emit(currentState.copyWith( inchingHours: inchingDuration.inHours, inchingMinutes: inchingDuration.inMinutes % 60, isInchingActive: true, )); - // } } } } @@ -224,8 +222,7 @@ class WaterHeaterBloc extends Bloc { try { final status = await DevicesManagementApi().deviceControl( event.deviceId, - Status( - code: isCountDown ? 'countdown_1' : 'switch_inching', value: 0), + Status(code: isCountDown ? 'countdown_1' : 'switch_inching', value: 0), ); if (!status) { emit(const WaterHeaterFailedState(error: 'Failed to stop schedule.')); @@ -243,10 +240,8 @@ class WaterHeaterBloc extends Bloc { emit(WaterHeaterLoadingState()); try { - final status = - await DevicesManagementApi().getDeviceStatus(event.deviceId); - deviceStatus = - WaterHeaterStatusModel.fromJson(event.deviceId, status.status); + final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); + deviceStatus = WaterHeaterStatusModel.fromJson(event.deviceId, status.status); if (deviceStatus.scheduleMode == ScheduleModes.countdown) { final countdownRemaining = Duration( @@ -288,7 +283,6 @@ class WaterHeaterBloc extends Bloc { inchingMinutes: deviceStatus.inchingMinutes, isInchingActive: true, )); -//_startInchingTimer(emit, inchingDuration); } else { emit(WaterHeaterDeviceStatusLoaded( deviceStatus, @@ -316,7 +310,7 @@ class WaterHeaterBloc extends Bloc { } } - _listenToChanges(deviceId) { + void _listenToChanges(deviceId) { try { DatabaseReference ref = FirebaseDatabase.instance.ref('device-status/$deviceId'); @@ -328,12 +322,11 @@ class WaterHeaterBloc 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 = WaterHeaterStatusModel.fromJson( - usersMap['productUuid'], statusList); + deviceStatus = + WaterHeaterStatusModel.fromJson(usersMap['productUuid'], statusList); if (!isClosed) { add(StatusUpdated(deviceStatus)); } @@ -357,17 +350,6 @@ class WaterHeaterBloc extends Bloc { }); } - // void _startInchingTimer( - // Emitter emit, - // Duration inchingDuration, - // ) { - // _inchingTimer?.cancel(); - - // _inchingTimer = Timer.periodic(const Duration(minutes: 1), (timer) { - // add(DecrementInchingEvent()); - // }); - // } - _onDecrementCountdown( DecrementCountdownEvent event, Emitter emit, @@ -405,85 +387,6 @@ class WaterHeaterBloc extends Bloc { } } - // FutureOr _onDecrementInching( - // DecrementInchingEvent event, - // Emitter emit, - // ) { - // if (state is WaterHeaterDeviceStatusLoaded) { - // final currentState = state as WaterHeaterDeviceStatusLoaded; - - // if (currentState.inchingHours > 0 || currentState.inchingMinutes > 0) { - // final newRemaining = Duration( - // hours: currentState.inchingHours, - // minutes: currentState.inchingMinutes, - // ) - - // const Duration(minutes: 1); - - // if (newRemaining <= Duration.zero) { - // _inchingTimer?.cancel(); - // emit(currentState.copyWith( - // inchingHours: 0, - // inchingMinutes: 0, - // isInchingActive: false, - // )); - // } else { - // emit(currentState.copyWith( - // inchingHours: newRemaining.inHours, - // inchingMinutes: newRemaining.inMinutes % 60, - // )); - // } - // } - // } - // } - - Future _runDebounce({ - required dynamic deviceId, - required String code, - required dynamic value, - required dynamic oldValue, - required Emitter emit, - required bool isBatch, - }) async { - try { - late bool status; - await Future.delayed(const Duration(milliseconds: 500)); - - if (isBatch) { - status = await DevicesManagementApi().deviceBatchControl( - deviceId, - code, - value, - ); - } else { - status = await DevicesManagementApi().deviceControl( - deviceId, - Status(code: code, value: value), - ); - } - - if (!status) { - _revertValue(code, oldValue, emit.call); - return false; - } else { - return true; - } - } catch (e) { - _revertValue(code, oldValue, emit.call); - return false; - } - } - - void _revertValue(String code, dynamic oldValue, - void Function(WaterHeaterState state) emit) { - _updateLocalValue(code, oldValue); - if (state is WaterHeaterDeviceStatusLoaded) { - final currentState = state as WaterHeaterDeviceStatusLoaded; - emit(currentState.copyWith( - status: deviceStatus, - )); - } - } - void _updateLocalValue(String code, dynamic value) { switch (code) { case 'switch_1': @@ -505,14 +408,12 @@ class WaterHeaterBloc extends Bloc { } dynamic _getValueByCode(String code) { - switch (code) { - case 'switch_1': - return deviceStatus.heaterSwitch; - case 'countdown_1': - return deviceStatus.countdownHours * 60 + deviceStatus.countdownMinutes; - default: - return null; - } + return switch (code) { + 'switch_1' => deviceStatus.heaterSwitch, + 'countdown_1' => + (deviceStatus.countdownHours * 60) + deviceStatus.countdownMinutes, + _ => null, + }; } @override @@ -571,8 +472,10 @@ class WaterHeaterBloc extends Bloc { } } - FutureOr _onEditSchedule(EditWaterHeaterScheduleEvent event, - Emitter emit) async { + FutureOr _onEditSchedule( + EditWaterHeaterScheduleEvent event, + Emitter emit, + ) async { if (state is WaterHeaterDeviceStatusLoaded) { final currentState = state as WaterHeaterDeviceStatusLoaded; @@ -584,8 +487,6 @@ class WaterHeaterBloc extends Bloc { days: ScheduleModel.convertSelectedDaysToStrings(event.selectedDays), ); - // emit(ScheduleLoadingState()); - bool success = await DevicesManagementApi().editScheduleRecord( currentState.status.uuid, newSchedule, @@ -595,7 +496,6 @@ class WaterHeaterBloc extends Bloc { add(GetSchedulesEvent(category: 'switch_1', uuid: deviceStatus.uuid)); } else { emit(currentState); - //emit(const WaterHeaterFailedState(error: 'Failed to add schedule.')); } } } @@ -627,7 +527,6 @@ class WaterHeaterBloc extends Bloc { emit(currentState.copyWith(schedules: updatedSchedules)); } else { emit(currentState); - // emit(const WaterHeaterFailedState(error: 'Failed to update schedule.')); } } } @@ -639,8 +538,6 @@ class WaterHeaterBloc extends Bloc { if (state is WaterHeaterDeviceStatusLoaded) { final currentState = state as WaterHeaterDeviceStatusLoaded; - // emit(ScheduleLoadingState()); - bool success = await DevicesManagementApi() .deleteScheduleRecord(currentState.status.uuid, event.scheduleId); @@ -652,20 +549,18 @@ class WaterHeaterBloc extends Bloc { emit(currentState.copyWith(schedules: updatedSchedules)); } else { emit(currentState); - // emit(const WaterHeaterFailedState(error: 'Failed to delete schedule.')); } } } - FutureOr _batchFetchWaterHeater(FetchWaterHeaterBatchStatusEvent event, - Emitter emit) async { + FutureOr _batchFetchWaterHeater( + FetchWaterHeaterBatchStatusEvent event, Emitter emit) async { emit(WaterHeaterLoadingState()); try { - final status = - await DevicesManagementApi().getBatchStatus(event.devicesUuid); - deviceStatus = WaterHeaterStatusModel.fromJson( - event.devicesUuid.first, status.status); + final status = await DevicesManagementApi().getBatchStatus(event.devicesUuid); + deviceStatus = + WaterHeaterStatusModel.fromJson(event.devicesUuid.first, status.status); emit(WaterHeaterDeviceStatusLoaded(deviceStatus)); } catch (e) { @@ -673,8 +568,8 @@ class WaterHeaterBloc extends Bloc { } } - FutureOr _batchControlWaterHeater(ControlWaterHeaterBatchEvent event, - Emitter emit) async { + FutureOr _batchControlWaterHeater( + ControlWaterHeaterBatchEvent event, Emitter emit) async { if (state is WaterHeaterDeviceStatusLoaded) { final currentState = state as WaterHeaterDeviceStatusLoaded; @@ -686,13 +581,10 @@ class WaterHeaterBloc extends Bloc { status: deviceStatus, )); - final success = await _runDebounce( - deviceId: event.devicesUuid, + final success = await batchControlDevicesService.batchControlDevices( + uuids: event.devicesUuid, code: event.code, value: event.value, - oldValue: oldValue, - emit: emit, - isBatch: true, ); if (success) { diff --git a/lib/pages/device_managment/water_heater/factories/water_heater_bloc_factory.dart b/lib/pages/device_managment/water_heater/factories/water_heater_bloc_factory.dart new file mode 100644 index 00000000..9c0c8ab6 --- /dev/null +++ b/lib/pages/device_managment/water_heater/factories/water_heater_bloc_factory.dart @@ -0,0 +1,18 @@ +import 'package:syncrow_web/pages/device_managment/factories/device_bloc_dependencies_factory.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; + +abstract final class WaterHeaterBlocFactory { + const WaterHeaterBlocFactory._(); + + static WaterHeaterBloc create({ + required String deviceId, + }) { + return WaterHeaterBloc( + deviceId: deviceId, + controlDeviceService: + DeviceBlocDependenciesFactory.createControlDeviceService(), + batchControlDevicesService: + DeviceBlocDependenciesFactory.createBatchControlDevicesService(), + ); + } +} diff --git a/lib/pages/device_managment/water_heater/view/water_heater_batch_control.dart b/lib/pages/device_managment/water_heater/view/water_heater_batch_control.dart index aaab5271..3c8a3858 100644 --- a/lib/pages/device_managment/water_heater/view/water_heater_batch_control.dart +++ b/lib/pages/device_managment/water_heater/view/water_heater_batch_control.dart @@ -1,15 +1,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; - import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; // import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.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'; +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/utils/constants/assets.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; -class WaterHEaterBatchControlView extends StatelessWidget with HelperResponsiveLayout { +class WaterHEaterBatchControlView extends StatelessWidget + with HelperResponsiveLayout { const WaterHEaterBatchControlView({super.key, required this.deviceIds}); final List deviceIds; @@ -17,8 +18,9 @@ class WaterHEaterBatchControlView extends StatelessWidget with HelperResponsiveL @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => - WaterHeaterBloc()..add(FetchWaterHeaterBatchStatusEvent(devicesUuid: deviceIds)), + create: (context) => WaterHeaterBlocFactory.create( + deviceId: deviceIds.first, + )..add(FetchWaterHeaterBatchStatusEvent(devicesUuid: deviceIds)), child: BlocBuilder( builder: (context, state) { if (state is WaterHeaterLoadingState) { 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 40d3edb5..f1e56136 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 @@ -5,6 +5,7 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_mo 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'; +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/utils/color_manager.dart'; @@ -21,8 +22,9 @@ class WaterHeaterDeviceControlView extends StatelessWidget @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => - WaterHeaterBloc()..add(WaterHeaterFetchStatusEvent(device.uuid!)), + create: (context) => WaterHeaterBlocFactory.create( + deviceId: device.uuid ?? '', + )..add(WaterHeaterFetchStatusEvent(device.uuid!)), child: BlocBuilder( builder: (context, state) { if (state is WaterHeaterLoadingState) { @@ -33,8 +35,7 @@ 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())); } }, )); From 19548e99abf8aa58008fe23fa2dec3af99a6ecc3 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 2 Jun 2025 10:18:20 +0300 Subject: [PATCH 32/58] indentation and formatting of `WaterHeaterBloc`. --- .../water_heater/bloc/water_heater_bloc.dart | 197 ++++++++++-------- .../water_heater/bloc/water_heater_state.dart | 2 - 2 files changed, 111 insertions(+), 88 deletions(-) diff --git a/lib/pages/device_managment/water_heater/bloc/water_heater_bloc.dart b/lib/pages/device_managment/water_heater/bloc/water_heater_bloc.dart index 38c7f2d6..560a61e1 100644 --- a/lib/pages/device_managment/water_heater/bloc/water_heater_bloc.dart +++ b/lib/pages/device_managment/water_heater/bloc/water_heater_bloc.dart @@ -1,5 +1,3 @@ -// water_heater_bloc.dart - import 'dart:async'; import 'package:bloc/bloc.dart'; @@ -49,7 +47,7 @@ class WaterHeaterBloc extends Bloc { on(_onStatusUpdated); } - FutureOr _initializeAddSchedule( + void _initializeAddSchedule( InitializeAddScheduleEvent event, Emitter emit, ) { @@ -71,7 +69,7 @@ class WaterHeaterBloc extends Bloc { } } - FutureOr _updateSelectedTime( + void _updateSelectedTime( UpdateSelectedTimeEvent event, Emitter emit, ) { @@ -80,7 +78,7 @@ class WaterHeaterBloc extends Bloc { emit(currentState.copyWith(selectedTime: event.selectedTime)); } - FutureOr _updateSelectedDay( + void _updateSelectedDay( UpdateSelectedDayEvent event, Emitter emit, ) { @@ -91,7 +89,7 @@ class WaterHeaterBloc extends Bloc { selectedDays: updatedDays, selectedTime: currentState.selectedTime)); } - FutureOr _updateFunctionOn( + void _updateFunctionOn( UpdateFunctionOnEvent event, Emitter emit, ) { @@ -100,16 +98,18 @@ class WaterHeaterBloc extends Bloc { functionOn: event.isOn, selectedTime: currentState.selectedTime)); } - FutureOr _updateScheduleEvent( + Future _updateScheduleEvent( UpdateScheduleEvent event, Emitter emit, ) async { final currentState = state; if (currentState is WaterHeaterDeviceStatusLoaded) { if (event.scheduleMode == ScheduleModes.schedule) { - emit(currentState.copyWith( - scheduleMode: ScheduleModes.schedule, - )); + emit( + currentState.copyWith( + scheduleMode: ScheduleModes.schedule, + ), + ); } if (event.scheduleMode == ScheduleModes.countdown) { final countdownRemaining = @@ -129,17 +129,19 @@ class WaterHeaterBloc extends Bloc { } else if (event.scheduleMode == ScheduleModes.inching) { final inchingDuration = Duration(hours: event.hours, minutes: event.minutes); - emit(currentState.copyWith( - scheduleMode: ScheduleModes.inching, - inchingHours: inchingDuration.inHours, - inchingMinutes: inchingDuration.inMinutes % 60, - isInchingActive: currentState.isInchingActive, - )); + emit( + currentState.copyWith( + scheduleMode: ScheduleModes.inching, + inchingHours: inchingDuration.inHours, + inchingMinutes: inchingDuration.inMinutes % 60, + isInchingActive: currentState.isInchingActive, + ), + ); } } } - FutureOr _controlWaterHeater( + Future _controlWaterHeater( ToggleWaterHeaterEvent event, Emitter emit, ) async { @@ -148,9 +150,11 @@ class WaterHeaterBloc extends Bloc { _updateLocalValue(event.code, event.value); - emit(currentState.copyWith( - status: deviceStatus, - )); + emit( + currentState.copyWith( + status: deviceStatus, + ), + ); final success = await controlDeviceService.controlDevice( deviceUuid: event.deviceId, @@ -164,37 +168,43 @@ class WaterHeaterBloc extends Bloc { if (event.code == "countdown_1") { final countdownDuration = Duration(seconds: event.value); - emit(currentState.copyWith( - countdownHours: countdownDuration.inHours, - countdownMinutes: countdownDuration.inMinutes % 60, - countdownRemaining: countdownDuration, - isCountdownActive: true, - )); + 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, - )); + emit( + currentState.copyWith( + countdownHours: 0, + countdownMinutes: 0, + countdownRemaining: Duration.zero, + isCountdownActive: false, + ), + ); } } else if (event.code == "switch_inching") { final inchingDuration = Duration(seconds: event.value); - emit(currentState.copyWith( - inchingHours: inchingDuration.inHours, - inchingMinutes: inchingDuration.inMinutes % 60, - isInchingActive: true, - )); + emit( + currentState.copyWith( + inchingHours: inchingDuration.inHours, + inchingMinutes: inchingDuration.inMinutes % 60, + isInchingActive: true, + ), + ); } } } } - FutureOr _stopScheduleEvent( + Future _stopScheduleEvent( StopScheduleEvent event, Emitter emit, ) async { @@ -205,18 +215,22 @@ class WaterHeaterBloc extends Bloc { _countdownTimer?.cancel(); if (isCountDown) { - emit(currentState.copyWith( - countdownHours: 0, - countdownMinutes: 0, - countdownRemaining: Duration.zero, - isCountdownActive: false, - )); + emit( + currentState.copyWith( + countdownHours: 0, + countdownMinutes: 0, + countdownRemaining: Duration.zero, + isCountdownActive: false, + ), + ); } else if (currentState.scheduleMode == ScheduleModes.inching) { - emit(currentState.copyWith( - inchingHours: 0, - inchingMinutes: 0, - isInchingActive: false, - )); + emit( + currentState.copyWith( + inchingHours: 0, + inchingMinutes: 0, + isInchingActive: false, + ), + ); } try { @@ -233,7 +247,7 @@ class WaterHeaterBloc extends Bloc { } } - FutureOr _fetchWaterHeaterStatus( + Future _fetchWaterHeaterStatus( WaterHeaterFetchStatusEvent event, Emitter emit, ) async { @@ -334,7 +348,10 @@ class WaterHeaterBloc extends Bloc { } catch (_) {} } - void _onStatusUpdated(StatusUpdated event, Emitter emit) { + void _onStatusUpdated( + StatusUpdated event, + Emitter emit, + ) { deviceStatus = event.deviceStatus; emit(WaterHeaterDeviceStatusLoaded(deviceStatus)); } @@ -345,12 +362,13 @@ class WaterHeaterBloc extends Bloc { ) { _countdownTimer?.cancel(); - _countdownTimer = Timer.periodic(const Duration(minutes: 1), (timer) { - add(DecrementCountdownEvent()); - }); + _countdownTimer = Timer.periodic( + const Duration(minutes: 1), + (timer) => add(DecrementCountdownEvent()), + ); } - _onDecrementCountdown( + void _onDecrementCountdown( DecrementCountdownEvent event, Emitter emit, ) { @@ -364,25 +382,28 @@ class WaterHeaterBloc extends Bloc { if (newRemaining <= Duration.zero) { _countdownTimer?.cancel(); - emit(currentState.copyWith( - countdownHours: 0, - countdownMinutes: 0, - isCountdownActive: false, - countdownRemaining: Duration.zero, - )); + emit( + currentState.copyWith( + countdownHours: 0, + countdownMinutes: 0, + isCountdownActive: false, + countdownRemaining: Duration.zero, + ), + ); return; } - int totalSeconds = newRemaining.inSeconds; + final totalSeconds = newRemaining.inSeconds; + final newHours = totalSeconds ~/ 3600; + final newMinutes = (totalSeconds % 3600) ~/ 60; - int newHours = totalSeconds ~/ 3600; - int newMinutes = (totalSeconds % 3600) ~/ 60; - - emit(currentState.copyWith( - countdownHours: newHours, - countdownMinutes: newMinutes, - countdownRemaining: newRemaining, - )); + emit( + currentState.copyWith( + countdownHours: newHours, + countdownMinutes: newMinutes, + countdownRemaining: newRemaining, + ), + ); } } } @@ -422,13 +443,17 @@ class WaterHeaterBloc extends Bloc { return super.close(); } - FutureOr _getSchedule( - GetSchedulesEvent event, Emitter emit) async { + Future _getSchedule( + GetSchedulesEvent event, + Emitter emit, + ) async { emit(ScheduleLoadingState()); try { - List schedules = await DevicesManagementApi() - .getDeviceSchedules(deviceStatus.uuid, event.category); + final schedules = await DevicesManagementApi().getDeviceSchedules( + deviceStatus.uuid, + event.category, + ); emit(WaterHeaterDeviceStatusLoaded( deviceStatus, @@ -436,7 +461,6 @@ class WaterHeaterBloc extends Bloc { scheduleMode: ScheduleModes.schedule, )); } catch (e) { - //(const WaterHeaterFailedState(error: 'Failed to fetch schedules.')); emit(WaterHeaterDeviceStatusLoaded( deviceStatus, schedules: const [], @@ -444,7 +468,7 @@ class WaterHeaterBloc extends Bloc { } } - FutureOr _onAddSchedule( + Future _onAddSchedule( AddScheduleEvent event, Emitter emit, ) async { @@ -458,8 +482,6 @@ class WaterHeaterBloc extends Bloc { days: ScheduleModel.convertSelectedDaysToStrings(event.selectedDays), ); - // emit(ScheduleLoadingState()); - bool success = await DevicesManagementApi() .addScheduleRecord(newSchedule, currentState.status.uuid); @@ -467,12 +489,11 @@ class WaterHeaterBloc extends Bloc { add(GetSchedulesEvent(category: 'switch_1', uuid: deviceStatus.uuid)); } else { emit(currentState); - //emit(const WaterHeaterFailedState(error: 'Failed to add schedule.')); } } } - FutureOr _onEditSchedule( + Future _onEditSchedule( EditWaterHeaterScheduleEvent event, Emitter emit, ) async { @@ -500,7 +521,7 @@ class WaterHeaterBloc extends Bloc { } } - FutureOr _onUpdateSchedule( + Future _onUpdateSchedule( UpdateScheduleEntryEvent event, Emitter emit, ) async { @@ -531,7 +552,7 @@ class WaterHeaterBloc extends Bloc { } } - FutureOr _onDeleteSchedule( + Future _onDeleteSchedule( DeleteScheduleEvent event, Emitter emit, ) async { @@ -553,12 +574,16 @@ class WaterHeaterBloc extends Bloc { } } - FutureOr _batchFetchWaterHeater( - FetchWaterHeaterBatchStatusEvent event, Emitter emit) async { + Future _batchFetchWaterHeater( + FetchWaterHeaterBatchStatusEvent event, + Emitter emit, + ) async { emit(WaterHeaterLoadingState()); try { - final status = await DevicesManagementApi().getBatchStatus(event.devicesUuid); + final status = await DevicesManagementApi().getBatchStatus( + event.devicesUuid, + ); deviceStatus = WaterHeaterStatusModel.fromJson(event.devicesUuid.first, status.status); @@ -568,7 +593,7 @@ class WaterHeaterBloc extends Bloc { } } - FutureOr _batchControlWaterHeater( + Future _batchControlWaterHeater( ControlWaterHeaterBatchEvent event, Emitter emit) async { if (state is WaterHeaterDeviceStatusLoaded) { final currentState = state as WaterHeaterDeviceStatusLoaded; diff --git a/lib/pages/device_managment/water_heater/bloc/water_heater_state.dart b/lib/pages/device_managment/water_heater/bloc/water_heater_state.dart index c2df43c3..974f5f2d 100644 --- a/lib/pages/device_managment/water_heater/bloc/water_heater_state.dart +++ b/lib/pages/device_managment/water_heater/bloc/water_heater_state.dart @@ -1,5 +1,3 @@ -// water_heater_state.dart - part of 'water_heater_bloc.dart'; sealed class WaterHeaterState extends Equatable { From f98636a2e5ba51d4fda99e6113439dd45710fdd1 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 2 Jun 2025 10:44:43 +0300 Subject: [PATCH 33/58] Migrated `AcBloc` single/batch controls the new services. --- .../device_managment/ac/bloc/ac_bloc.dart | 340 ++++++++---------- .../ac/factories/ac_bloc_factory.dart | 18 + .../ac/view/ac_device_batch_control.dart | 7 +- .../ac/view/ac_device_control.dart | 6 +- 4 files changed, 175 insertions(+), 196 deletions(-) create mode 100644 lib/pages/device_managment/ac/factories/ac_bloc_factory.dart diff --git a/lib/pages/device_managment/ac/bloc/ac_bloc.dart b/lib/pages/device_managment/ac/bloc/ac_bloc.dart index 501d29d8..af5a7b0a 100644 --- a/lib/pages/device_managment/ac/bloc/ac_bloc.dart +++ b/lib/pages/device_managment/ac/bloc/ac_bloc.dart @@ -1,21 +1,27 @@ import 'dart:async'; -import 'package:dio/dio.dart'; + import 'package:firebase_database/firebase_database.dart'; -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/ac/bloc/ac_event.dart'; import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_state.dart'; import 'package:syncrow_web/pages/device_managment/ac/model/ac_model.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.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'; class AcBloc extends Bloc { late AcStatusModel deviceStatus; final String deviceId; - Timer? _timer; + final ControlDeviceService controlDeviceService; + final BatchControlDevicesService batchControlDevicesService; Timer? _countdownTimer; - AcBloc({required this.deviceId}) : super(AcsInitialState()) { + AcBloc({ + required this.deviceId, + required this.controlDeviceService, + required this.batchControlDevicesService, + }) : super(AcsInitialState()) { on(_onFetchAcStatus); on(_onFetchAcBatchStatus); on(_onAcControl); @@ -34,14 +40,14 @@ class AcBloc extends Bloc { int scheduledMinutes = 0; FutureOr _onFetchAcStatus( - AcFetchDeviceStatusEvent event, Emitter emit) async { + AcFetchDeviceStatusEvent event, + Emitter emit, + ) 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) { - // Convert API value to minutes final totalMinutes = deviceStatus.countdown1 * 6; scheduledHours = totalMinutes ~/ 60; scheduledMinutes = totalMinutes % 60; @@ -62,30 +68,24 @@ class AcBloc extends Bloc { } } - _listenToChanges(deviceId) { + void _listenToChanges(deviceId) { try { - DatabaseReference ref = - FirebaseDatabase.instance.ref('device-status/$deviceId'); - Stream stream = ref.onValue; + final ref = FirebaseDatabase.instance.ref('device-status/$deviceId'); + final stream = ref.onValue; stream.listen((DatabaseEvent event) async { if (event.snapshot.value == null) return; - if (_timer != null) { - await Future.delayed(const Duration(seconds: 1)); - } Map usersMap = event.snapshot.value as Map; 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)); } @@ -93,146 +93,44 @@ class AcBloc extends Bloc { } catch (_) {} } - void _onAcStatusUpdated(AcStatusUpdated event, Emitter emit) { + void _onAcStatusUpdated( + AcStatusUpdated event, + Emitter emit, + ) { deviceStatus = event.deviceStatus; emit(ACStatusLoaded(status: deviceStatus)); } FutureOr _onAcControl( - AcControlEvent event, Emitter emit) async { - final oldValue = _getValueByCode(event.code); - - _updateLocalValue(event.code, event.value, emit); - + AcControlEvent event, + Emitter emit, + ) async { + emit(AcsLoadingState()); + _updateDeviceFunctionFromCode(event.code, event.value); emit(ACStatusLoaded(status: deviceStatus)); - await _runDebounce( - isBatch: false, - deviceId: event.deviceId, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - ); - } + try { + final success = await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status(code: event.code, value: event.value), + ); - Future _runDebounce({ - required dynamic deviceId, - required String code, - required dynamic value, - required dynamic oldValue, - required Emitter emit, - required bool isBatch, - }) async { - late String id; - - if (deviceId is List) { - id = deviceId.first; - } else { - id = deviceId; - } - - if (_timer != null) { - _timer!.cancel(); - } - _timer = Timer(const Duration(seconds: 1), () async { - try { - late bool response; - if (isBatch) { - response = await DevicesManagementApi() - .deviceBatchControl(deviceId, code, value); - } else { - response = await DevicesManagementApi() - .deviceControl(deviceId, Status(code: code, value: value)); - } - - if (!response) { - _revertValueAndEmit(id, code, oldValue, emit); - } - } catch (e) { - if (e is DioException && e.response != null) { - debugPrint('Error response: ${e.response?.data}'); - } - _revertValueAndEmit(id, code, oldValue, emit); + if (!success) { + emit(const AcsFailedState(error: 'Failed to control device')); } - }); - } - - void _revertValueAndEmit( - String deviceId, String code, dynamic oldValue, Emitter emit) { - _updateLocalValue(code, oldValue, emit); - emit(ACStatusLoaded(status: deviceStatus)); - } - - void _updateLocalValue(String code, dynamic value, Emitter emit) { - switch (code) { - case 'switch': - if (value is bool) { - deviceStatus = deviceStatus.copyWith(acSwitch: value); - } - break; - case 'temp_set': - if (value is int) { - deviceStatus = deviceStatus.copyWith(tempSet: value); - } - break; - case 'mode': - if (value is String) { - deviceStatus = deviceStatus.copyWith( - modeString: value, - ); - } - break; - case 'level': - if (value is String) { - deviceStatus = deviceStatus.copyWith( - fanSpeedsString: value, - ); - } - break; - case 'child_lock': - if (value is bool) { - deviceStatus = deviceStatus.copyWith(childLock: value); - } - - case 'countdown_time': - if (value is int) { - deviceStatus = deviceStatus.copyWith(countdown1: value); - } - break; - default: - break; - } - emit(ACStatusLoaded(status: deviceStatus)); - } - - dynamic _getValueByCode(String code) { - switch (code) { - case 'switch': - return deviceStatus.acSwitch; - case 'temp_set': - return deviceStatus.tempSet; - case 'mode': - return deviceStatus.modeString; - case 'level': - return deviceStatus.fanSpeedsString; - case 'child_lock': - return deviceStatus.childLock; - case 'countdown_time': - return deviceStatus.countdown1; - default: - return null; + } catch (e) { + emit(AcsFailedState(error: e.toString())); } } FutureOr _onFetchAcBatchStatus( - AcFetchBatchStatusEvent event, Emitter emit) async { + AcFetchBatchStatusEvent event, + Emitter emit, + ) 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())); @@ -240,25 +138,32 @@ class AcBloc extends Bloc { } FutureOr _onAcBatchControl( - AcBatchControlEvent event, Emitter emit) async { - final oldValue = _getValueByCode(event.code); - - _updateLocalValue(event.code, event.value, emit); - + AcBatchControlEvent event, + Emitter emit, + ) async { + emit(AcsLoadingState()); + _updateDeviceFunctionFromCode(event.code, event.value); emit(ACStatusLoaded(status: deviceStatus)); - await _runDebounce( - isBatch: true, - deviceId: event.devicesIds, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - ); + try { + final success = 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())); + } } - FutureOr _onFactoryReset( - AcFactoryResetEvent event, Emitter emit) async { + Future _onFactoryReset( + AcFactoryResetEvent event, + Emitter emit, + ) async { emit(AcsLoadingState()); try { final response = await DevicesManagementApi().factoryReset( @@ -275,9 +180,11 @@ class AcBloc extends Bloc { } } - void _onClose(OnClose event, Emitter emit) { + void _onClose( + OnClose event, + Emitter emit, + ) { _countdownTimer?.cancel(); - _timer?.cancel(); } void _handleIncreaseTime(IncreaseTimeEvent event, Emitter emit) { @@ -300,7 +207,10 @@ class AcBloc extends Bloc { )); } - void _handleDecreaseTime(DecreaseTimeEvent event, Emitter emit) { + void _handleDecreaseTime( + DecreaseTimeEvent event, + Emitter emit, + ) { if (state is! ACStatusLoaded) return; final currentState = state as ACStatusLoaded; int totalMinutes = (scheduledHours * 60) + scheduledMinutes; @@ -315,7 +225,9 @@ class AcBloc extends Bloc { } Future _handleToggleTimer( - ToggleScheduleEvent event, Emitter emit) async { + ToggleScheduleEvent event, + Emitter emit, + ) async { if (state is! ACStatusLoaded) return; final currentState = state as ACStatusLoaded; @@ -331,37 +243,44 @@ class AcBloc extends Bloc { try { final scaledValue = totalMinutes ~/ 6; - await _runDebounce( - isBatch: false, - deviceId: deviceId, - code: 'countdown_time', - value: scaledValue, - oldValue: scaledValue, - emit: emit, + final success = await controlDeviceService.controlDevice( + deviceUuid: deviceId, + status: Status(code: 'countdown_time', value: scaledValue), ); - _startCountdownTimer(emit); - emit(currentState.copyWith(isTimerActive: timerActive)); + + if (success) { + _startCountdownTimer(emit); + emit(currentState.copyWith(isTimerActive: timerActive)); + } else { + timerActive = false; + emit(const AcsFailedState(error: 'Failed to set timer')); + } } catch (e) { timerActive = false; emit(AcsFailedState(error: e.toString())); } } else { - await _runDebounce( - isBatch: false, - deviceId: deviceId, - code: 'countdown_time', - value: 0, - oldValue: 0, - emit: emit, - ); - _countdownTimer?.cancel(); - scheduledHours = 0; - scheduledMinutes = 0; - emit(currentState.copyWith( - isTimerActive: timerActive, - scheduledHours: 0, - scheduledMinutes: 0, - )); + try { + final success = await controlDeviceService.controlDevice( + deviceUuid: deviceId, + status: Status(code: 'countdown_time', value: 0), + ); + + if (success) { + _countdownTimer?.cancel(); + scheduledHours = 0; + scheduledMinutes = 0; + emit(currentState.copyWith( + isTimerActive: timerActive, + scheduledHours: 0, + scheduledMinutes: 0, + )); + } else { + emit(const AcsFailedState(error: 'Failed to stop timer')); + } + } catch (e) { + emit(AcsFailedState(error: e.toString())); + } } } @@ -385,7 +304,10 @@ class AcBloc extends Bloc { }); } - void _handleUpdateTimer(UpdateTimerEvent event, Emitter emit) { + void _handleUpdateTimer( + UpdateTimerEvent event, + Emitter emit, + ) { if (state is ACStatusLoaded) { final currentState = state as ACStatusLoaded; emit(currentState.copyWith( @@ -400,7 +322,6 @@ class AcBloc extends Bloc { ApiCountdownValueEvent event, Emitter emit) { if (state is ACStatusLoaded) { final totalMinutes = event.apiValue * 6; - final scheduledHours = totalMinutes ~/ 60; scheduledMinutes = totalMinutes % 60; _startCountdownTimer( emit, @@ -409,6 +330,43 @@ class AcBloc extends Bloc { } } + void _updateDeviceFunctionFromCode(String code, dynamic value) { + switch (code) { + case 'switch': + if (value is bool) { + deviceStatus = deviceStatus.copyWith(acSwitch: value); + } + break; + case 'temp_set': + if (value is int) { + deviceStatus = deviceStatus.copyWith(tempSet: value); + } + break; + case 'mode': + if (value is String) { + deviceStatus = deviceStatus.copyWith(modeString: value); + } + break; + case 'level': + if (value is String) { + deviceStatus = deviceStatus.copyWith(fanSpeedsString: value); + } + break; + case 'child_lock': + if (value is bool) { + deviceStatus = deviceStatus.copyWith(childLock: value); + } + break; + case 'countdown_time': + if (value is int) { + deviceStatus = deviceStatus.copyWith(countdown1: value); + } + break; + default: + break; + } + } + @override Future close() { add(OnClose()); diff --git a/lib/pages/device_managment/ac/factories/ac_bloc_factory.dart b/lib/pages/device_managment/ac/factories/ac_bloc_factory.dart new file mode 100644 index 00000000..9e5f4c1c --- /dev/null +++ b/lib/pages/device_managment/ac/factories/ac_bloc_factory.dart @@ -0,0 +1,18 @@ +import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/factories/device_bloc_dependencies_factory.dart'; + +abstract final class AcBlocFactory { + const AcBlocFactory._(); + + static AcBloc create({ + required String deviceId, + }) { + return AcBloc( + deviceId: deviceId, + controlDeviceService: + DeviceBlocDependenciesFactory.createControlDeviceService(), + batchControlDevicesService: + DeviceBlocDependenciesFactory.createBatchControlDevicesService(), + ); + } +} diff --git a/lib/pages/device_managment/ac/view/ac_device_batch_control.dart b/lib/pages/device_managment/ac/view/ac_device_batch_control.dart index 3005c1c5..aad0669b 100644 --- a/lib/pages/device_managment/ac/view/ac_device_batch_control.dart +++ b/lib/pages/device_managment/ac/view/ac_device_batch_control.dart @@ -3,12 +3,12 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_bloc.dart'; import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_event.dart'; import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_state.dart'; +import 'package:syncrow_web/pages/device_managment/ac/factories/ac_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/ac/view/batch_control_list/batch_ac_mode.dart'; import 'package:syncrow_web/pages/device_managment/ac/view/batch_control_list/batch_current_temp.dart'; import 'package:syncrow_web/pages/device_managment/ac/view/batch_control_list/batch_fan_speed.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; -// import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.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'; @@ -26,8 +26,9 @@ class AcDeviceBatchControlView extends StatelessWidget with HelperResponsiveLayo final isLarge = isLargeScreenSize(context); final isMedium = isMediumScreenSize(context); return BlocProvider( - create: (context) => - AcBloc(deviceId: devicesIds.first)..add(AcFetchBatchStatusEvent(devicesIds)), + create: (context) => AcBlocFactory.create( + deviceId: devicesIds.first, + )..add(AcFetchBatchStatusEvent(devicesIds)), child: BlocBuilder( builder: (context, state) { if (state is ACStatusLoaded) { diff --git a/lib/pages/device_managment/ac/view/ac_device_control.dart b/lib/pages/device_managment/ac/view/ac_device_control.dart index 8c33c853..a882e6d5 100644 --- a/lib/pages/device_managment/ac/view/ac_device_control.dart +++ b/lib/pages/device_managment/ac/view/ac_device_control.dart @@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_bloc.dart'; import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_event.dart'; import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_state.dart'; +import 'package:syncrow_web/pages/device_managment/ac/factories/ac_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/ac/view/control_list/ac_mode.dart'; import 'package:syncrow_web/pages/device_managment/ac/view/control_list/current_temp.dart'; import 'package:syncrow_web/pages/device_managment/ac/view/control_list/fan_speed.dart'; @@ -24,8 +25,9 @@ class AcDeviceControlsView extends StatelessWidget with HelperResponsiveLayout { final isMedium = isMediumScreenSize(context); return BlocProvider( - create: (context) => AcBloc(deviceId: device.uuid!) - ..add(AcFetchDeviceStatusEvent(device.uuid!)), + create: (context) => AcBlocFactory.create( + deviceId: device.uuid!, + )..add(AcFetchDeviceStatusEvent(device.uuid!)), child: BlocBuilder( builder: (context, state) { final acBloc = BlocProvider.of(context); From 3bd2bd114b0e461900fc0211d6dc2a7099b7ebde Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 2 Jun 2025 11:13:56 +0300 Subject: [PATCH 34/58] migrate `CeilingSensorBloc` to use the new services. --- .../ceiling_sensor/bloc/ceiling_bloc.dart | 224 ++++++++---------- .../ceiling_sensor_bloc_factory.dart | 18 ++ .../view/ceiling_sensor_batch_control.dart | 8 +- .../view/ceiling_sensor_controls.dart | 6 +- 4 files changed, 129 insertions(+), 127 deletions(-) create mode 100644 lib/pages/device_managment/ceiling_sensor/factories/ceiling_sensor_bloc_factory.dart diff --git a/lib/pages/device_managment/ceiling_sensor/bloc/ceiling_bloc.dart b/lib/pages/device_managment/ceiling_sensor/bloc/ceiling_bloc.dart index 4e8d5a8b..42387e57 100644 --- a/lib/pages/device_managment/ceiling_sensor/bloc/ceiling_bloc.dart +++ b/lib/pages/device_managment/ceiling_sensor/bloc/ceiling_bloc.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import 'package:firebase_database/firebase_database.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; @@ -7,14 +5,21 @@ import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/ceiling_e import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/ceiling_state.dart'; import 'package:syncrow_web/pages/device_managment/ceiling_sensor/model/ceiling_sensor_model.dart'; import 'package:syncrow_web/pages/device_managment/ceiling_sensor/model/help_description.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'; class CeilingSensorBloc extends Bloc { final String deviceId; + final ControlDeviceService controlDeviceService; + final BatchControlDevicesService batchControlDevicesService; late CeilingSensorModel deviceStatus; - Timer? _timer; - CeilingSensorBloc({required this.deviceId}) : super(CeilingInitialState()) { + CeilingSensorBloc({ + required this.deviceId, + required this.controlDeviceService, + required this.batchControlDevicesService, + }) : super(CeilingInitialState()) { on(_fetchCeilingSensorStatus); on(_fetchCeilingSensorBatchControl); on(_changeValue); @@ -26,35 +31,34 @@ class CeilingSensorBloc extends Bloc { on(_onStatusUpdated); } - void _fetchCeilingSensorStatus( - CeilingInitialEvent event, Emitter emit) async { + Future _fetchCeilingSensorStatus( + CeilingInitialEvent event, + Emitter emit, + ) async { emit(CeilingLoadingInitialState()); try { - var response = - await DevicesManagementApi().getDeviceStatus(event.deviceId); + final response = await DevicesManagementApi().getDeviceStatus(event.deviceId); deviceStatus = CeilingSensorModel.fromJson(response.status); emit(CeilingUpdateState(ceilingSensorModel: deviceStatus)); _listenToChanges(event.deviceId); } catch (e) { emit(CeilingFailedState(error: e.toString())); - return; } } - _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'); + final stream = ref.onValue; stream.listen((DatabaseEvent event) { - Map usersMap = - event.snapshot.value as Map; + if (event.snapshot.value == null) return; + + final usersMap = event.snapshot.value as Map; + final statusList = []; - 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 = CeilingSensorModel.fromJson(statusList); @@ -65,149 +69,127 @@ class CeilingSensorBloc extends Bloc { } catch (_) {} } - void _onStatusUpdated(StatusUpdated event, Emitter emit) { + void _onStatusUpdated( + StatusUpdated event, + Emitter emit, + ) { deviceStatus = event.deviceStatus; emit(CeilingUpdateState(ceilingSensorModel: deviceStatus)); } - void _changeValue( - CeilingChangeValueEvent event, Emitter emit) async { + Future _changeValue( + CeilingChangeValueEvent event, + Emitter emit, + ) async { emit(CeilingLoadingNewSate(ceilingSensorModel: deviceStatus)); - if (event.code == 'sensitivity') { - deviceStatus.sensitivity = event.value; - } else if (event.code == 'none_body_time') { - deviceStatus.noBodyTime = event.value; - } else if (event.code == 'moving_max_dis') { - deviceStatus.maxDistance = event.value; - } else if (event.code == 'scene') { - deviceStatus.spaceType = getSpaceType(event.value); - } + _updateDeviceFunctionFromCode(event.code, event.value); emit(CeilingUpdateState(ceilingSensorModel: deviceStatus)); - await _runDeBouncer( - deviceId: deviceId, - code: event.code, - value: event.value, - emit: emit, - isBatch: false, - ); + + try { + await controlDeviceService.controlDevice( + deviceUuid: deviceId, + status: Status(code: event.code, value: event.value), + ); + } catch (e) { + emit(CeilingFailedState(error: e.toString())); + } } Future _onBatchControl( - CeilingBatchControlEvent event, Emitter emit) async { + CeilingBatchControlEvent event, + Emitter emit, + ) async { emit(CeilingLoadingNewSate(ceilingSensorModel: deviceStatus)); - if (event.code == 'sensitivity') { - deviceStatus.sensitivity = event.value; - } else if (event.code == 'none_body_time') { - deviceStatus.noBodyTime = event.value; - } else if (event.code == 'moving_max_dis') { - deviceStatus.maxDistance = event.value; - } else if (event.code == 'scene') { - deviceStatus.spaceType = getSpaceType(event.value); - } + _updateDeviceFunctionFromCode(event.code, event.value); emit(CeilingUpdateState(ceilingSensorModel: deviceStatus)); - await _runDeBouncer( - deviceId: event.deviceIds, - code: event.code, - value: event.value, - emit: emit, - isBatch: true, - ); - } - _runDeBouncer({ - required dynamic deviceId, - required String code, - required dynamic value, - required Emitter emit, - required bool isBatch, - }) { - late String id; + try { + final success = await batchControlDevicesService.batchControlDevices( + uuids: event.deviceIds, + code: event.code, + value: event.value, + ); - if (deviceId is List) { - id = deviceId.first; - } else { - id = deviceId; - } - - if (_timer != null) { - _timer!.cancel(); - } - _timer = Timer(const Duration(seconds: 1), () async { - try { - late bool response; - if (isBatch) { - response = await DevicesManagementApi() - .deviceBatchControl(deviceId, code, value); - } else { - response = await DevicesManagementApi() - .deviceControl(deviceId, Status(code: code, value: value)); - } - - if (!response) { - add(CeilingInitialEvent(id)); - } - if (response == true && code == 'scene') { - emit(CeilingLoadingInitialState()); - await Future.delayed(const Duration(seconds: 1)); - add(CeilingInitialEvent(id)); - } - } catch (_) { - await Future.delayed(const Duration(milliseconds: 500)); - add(CeilingInitialEvent(id)); + if (!success) { + emit(const CeilingFailedState(error: 'Failed to control devices')); } - }); + } catch (e) { + emit(CeilingFailedState(error: e.toString())); + } } - FutureOr _getDeviceReports(GetCeilingDeviceReportsEvent event, - Emitter emit) async { + void _updateDeviceFunctionFromCode(String code, dynamic value) { + switch (code) { + case 'sensitivity': + deviceStatus.sensitivity = value; + break; + case 'none_body_time': + deviceStatus.noBodyTime = value; + break; + case 'moving_max_dis': + deviceStatus.maxDistance = value; + break; + case 'scene': + deviceStatus.spaceType = getSpaceType(value); + break; + default: + break; + } + } + + Future _getDeviceReports( + GetCeilingDeviceReportsEvent event, + Emitter emit, + ) async { if (event.code.isEmpty) { emit(ShowCeilingDescriptionState(description: reportString)); return; - } else { - emit(CeilingReportsLoadingState()); - // final from = DateTime.now().subtract(const Duration(days: 30)).millisecondsSinceEpoch; - // final to = DateTime.now().millisecondsSinceEpoch; + } - try { - // await DevicesManagementApi.getDeviceReportsByDate(deviceId, event.code, from.toString(), to.toString()) - await DevicesManagementApi.getDeviceReports(deviceId, event.code) - .then((value) { - emit(CeilingReportsState(deviceReport: value)); - }); - } catch (e) { - emit(CeilingReportsFailedState(error: e.toString())); - return; - } + emit(CeilingReportsLoadingState()); + try { + final value = await DevicesManagementApi.getDeviceReports( + deviceId, + event.code, + ); + emit(CeilingReportsState(deviceReport: value)); + } catch (e) { + emit(CeilingReportsFailedState(error: e.toString())); } } void _showDescription( - ShowCeilingDescriptionEvent event, Emitter emit) { + ShowCeilingDescriptionEvent event, + Emitter emit, + ) { emit(ShowCeilingDescriptionState(description: event.description)); } void _backToGridView( - BackToCeilingGridViewEvent event, Emitter emit) { + BackToCeilingGridViewEvent event, + Emitter emit, + ) { emit(CeilingUpdateState(ceilingSensorModel: deviceStatus)); } - FutureOr _fetchCeilingSensorBatchControl( - CeilingFetchDeviceStatusEvent event, - Emitter emit) async { + Future _fetchCeilingSensorBatchControl( + CeilingFetchDeviceStatusEvent event, + Emitter emit, + ) async { emit(CeilingLoadingInitialState()); try { - var response = - await DevicesManagementApi().getBatchStatus(event.devicesIds); + final response = await DevicesManagementApi().getBatchStatus(event.devicesIds); deviceStatus = CeilingSensorModel.fromJson(response.status); emit(CeilingUpdateState(ceilingSensorModel: deviceStatus)); } catch (e) { emit(CeilingFailedState(error: e.toString())); - return; } } - FutureOr _onFactoryReset( - CeilingFactoryResetEvent event, Emitter emit) async { + Future _onFactoryReset( + CeilingFactoryResetEvent event, + Emitter emit, + ) async { emit(CeilingLoadingNewSate(ceilingSensorModel: deviceStatus)); try { final response = await DevicesManagementApi().factoryReset( diff --git a/lib/pages/device_managment/ceiling_sensor/factories/ceiling_sensor_bloc_factory.dart b/lib/pages/device_managment/ceiling_sensor/factories/ceiling_sensor_bloc_factory.dart new file mode 100644 index 00000000..d371efb1 --- /dev/null +++ b/lib/pages/device_managment/ceiling_sensor/factories/ceiling_sensor_bloc_factory.dart @@ -0,0 +1,18 @@ +import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/ceiling_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/factories/device_bloc_dependencies_factory.dart'; + +abstract final class CeilingSensorBlocFactory { + const CeilingSensorBlocFactory._(); + + static CeilingSensorBloc create({ + required String deviceId, + }) { + return CeilingSensorBloc( + deviceId: deviceId, + controlDeviceService: + DeviceBlocDependenciesFactory.createControlDeviceService(), + batchControlDevicesService: + DeviceBlocDependenciesFactory.createBatchControlDevicesService(), + ); + } +} diff --git a/lib/pages/device_managment/ceiling_sensor/view/ceiling_sensor_batch_control.dart b/lib/pages/device_managment/ceiling_sensor/view/ceiling_sensor_batch_control.dart index cf645b6f..9b5ab360 100644 --- a/lib/pages/device_managment/ceiling_sensor/view/ceiling_sensor_batch_control.dart +++ b/lib/pages/device_managment/ceiling_sensor/view/ceiling_sensor_batch_control.dart @@ -4,9 +4,9 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_re import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/ceiling_bloc.dart'; import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/ceiling_event.dart'; import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/ceiling_state.dart'; +import 'package:syncrow_web/pages/device_managment/ceiling_sensor/factories/ceiling_sensor_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/ceiling_sensor/model/ceiling_sensor_model.dart'; import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; -// import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presence_space_type.dart'; import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presence_update_data.dart'; import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presense_nobody_time.dart'; @@ -23,8 +23,9 @@ class CeilingSensorBatchControlView extends StatelessWidget with HelperResponsiv final isLarge = isLargeScreenSize(context); final isMedium = isMediumScreenSize(context); return BlocProvider( - create: (context) => CeilingSensorBloc(deviceId: devicesIds.first) - ..add(CeilingFetchDeviceStatusEvent(devicesIds)), + create: (context) => CeilingSensorBlocFactory.create( + deviceId: devicesIds.first, + )..add(CeilingFetchDeviceStatusEvent(devicesIds)), child: BlocBuilder( builder: (context, state) { if (state is CeilingLoadingInitialState || state is CeilingReportsLoadingState) { @@ -110,7 +111,6 @@ class CeilingSensorBatchControlView extends StatelessWidget with HelperResponsiv ), ), ), - // FirmwareUpdateWidget(deviceId: devicesIds.first, version: 4), FactoryResetWidget( callFactoryReset: () { context.read().add( diff --git a/lib/pages/device_managment/ceiling_sensor/view/ceiling_sensor_controls.dart b/lib/pages/device_managment/ceiling_sensor/view/ceiling_sensor_controls.dart index 36b676e9..f3017a7c 100644 --- a/lib/pages/device_managment/ceiling_sensor/view/ceiling_sensor_controls.dart +++ b/lib/pages/device_managment/ceiling_sensor/view/ceiling_sensor_controls.dart @@ -4,6 +4,7 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_mo import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/ceiling_bloc.dart'; import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/ceiling_event.dart'; import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/ceiling_state.dart'; +import 'package:syncrow_web/pages/device_managment/ceiling_sensor/factories/ceiling_sensor_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/ceiling_sensor/model/ceiling_sensor_model.dart'; import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presence_display_data.dart'; import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presence_space_type.dart'; @@ -28,8 +29,9 @@ class CeilingSensorControlsView extends StatelessWidget final isLarge = isLargeScreenSize(context); final isMedium = isMediumScreenSize(context); return BlocProvider( - create: (context) => CeilingSensorBloc(deviceId: device.uuid ?? '') - ..add(CeilingInitialEvent(device.uuid ?? '')), + create: (context) => CeilingSensorBlocFactory.create( + deviceId: device.uuid ?? '', + )..add(CeilingInitialEvent(device.uuid ?? '')), child: BlocBuilder( builder: (context, state) { if (state is CeilingLoadingInitialState || From 77d39bfc53e8fed0a3bdac2c98ecd58fd1c95c40 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 2 Jun 2025 11:26:30 +0300 Subject: [PATCH 35/58] Refactor `CurtainBloc` to use new service dependencies and implement a factory for instantiation. Updated event handling methods for improved error management and state updates. --- .../curtain/bloc/curtain_bloc.dart | 162 +++++++----------- .../factories/curtain_bloc_factory.dart | 18 ++ .../view/curtain_batch_status_view.dart | 3 +- .../curtain/view/curtain_status_view.dart | 3 +- 4 files changed, 86 insertions(+), 100 deletions(-) create mode 100644 lib/pages/device_managment/curtain/factories/curtain_bloc_factory.dart diff --git a/lib/pages/device_managment/curtain/bloc/curtain_bloc.dart b/lib/pages/device_managment/curtain/bloc/curtain_bloc.dart index 251d999f..749a7729 100644 --- a/lib/pages/device_managment/curtain/bloc/curtain_bloc.dart +++ b/lib/pages/device_managment/curtain/bloc/curtain_bloc.dart @@ -1,17 +1,25 @@ import 'dart:async'; + import 'package:firebase_database/firebase_database.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/bloc/curtain_event.dart'; import 'package:syncrow_web/pages/device_managment/curtain/bloc/curtain_state.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'; class CurtainBloc extends Bloc { late bool deviceStatus; final String deviceId; - Timer? _timer; + final ControlDeviceService controlDeviceService; + final BatchControlDevicesService batchControlDevicesService; - CurtainBloc({required this.deviceId}) : super(CurtainInitial()) { + CurtainBloc({ + required this.deviceId, + required this.controlDeviceService, + required this.batchControlDevicesService, + }) : super(CurtainInitial()) { on(_onFetchDeviceStatus); on(_onFetchBatchStatus); on(_onCurtainControl); @@ -20,32 +28,31 @@ class CurtainBloc extends Bloc { on(_onStatusUpdated); } - FutureOr _onFetchDeviceStatus( - CurtainFetchDeviceStatus event, Emitter emit) async { + Future _onFetchDeviceStatus( + CurtainFetchDeviceStatus event, + Emitter emit, + ) async { emit(CurtainStatusLoading()); try { - final status = - await DevicesManagementApi().getDeviceStatus(event.deviceId); - _listenToChanges(event.deviceId); + final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); + _listenToChanges(event.deviceId, emit); deviceStatus = _checkStatus(status.status[0].value); - emit(CurtainStatusLoaded(deviceStatus)); } catch (e) { emit(CurtainError(e.toString())); } } - void _listenToChanges(String deviceId) { + void _listenToChanges(String deviceId, Emitter emit) { try { - DatabaseReference ref = - FirebaseDatabase.instance.ref('device-status/$deviceId'); - Stream stream = ref.onValue; + final ref = FirebaseDatabase.instance.ref('device-status/$deviceId'); + final stream = ref.onValue; stream.listen((DatabaseEvent event) { final data = event.snapshot.value as Map?; if (data == null) return; - List statusList = []; + final statusList = []; if (data['status'] != null) { for (var element in data['status']) { statusList.add( @@ -57,7 +64,7 @@ class CurtainBloc extends Bloc { } } if (statusList.isNotEmpty) { - bool newStatus = _checkStatus(statusList[0].value); + final newStatus = _checkStatus(statusList[0].value); if (newStatus != deviceStatus) { deviceStatus = newStatus; if (!isClosed) { @@ -71,76 +78,32 @@ class CurtainBloc extends Bloc { } } - void _onStatusUpdated(StatusUpdated event, Emitter emit) { + void _onStatusUpdated( + StatusUpdated event, + Emitter emit, + ) { emit(CurtainStatusLoading()); deviceStatus = event.deviceStatus; emit(CurtainStatusLoaded(deviceStatus)); } - FutureOr _onCurtainControl( - CurtainControl event, Emitter emit) async { - final oldValue = deviceStatus; - + Future _onCurtainControl( + CurtainControl event, + Emitter emit, + ) async { + emit(CurtainStatusLoading()); _updateLocalValue(event.value, emit); - emit(CurtainStatusLoaded(deviceStatus)); - - await _runDebounce( - deviceId: event.deviceId, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - isBatch: false, - ); - } - - Future _runDebounce({ - required dynamic deviceId, - required String code, - required bool value, - required bool oldValue, - required Emitter emit, - required bool isBatch, - }) async { - late String id; - - if (deviceId is List) { - id = deviceId.first; - } else { - id = deviceId; + try { + final controlValue = event.value ? 'open' : 'close'; + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status(code: event.code, value: controlValue), + ); + } catch (e) { + _updateLocalValue(!event.value, emit); + emit(CurtainControlError(e.toString())); } - - if (_timer != null) { - _timer!.cancel(); - } - _timer = Timer(const Duration(seconds: 1), () async { - try { - final controlValue = value ? 'open' : 'close'; - - late bool response; - if (isBatch) { - response = await DevicesManagementApi() - .deviceBatchControl(deviceId, code, controlValue); - } else { - response = await DevicesManagementApi() - .deviceControl(deviceId, Status(code: code, value: controlValue)); - } - - if (!response) { - _revertValueAndEmit(id, oldValue, emit); - } - } catch (e) { - _revertValueAndEmit(id, oldValue, emit); - } - }); - } - - void _revertValueAndEmit( - String deviceId, bool oldValue, Emitter emit) { - _updateLocalValue(oldValue, emit); - emit(CurtainStatusLoaded(deviceStatus)); - emit(const CurtainControlError('Failed to control the device.')); } void _updateLocalValue(bool value, Emitter emit) { @@ -152,41 +115,44 @@ class CurtainBloc extends Bloc { return command.toLowerCase() == 'open'; } - FutureOr _onFetchBatchStatus( - CurtainFetchBatchStatus event, Emitter emit) async { + Future _onFetchBatchStatus( + CurtainFetchBatchStatus event, + Emitter emit, + ) async { emit(CurtainStatusLoading()); try { - final status = - await DevicesManagementApi().getBatchStatus(event.devicesIds); - + final status = await DevicesManagementApi().getBatchStatus(event.devicesIds); deviceStatus = _checkStatus(status.status[0].value); - emit(CurtainStatusLoaded(deviceStatus)); } catch (e) { emit(CurtainError(e.toString())); } } - FutureOr _onCurtainBatchControl( - CurtainBatchControl event, Emitter emit) async { - final oldValue = deviceStatus; - + Future _onCurtainBatchControl( + CurtainBatchControl event, + Emitter emit, + ) async { + emit(CurtainStatusLoading()); _updateLocalValue(event.value, emit); - emit(CurtainStatusLoaded(deviceStatus)); - - await _runDebounce( - deviceId: event.devicesIds, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - isBatch: true, - ); + try { + final controlValue = event.value ? 'open' : 'stop'; + await batchControlDevicesService.batchControlDevices( + uuids: event.devicesIds, + code: event.code, + value: controlValue, + ); + } catch (e) { + _updateLocalValue(!event.value, emit); + emit(CurtainControlError(e.toString())); + } } - FutureOr _onFactoryReset( - CurtainFactoryReset event, Emitter emit) async { + Future _onFactoryReset( + CurtainFactoryReset event, + Emitter emit, + ) async { emit(CurtainStatusLoading()); try { final response = await DevicesManagementApi().factoryReset( diff --git a/lib/pages/device_managment/curtain/factories/curtain_bloc_factory.dart b/lib/pages/device_managment/curtain/factories/curtain_bloc_factory.dart new file mode 100644 index 00000000..f6257b0a --- /dev/null +++ b/lib/pages/device_managment/curtain/factories/curtain_bloc_factory.dart @@ -0,0 +1,18 @@ +import 'package:syncrow_web/pages/device_managment/curtain/bloc/curtain_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/factories/device_bloc_dependencies_factory.dart'; + +abstract final class CurtainBlocFactory { + const CurtainBlocFactory._(); + + static CurtainBloc create({ + required String deviceId, + }) { + return CurtainBloc( + deviceId: deviceId, + controlDeviceService: + DeviceBlocDependenciesFactory.createControlDeviceService(), + batchControlDevicesService: + DeviceBlocDependenciesFactory.createBatchControlDevicesService(), + ); + } +} diff --git a/lib/pages/device_managment/curtain/view/curtain_batch_status_view.dart b/lib/pages/device_managment/curtain/view/curtain_batch_status_view.dart index 7c873e20..41dcaf9e 100644 --- a/lib/pages/device_managment/curtain/view/curtain_batch_status_view.dart +++ b/lib/pages/device_managment/curtain/view/curtain_batch_status_view.dart @@ -5,6 +5,7 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_re import 'package:syncrow_web/pages/device_managment/curtain/bloc/curtain_bloc.dart'; import 'package:syncrow_web/pages/device_managment/curtain/bloc/curtain_event.dart'; import 'package:syncrow_web/pages/device_managment/curtain/bloc/curtain_state.dart'; +import 'package:syncrow_web/pages/device_managment/curtain/factories/curtain_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; // import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; @@ -18,7 +19,7 @@ class CurtainBatchStatusView extends StatelessWidget with HelperResponsiveLayout Widget build(BuildContext context) { return BlocProvider( create: (context) => - CurtainBloc(deviceId: devicesIds.first)..add(CurtainFetchBatchStatus(devicesIds)), + CurtainBlocFactory.create(deviceId: devicesIds.first)..add(CurtainFetchBatchStatus(devicesIds)), child: BlocBuilder( builder: (context, state) { if (state is CurtainStatusLoading) { diff --git a/lib/pages/device_managment/curtain/view/curtain_status_view.dart b/lib/pages/device_managment/curtain/view/curtain_status_view.dart index 2afe49f4..84b0a943 100644 --- a/lib/pages/device_managment/curtain/view/curtain_status_view.dart +++ b/lib/pages/device_managment/curtain/view/curtain_status_view.dart @@ -4,6 +4,7 @@ import 'package:syncrow_web/pages/common/curtain_toggle.dart'; import 'package:syncrow_web/pages/device_managment/curtain/bloc/curtain_bloc.dart'; import 'package:syncrow_web/pages/device_managment/curtain/bloc/curtain_event.dart'; import 'package:syncrow_web/pages/device_managment/curtain/bloc/curtain_state.dart'; +import 'package:syncrow_web/pages/device_managment/curtain/factories/curtain_bloc_factory.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; class CurtainStatusControlsView extends StatelessWidget @@ -15,7 +16,7 @@ class CurtainStatusControlsView extends StatelessWidget @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => CurtainBloc(deviceId: deviceId) + create: (context) => CurtainBlocFactory.create(deviceId: deviceId) ..add(CurtainFetchDeviceStatus(deviceId)), child: BlocBuilder( builder: (context, state) { From cf5e05a8885a947c7678284d758d6e5e7715b1fb Mon Sep 17 00:00:00 2001 From: mohammad Date: Mon, 2 Jun 2025 12:52:48 +0300 Subject: [PATCH 36/58] Refactor code by adding new API endpoint for assigning a device to a room and removing redundant code in device management settings. --- .../bloc/setting_bloc_bloc.dart | 64 ++- .../bloc/setting_bloc_event.dart | 23 +- .../bloc/setting_bloc_state.dart | 18 +- .../device_icon_type_helper.dart | 28 ++ .../device_setting/device_settings_panel.dart | 438 +++++++++++------- .../device_info_model.dart | 59 +-- .../sub_space_model.dart | 6 +- .../device_setting/sub_space_dialog.dart | 178 +++++++ lib/services/devices_mang_api.dart | 2 + lib/services/space_mana_api.dart | 31 +- lib/utils/constants/api_const.dart | 3 + 11 files changed, 620 insertions(+), 230 deletions(-) create mode 100644 lib/pages/device_managment/device_setting/device_icon_type_helper.dart rename lib/pages/device_managment/device_setting/{bloc => settings_model}/device_info_model.dart (69%) rename lib/pages/device_managment/device_setting/{bloc => settings_model}/sub_space_model.dart (81%) create mode 100644 lib/pages/device_managment/device_setting/sub_space_dialog.dart diff --git a/lib/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart b/lib/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart index 55e5e74e..b9aae0b8 100644 --- a/lib/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart +++ b/lib/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart @@ -1,10 +1,12 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; -import 'package:syncrow_web/pages/device_managment/device_setting/bloc/device_info_model.dart'; +import 'package:syncrow_web/pages/common/bloc/project_manager.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/device_info_model.dart'; import 'package:syncrow_web/pages/device_managment/device_setting/bloc/setting_bloc_state.dart'; -import 'package:syncrow_web/pages/device_managment/device_setting/bloc/sub_space_model.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/sub_space_model.dart'; import 'package:syncrow_web/services/devices_mang_api.dart'; +import 'package:syncrow_web/services/space_mana_api.dart'; import 'package:syncrow_web/utils/snack_bar.dart'; part 'setting_bloc_event.dart'; @@ -17,7 +19,8 @@ class SettingBlocBloc extends Bloc { on(saveName); on(_changeName); on(deleteDevice); - //on(_fetchRoomsAndDevices); + on(_fetchRooms); + on(_assignDevice); } static String deviceName = ''; final TextEditingController nameController = @@ -51,7 +54,7 @@ class SettingBlocBloc extends Bloc { if (_validateInputs()) return; try { emit(SettingLoadingState()); - var response = await DevicesManagementApi.putDeviceName( + await DevicesManagementApi.putDeviceName( deviceId: deviceId, deviceName: nameController.text); add(DeviceSettingInitialInfo()); CustomSnackBar.displaySnackBar('Save Successfully'); @@ -107,6 +110,7 @@ class SettingBlocBloc extends Bloc { emit(UpdateSettingState( deviceName: nameController.text, deviceInfo: deviceInfo, + roomsList: roomsList, )); } catch (e) { emit(ErrorState(message: e.toString())); @@ -127,19 +131,65 @@ class SettingBlocBloc extends Bloc { add(const SaveNameEvent()); focusNode.unfocus(); } - emit(UpdateSettingState(deviceName: deviceName, deviceInfo: deviceInfo)); + emit(UpdateSettingState( + deviceName: deviceName, + deviceInfo: deviceInfo, + roomsList: roomsList, + )); } void deleteDevice( DeleteDeviceEvent event, Emitter emit) async { try { emit(SettingLoadingState()); - var response = - await DevicesManagementApi.resetDevise(devicesUuid: deviceId); + await DevicesManagementApi.resetDevise(devicesUuid: deviceId); CustomSnackBar.displaySnackBar('Reset Successfully'); emit(UpdateSettingState( deviceName: nameController.text, deviceInfo: deviceInfo, + roomsList: roomsList, + )); + } catch (e) { + emit(ErrorState(message: e.toString())); + return; + } + } + + //=========================== assign device to room ========================== + + void _assignDevice( + AssignRoomEvent event, Emitter emit) async { + try { + emit(SettingLoadingState()); + final projectUuid = await ProjectManager.getProjectUUID() ?? ''; + await CommunitySpaceManagementApi.assignDeviceToRoom( + communityId: event.communityUuid, + spaceId: event.spaceUuid, + subSpaceId: event.subSpaceUuid, + deviceId: deviceId, + projectId: projectUuid); + add(DeviceSettingInitialInfo()); + CustomSnackBar.displaySnackBar('Save Successfully'); + emit(SaveSelectionSuccessState()); + } catch (e) { + emit(ErrorState(message: e.toString())); + return; + } + } + + void _fetchRooms( + FetchRoomsEvent event, Emitter emit) async { + try { + emit(SettingLoadingState()); + final projectUuid = await ProjectManager.getProjectUUID() ?? ''; + roomsList = await CommunitySpaceManagementApi.getSubSpaceBySpaceId( + communityId: event.communityUuid, + spaceId: event.spaceUuid, + projectId: projectUuid); + emit(UpdateSettingState( + deviceName: nameController.text, + deviceInfo: deviceInfo, + roomsList: roomsList, )); } catch (e) { emit(ErrorState(message: e.toString())); diff --git a/lib/pages/device_managment/device_setting/bloc/setting_bloc_event.dart b/lib/pages/device_managment/device_setting/bloc/setting_bloc_event.dart index 737c8889..66d9e09f 100644 --- a/lib/pages/device_managment/device_setting/bloc/setting_bloc_event.dart +++ b/lib/pages/device_managment/device_setting/bloc/setting_bloc_event.dart @@ -29,12 +29,13 @@ class ChangeEditingNameValue extends SettingBlocEvent { } class FetchRoomsEvent extends SettingBlocEvent { - final String deviceId; + final String communityUuid; + final String spaceUuid; - const FetchRoomsEvent(this.deviceId); + const FetchRoomsEvent({required this.communityUuid, required this.spaceUuid}); @override - List get props => [deviceId]; + List get props => [communityUuid, spaceUuid]; } class SaveNameEvent extends SettingBlocEvent { @@ -47,4 +48,20 @@ class ChangeNameEvent extends SettingBlocEvent { final bool? value; const ChangeNameEvent({this.value}); } + class DeleteDeviceEvent extends SettingBlocEvent {} + +class AssignRoomEvent extends SettingBlocEvent { + final String communityUuid; + final String spaceUuid; + final String subSpaceUuid; + + const AssignRoomEvent({ + required this.communityUuid, + required this.spaceUuid, + required this.subSpaceUuid, + }); + + @override + List get props => [spaceUuid, communityUuid, subSpaceUuid]; +} diff --git a/lib/pages/device_managment/device_setting/bloc/setting_bloc_state.dart b/lib/pages/device_managment/device_setting/bloc/setting_bloc_state.dart index 65907c67..eb30b70a 100644 --- a/lib/pages/device_managment/device_setting/bloc/setting_bloc_state.dart +++ b/lib/pages/device_managment/device_setting/bloc/setting_bloc_state.dart @@ -1,6 +1,6 @@ import 'package:equatable/equatable.dart'; -import 'package:syncrow_web/pages/device_managment/device_setting/bloc/device_info_model.dart'; -import 'package:syncrow_web/pages/device_managment/device_setting/bloc/sub_space_model.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/device_info_model.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/sub_space_model.dart'; abstract class DeviceSettingsState extends Equatable { const DeviceSettingsState(); @@ -43,12 +43,16 @@ class SettingBlocInitial extends DeviceSettingsState { class SettingLoadingState extends DeviceSettingsState {} class UpdateSettingState extends DeviceSettingsState { - final String deviceName; + final String? deviceName; final DeviceInfoModel? deviceInfo; - const UpdateSettingState({required this.deviceName, this.deviceInfo}); + final List roomsList; - @override - List get props => [deviceName, deviceInfo]; + const UpdateSettingState({ + this.deviceName, + this.deviceInfo, + this.roomsList = const [], + }); + List get props => [deviceName, deviceInfo, roomsList]; } class ErrorState extends DeviceSettingsState { @@ -67,3 +71,5 @@ class FetchRoomsState extends DeviceSettingsState { @override List get props => [roomsList]; } + +class SaveSelectionSuccessState extends DeviceSettingsState {} diff --git a/lib/pages/device_managment/device_setting/device_icon_type_helper.dart b/lib/pages/device_managment/device_setting/device_icon_type_helper.dart new file mode 100644 index 00000000..13f8abfe --- /dev/null +++ b/lib/pages/device_managment/device_setting/device_icon_type_helper.dart @@ -0,0 +1,28 @@ +import 'package:syncrow_web/utils/constants/assets.dart'; + +class DeviceIconTypeHelper { + static const Map _iconMap = { + 'AC': Assets.ac, + 'GW': Assets.gateway, + 'CPS': Assets.sensors, + 'DL': Assets.doorLock, + 'WPS': Assets.sensors, + '3G': Assets.gangSwitch, + '2G': Assets.twoGang, + '1G': Assets.oneGang, + 'CUR': Assets.curtain, + 'WH': Assets.waterHeater, + 'DS': Assets.doorSensor, + '1GT': Assets.oneTouchSwitch, + '2GT': Assets.twoTouchSwitch, + '3GT': Assets.threeTouchSwitch, + 'GD': Assets.garageDoor, + 'WL': Assets.waterLeakNormal, + 'NCPS': Assets.sensors, + }; + + static String getDeviceIconByTypeCode(String? typeCode) { + if (typeCode == null) return Assets.logoHorizontal; + return _iconMap[typeCode] ?? Assets.logoHorizontal; + } +} diff --git a/lib/pages/device_managment/device_setting/device_settings_panel.dart b/lib/pages/device_managment/device_setting/device_settings_panel.dart index 2415ab90..6d960a20 100644 --- a/lib/pages/device_managment/device_setting/device_settings_panel.dart +++ b/lib/pages/device_managment/device_setting/device_settings_panel.dart @@ -3,9 +3,12 @@ import 'package:flutter/services.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/device_setting/bloc/device_info_model.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/device_icon_type_helper.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/device_info_model.dart'; import 'package:syncrow_web/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart'; import 'package:syncrow_web/pages/device_managment/device_setting/bloc/setting_bloc_state.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/sub_space_dialog.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/sub_space_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'; @@ -14,9 +17,7 @@ import 'package:syncrow_web/web_layout/default_container.dart'; class DeviceSettingsPanel extends StatelessWidget { final VoidCallback? onClose; final AllDevicesModel device; - - const DeviceSettingsPanel({this.onClose, super.key, required this.device}); - + const DeviceSettingsPanel({super.key, this.onClose, required this.device}); @override Widget build(BuildContext context) { final sectionTitle = context.theme.textTheme.titleMedium!.copyWith( @@ -24,7 +25,10 @@ class DeviceSettingsPanel extends StatelessWidget { color: ColorsManager.grayColor, ); Widget infoRow( - {required String label, required String value, Widget? trailing}) { + {required String label, + required String value, + Widget? trailing, + required Color? valueColor}) { return Padding( padding: const EdgeInsets.symmetric(vertical: 6.0), child: Row( @@ -41,10 +45,8 @@ class DeviceSettingsPanel extends StatelessWidget { child: Text( value, textAlign: TextAlign.end, - style: context.theme.textTheme.bodyMedium!.copyWith( - fontSize: 14, - color: ColorsManager.blackColor, - ), + style: context.theme.textTheme.bodyMedium! + .copyWith(fontSize: 14, color: valueColor), overflow: TextOverflow.ellipsis, ), ), @@ -56,87 +58,114 @@ class DeviceSettingsPanel extends StatelessWidget { } return BlocProvider( - create: (context) => SettingBlocBloc( - deviceId: device.uuid ?? '', - )..add(DeviceSettingInitialInfo()), - child: BlocBuilder( - builder: (context, state) { + create: (context) => SettingBlocBloc( + deviceId: device.uuid ?? '', + ) + ..add(DeviceSettingInitialInfo()) + ..add(FetchRoomsEvent( + communityUuid: device.community!.uuid!, + spaceUuid: device.spaces!.first.uuid!, + )), + child: BlocBuilder( + builder: (context, state) { final iconPath = - DeviceTypeHelper.getDeviceIconByTypeCode(device.productType); + DeviceIconTypeHelper.getDeviceIconByTypeCode(device.productType); final _bloc = BlocProvider.of(context); DeviceInfoModel deviceInfo = DeviceInfoModel.empty(); + List subSpaces = []; if (state is UpdateSettingState) { deviceInfo = state.deviceInfo!; + subSpaces = state.roomsList; } - return Container( - width: MediaQuery.of(context).size.width * 0.3, - color: ColorsManager.grey25, - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24), - child: ListView( - children: [ - /// Header - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + return Stack( + children: [ + Container( + width: MediaQuery.of(context).size.width * 0.3, + color: ColorsManager.grey25, + padding: + const EdgeInsets.symmetric(horizontal: 20, vertical: 24), + child: ListView( children: [ - IconButton( - icon: SvgPicture.asset(Assets.closeSettingsIcon), - onPressed: onClose ?? () => Navigator.of(context).pop(), + // Header + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + icon: SvgPicture.asset(Assets.closeSettingsIcon), + onPressed: + onClose ?? () => Navigator.of(context).pop(), + ), + ], ), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text('Device Settings', - style: context.theme.textTheme.titleLarge!.copyWith( + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Device Settings', + style: context.theme.textTheme.titleLarge!.copyWith( fontWeight: FontWeight.bold, - color: ColorsManager.primaryColor)), - ], - ), - const SizedBox(height: 24), - - /// Device Name + Icon - DefaultContainer( - child: Row( - children: [ - CircleAvatar( - radius: 40, - backgroundColor: - const Color.fromARGB(177, 213, 213, 213), - child: CircleAvatar( - backgroundColor: ColorsManager.whiteColors, - radius: 36, - child: SvgPicture.asset( - iconPath, - fit: BoxFit.cover, + color: ColorsManager.primaryColor, ), ), - ), - const SizedBox(width: 12), - Expanded( - child: TextFormField( - maxLength: 30, - style: const TextStyle( - color: ColorsManager.blackColor, + ], + ), + const SizedBox(height: 24), + // Device Name + Icon + DefaultContainer( + child: Row( + children: [ + CircleAvatar( + radius: 40, + backgroundColor: + const Color.fromARGB(177, 213, 213, 213), + child: CircleAvatar( + backgroundColor: ColorsManager.whiteColors, + radius: 36, + child: SvgPicture.asset( + iconPath, + fit: BoxFit.cover, + ), + ), ), - textAlign: TextAlign.center, - focusNode: _bloc.focusNode, - controller: _bloc.nameController, - enabled: _bloc.editName, - onFieldSubmitted: (value) { - _bloc.add(const ChangeNameEvent(value: false)); - }, - decoration: const InputDecoration( - border: InputBorder.none, - fillColor: Colors.white10, - counterText: '', + const SizedBox(width: 12), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Device Name:', + style: context.textTheme.bodyMedium!.copyWith( + color: ColorsManager.grayColor, + ), + ), + TextFormField( + maxLength: 30, + style: const TextStyle( + color: ColorsManager.blackColor, + ), + textAlign: TextAlign.start, + focusNode: _bloc.focusNode, + controller: _bloc.nameController, + enabled: _bloc.editName, + onFieldSubmitted: (value) { + _bloc.add( + const ChangeNameEvent(value: false)); + }, + decoration: const InputDecoration( + border: InputBorder.none, + fillColor: Colors.white10, + counterText: '', + ), + ), + ], + ), ), - ), - ), - const SizedBox(width: 8), - _bloc.editName == true - ? const SizedBox() - : GestureDetector( + const SizedBox(width: 8), + Visibility( + visible: _bloc.editName != true, + replacement: const SizedBox(), + child: GestureDetector( onTap: () { _bloc.add(const ChangeNameEvent(value: true)); }, @@ -147,121 +176,170 @@ class DeviceSettingsPanel extends StatelessWidget { width: 20, ), ), - ], - ), - ), - const SizedBox(height: 32), - - /// Device Management - Text('Device Management', style: sectionTitle), - DefaultContainer( - padding: EdgeInsets.zero, - child: Column( - children: [ - const SizedBox( - height: 5, + ) + ], ), - Padding( - padding: const EdgeInsets.all(10.0), - child: infoRow( - label: 'Sub-Space:', - value: device.subspace!.subspaceName, - trailing: const Icon( - Icons.arrow_forward_ios, - size: 16, - color: ColorsManager.greyColor, - ), - ), - ), - const Divider(color: ColorsManager.dividerColor), - Padding( - padding: const EdgeInsets.all(10.0), - child: infoRow( - label: 'Virtual Address:', - value: deviceInfo.productUuid, - trailing: InkWell( - onTap: () { - Clipboard.setData( - ClipboardData(text: device.productUuid ?? ''), - ); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - 'Virtual Address copied to clipboard'), + ), + const SizedBox(height: 32), + // Device Management + Text('Device Management', style: sectionTitle), + DefaultContainer( + padding: EdgeInsets.zero, + child: Column( + children: [ + const SizedBox(height: 5), + Padding( + padding: const EdgeInsets.all(10.0), + child: InkWell( + onTap: () { + showSubSpaceDialog( + context, + communityUuid: device.community!.uuid!, + spaceUuid: device.spaces!.first.uuid!, + subSpaces: subSpaces, + selected: device.subspace!.uuid, + ); + }, + child: infoRow( + label: 'Sub-Space:', + value: deviceInfo.subspace.subspaceName, + valueColor: ColorsManager.textGray, + trailing: const Icon( + Icons.arrow_forward_ios, + size: 16, + color: ColorsManager.greyColor, ), + ), + ), + ), + const Divider(color: ColorsManager.dividerColor), + Padding( + padding: const EdgeInsets.all(10.0), + child: infoRow( + label: 'Virtual Address:', + value: deviceInfo.productUuid, + valueColor: ColorsManager.blackColor, + trailing: InkWell( + onTap: () { + Clipboard.setData( + ClipboardData( + text: device.productUuid ?? ''), + ); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Virtual Address copied to clipboard'), + ), + ); + }, + child: const Icon( + Icons.copy, + size: 16, + color: ColorsManager.greyColor, + ), + ), + ), + ), + const Divider(color: ColorsManager.dividerColor), + Padding( + padding: const EdgeInsets.all(10.0), + child: infoRow( + label: 'MAC Address:', + valueColor: ColorsManager.blackColor, + value: deviceInfo.macAddress, + ), + ), + const SizedBox(height: 5), + ], + ), + ), + const SizedBox(height: 32), + + // Remove Device Button + SizedBox( + width: double.infinity, + child: InkWell( + onTap: () { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text( + 'Remove Device', + style: context.textTheme.bodyMedium!.copyWith( + fontWeight: FontWeight.w700, + color: ColorsManager.red, + ), + ), + content: Text( + 'Are you sure you want to remove this device?', + style: context.textTheme.bodyMedium!.copyWith( + color: ColorsManager.grayColor, + ), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text( + 'Cancel', + style: context.textTheme.bodyMedium! + .copyWith( + color: ColorsManager.grayColor, + ), + ), + ), + TextButton( + onPressed: () { + _bloc.add(DeleteDeviceEvent()); + Navigator.of(context).pop(); + }, + child: Text( + 'Remove', + style: context.textTheme.bodyMedium! + .copyWith( + color: ColorsManager.red, + ), + ), + ), + ], ); }, - child: const Icon( - Icons.copy, - size: 16, - color: ColorsManager.greyColor, + ); + }, + child: DefaultContainer( + padding: const EdgeInsets.all(25), + child: Center( + child: Text( + 'Remove Device', + style: context.textTheme.bodyMedium!.copyWith( + fontSize: 14, + color: ColorsManager.red, + fontWeight: FontWeight.w700), ), ), ), ), - const Divider(color: ColorsManager.dividerColor), - Padding( - padding: const EdgeInsets.all(10.0), - child: infoRow( - label: 'MAC Address:', - value: deviceInfo.macAddress), - ), - const SizedBox( - height: 5, - ), - ], - ), + ), + ], ), - const SizedBox(height: 32), - - /// Remove Device Button - SizedBox( - width: double.infinity, - child: InkWell( - onTap: () { - _bloc.add(DeleteDeviceEvent()); - }, - child: const DefaultContainer( - padding: EdgeInsets.all(25), - child: Center( - child: Text( - 'Remove Device', - style: TextStyle(color: ColorsManager.red), - ), + ), + if (state is SettingLoadingState) + Positioned.fill( + child: Container( + color: Colors.black.withOpacity(0.1), + child: const Center( + child: CircularProgressIndicator( + color: ColorsManager.primaryColor, ), ), ), - ) - ], - ), + ), + ], ); - })); - } -} - -class DeviceTypeHelper { - static const Map _iconMap = { - 'AC': Assets.ac, - 'GW': Assets.gateway, - 'CPS': Assets.sensors, - 'DL': Assets.doorLock, - 'WPS': Assets.sensors, - '3G': Assets.gangSwitch, - '2G': Assets.twoGang, - '1G': Assets.oneGang, - 'CUR': Assets.curtain, - 'WH': Assets.waterHeater, - 'DS': Assets.doorSensor, - '1GT': Assets.oneTouchSwitch, - '2GT': Assets.twoTouchSwitch, - '3GT': Assets.threeTouchSwitch, - 'GD': Assets.garageDoor, - 'WL': Assets.waterLeakNormal, - 'NCPS': Assets.sensors, - }; - - static String getDeviceIconByTypeCode(String? typeCode) { - if (typeCode == null) return Assets.logoHorizontal; - return _iconMap[typeCode] ?? Assets.logoHorizontal; + }, + ), + ); } } diff --git a/lib/pages/device_managment/device_setting/bloc/device_info_model.dart b/lib/pages/device_managment/device_setting/settings_model/device_info_model.dart similarity index 69% rename from lib/pages/device_managment/device_setting/bloc/device_info_model.dart rename to lib/pages/device_managment/device_setting/settings_model/device_info_model.dart index 65a48508..ce9b6750 100644 --- a/lib/pages/device_managment/device_setting/bloc/device_info_model.dart +++ b/lib/pages/device_managment/device_setting/settings_model/device_info_model.dart @@ -55,31 +55,32 @@ class DeviceInfoModel { factory DeviceInfoModel.fromJson(Map json) { return DeviceInfoModel( - activeTime: json['activeTime'], - category: json['category'], - categoryName: json['categoryName'], - createTime: json['createTime'], - gatewayId: json['gatewayId'], - icon: json['icon'], - ip: json['ip'] ?? "", - lat: json['lat'], - localKey: json['localKey'], - lon: json['lon'], - model: json['model'], - name: json['name'], - nodeId: json['nodeId'], - online: json['online'], - ownerId: json['ownerId'], - productName: json['productName'], - sub: json['sub'], - timeZone: json['timeZone'], - updateTime: json['updateTime'], - uuid: json['uuid'], - productUuid: json['productUuid'], - productType: json['productType'], - permissionType: json['permissionType'] ?? '', - macAddress: json['macAddress'], - subspace: Subspace.fromJson(json['subspace']), + activeTime: json['activeTime'] as int? ?? 0, + category: json['category'] ?? '', + categoryName: json['categoryName'] as String? ?? '', + createTime: json['createTime'] as int? ?? 0, + gatewayId: json['gatewayId'] as String? ?? '', + icon: json['icon'] as String? ?? '', + ip: json['ip'] as String? ?? '', + lat: json['lat'] as String? ?? '', + localKey: json['localKey'] as String? ?? '', + lon: json['lon'] as String? ?? '', + model: json['model'] as String? ?? '', + name: json['name'] as String? ?? '', + nodeId: json['nodeId'] as String? ?? '', + online: json['online'] as bool? ?? false, + ownerId: json['ownerId'] as String? ?? '', + productName: json['productName'] as String? ?? '', + sub: json['sub'] as bool? ?? false, + timeZone: json['timeZone'] as String? ?? '', + updateTime: json['updateTime'] as int? ?? 0, + uuid: json['uuid'] as String? ?? '', + productUuid: json['productUuid'] as String? ?? '', + productType: json['productType'] as String? ?? '', + permissionType: json['permissionType'] as String? ?? '', + macAddress: json['macAddress'] as String? ?? '', + subspace: + Subspace.fromJson(json['subspace'] as Map? ?? {}), ); } @@ -164,10 +165,10 @@ class Subspace { factory Subspace.fromJson(Map json) { return Subspace( - uuid: json['uuid'], - createdAt: json['createdAt'], - updatedAt: json['updatedAt'], - subspaceName: json['subspaceName'], + uuid: json['uuid'] as String? ?? '', + createdAt: json['createdAt'] as String? ?? '', + updatedAt: json['updatedAt'] as String? ?? '', + subspaceName: json['subspaceName'] as String? ?? '', ); } diff --git a/lib/pages/device_managment/device_setting/bloc/sub_space_model.dart b/lib/pages/device_managment/device_setting/settings_model/sub_space_model.dart similarity index 81% rename from lib/pages/device_managment/device_setting/bloc/sub_space_model.dart rename to lib/pages/device_managment/device_setting/settings_model/sub_space_model.dart index bc68b33e..9d3f4036 100644 --- a/lib/pages/device_managment/device_setting/bloc/sub_space_model.dart +++ b/lib/pages/device_managment/device_setting/settings_model/sub_space_model.dart @@ -27,9 +27,9 @@ class SubSpaceModel { } } return SubSpaceModel( - id: json['uuid'], - name: json['subspaceName'], - devices: devices, + id: json['uuid'] as String? ?? '', + name: json['subspaceName'] as String? ?? '', + devices: devices.isNotEmpty ? devices : null as List?, ); } } diff --git a/lib/pages/device_managment/device_setting/sub_space_dialog.dart b/lib/pages/device_managment/device_setting/sub_space_dialog.dart new file mode 100644 index 00000000..f2fdfa3e --- /dev/null +++ b/lib/pages/device_managment/device_setting/sub_space_dialog.dart @@ -0,0 +1,178 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/sub_space_model.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class SubSpaceDialog extends StatefulWidget { + final List subSpaces; + final String? selected; + final void Function(SubSpaceModel?) onConfirmed; + + const SubSpaceDialog({ + Key? key, + required this.subSpaces, + this.selected, + required this.onConfirmed, + }) : super(key: key); + + @override + State createState() => _SubSpaceDialogState(); +} + +class _SubSpaceDialogState extends State { + String? _selectedId; + + @override + void initState() { + super.initState(); + _selectedId = widget.selected; + } + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: ColorsManager.whiteColors, + insetPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 60), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(28), + ), + child: Container( + width: MediaQuery.of(context).size.width * 0.35, + padding: const EdgeInsets.fromLTRB(0, 24, 0, 0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Sub-Space', + style: context.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w700, + color: ColorsManager.blueColor, + fontSize: 20), + ), + const Divider(), + const SizedBox(height: 10), + ...widget.subSpaces.map((space) { + return RadioListTile( + value: space.id!, + groupValue: _selectedId, + onChanged: (value) { + setState(() { + _selectedId = value; + }); + }, + activeColor: Color(0xFF2962FF), + title: Text( + space.name ?? 'Unnamed Sub-Space', + style: context.textTheme.bodyMedium?.copyWith( + fontSize: 15, + color: ColorsManager.grayColor, + fontWeight: FontWeight.w400, + ), + ), + controlAffinity: ListTileControlAffinity.trailing, + contentPadding: const EdgeInsets.symmetric(horizontal: 24), + ); + }).toList(), + const SizedBox(height: 12), + const Divider(height: 1, thickness: 1), + SizedBox( + height: 50, + child: Row( + children: [ + Expanded( + child: Container( + decoration: const BoxDecoration( + border: Border( + right: BorderSide( + color: ColorsManager.dividerColor, + width: 0.5, + ), + ), + ), + child: TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text( + 'Cancel', + style: context.textTheme.bodyMedium?.copyWith( + color: ColorsManager.textGray, + fontSize: 18, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ), + Expanded( + child: Container( + decoration: const BoxDecoration( + border: Border( + left: BorderSide( + color: ColorsManager.dividerColor, + width: 0.5, + ), + ), + ), + child: TextButton( + onPressed: _selectedId == null + ? null + : () { + final selectedModel = widget.subSpaces + .firstWhere( + (space) => space.id == _selectedId, + orElse: () => SubSpaceModel( + id: null, name: '', devices: [])); + widget.onConfirmed(selectedModel); + Navigator.of(context).pop(); + }, + child: Text( + 'Confirm', + style: context.textTheme.bodyMedium?.copyWith( + color: ColorsManager.secondaryColor, + fontSize: 14, + fontWeight: FontWeight.w400, + ), + ), + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} + +void showSubSpaceDialog( + BuildContext context, { + required List subSpaces, + String? selected, + required String communityUuid, + required String spaceUuid, +}) { + showDialog( + context: context, + barrierDismissible: true, + builder: (ctx) => SubSpaceDialog( + subSpaces: subSpaces, + selected: selected, + onConfirmed: (selectedModel) { + if (selectedModel != null) { + context.read().add( + AssignRoomEvent( + communityUuid: communityUuid, + spaceUuid: spaceUuid, + subSpaceUuid: selectedModel.id ?? '', + ), + ); + } + }, + ), + ); +} diff --git a/lib/services/devices_mang_api.dart b/lib/services/devices_mang_api.dart index 97ac95d8..4d5200d4 100644 --- a/lib/services/devices_mang_api.dart +++ b/lib/services/devices_mang_api.dart @@ -386,4 +386,6 @@ class DevicesManagementApi { return response; } + + } diff --git a/lib/services/space_mana_api.dart b/lib/services/space_mana_api.dart index 048c7b40..514be163 100644 --- a/lib/services/space_mana_api.dart +++ b/lib/services/space_mana_api.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:syncrow_web/pages/device_managment/device_setting/bloc/sub_space_model.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/sub_space_model.dart'; import 'package:syncrow_web/pages/space_tree/model/pagination_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/create_subspace_model.dart'; @@ -369,7 +369,9 @@ class CommunitySpaceManagementApi { } static Future> getSubSpaceBySpaceId( - String communityId, String spaceId, String projectId) async { + {required String communityId, + required String spaceId, + required String projectId}) async { try { // Construct the API path final path = ApiEndpoints.listSubspace @@ -399,4 +401,29 @@ class CommunitySpaceManagementApi { return []; // Return an empty list if there's an error } } + + static Future> assignDeviceToRoom( + {required String communityId, + required String spaceId, + required String subSpaceId, + required String deviceId, + required String projectId}) async { + try { + final response = await HTTPService().post( + path: ApiEndpoints.assignDeviceToRoom + .replaceAll('{projectUuid}', projectId) + .replaceAll('{communityUuid}', communityId) + .replaceAll('{spaceUuid}', spaceId) + .replaceAll('{subSpaceUuid}', subSpaceId) + .replaceAll('{deviceUuid}', deviceId), + expectedResponseModel: (json) { + print('Assign Device Response: $json'); + return json; + }, + ); + return response; + } catch (e) { + rethrow; + } + } } diff --git a/lib/utils/constants/api_const.dart b/lib/utils/constants/api_const.dart index 472055bd..411e72a5 100644 --- a/lib/utils/constants/api_const.dart +++ b/lib/utils/constants/api_const.dart @@ -133,4 +133,7 @@ abstract class ApiEndpoints { static const String deviceByUuid = '/devices/{deviceUuid}'; static const String resetDevice = '/factory/reset/{deviceUuid}'; + + static const String assignDeviceToRoom = + '/projects/{projectUuid}/communities/{communityUuid}/spaces/{spaceUuid}/subspaces/{subSpaceUuid}/devices/{deviceUuid}'; } From ba08fcf71f00fd4734cd51832ad8a3c69c0a8d66 Mon Sep 17 00:00:00 2001 From: mohammad Date: Mon, 2 Jun 2025 12:58:11 +0300 Subject: [PATCH 37/58] Refactor debug print statements in space management API --- lib/services/space_mana_api.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/services/space_mana_api.dart b/lib/services/space_mana_api.dart index 514be163..31f3cebd 100644 --- a/lib/services/space_mana_api.dart +++ b/lib/services/space_mana_api.dart @@ -390,7 +390,8 @@ class CommunitySpaceManagementApi { rooms.add(SubSpaceModel.fromJson(subspace)); } } else { - print("Warning: 'data' key is missing or null in response JSON."); + debugPrint( + "Warning: 'data' key is missing or null in response JSON."); } return rooms; }, @@ -417,7 +418,6 @@ class CommunitySpaceManagementApi { .replaceAll('{subSpaceUuid}', subSpaceId) .replaceAll('{deviceUuid}', deviceId), expectedResponseModel: (json) { - print('Assign Device Response: $json'); return json; }, ); From cabd37a08a52ae2bf59744b2c5bb0d34507fe65a Mon Sep 17 00:00:00 2001 From: mohammad Date: Mon, 2 Jun 2025 13:30:26 +0300 Subject: [PATCH 38/58] remove un use code --- .../device_setting/bloc/setting_bloc_bloc.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart b/lib/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart index b9aae0b8..e4d6a835 100644 --- a/lib/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart +++ b/lib/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart @@ -61,9 +61,7 @@ class SettingBlocBloc extends Bloc { emit(UpdateSettingState(deviceName: nameController.text)); } catch (e) { emit(ErrorState(message: e.toString())); - } finally { - // isSaving = false; - } + } } DeviceInfoModel deviceInfo = DeviceInfoModel( From 57b6f0117756dea268c69923fedac0b68e906dba Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 2 Jun 2025 14:26:47 +0300 Subject: [PATCH 39/58] SP-1593 Implemented the agreed upon api contract. --- lib/pages/analytics/models/range_of_aqi.dart | 27 +++++++--- .../blocs/range_of_aqi/range_of_aqi_bloc.dart | 50 ++++++++++++++++--- .../range_of_aqi/range_of_aqi_event.dart | 9 ++++ .../range_of_aqi/range_of_aqi_state.dart | 23 ++++++++- .../fetch_air_quality_data_helper.dart | 4 -- .../widgets/aqi_type_dropdown.dart | 15 +++--- .../widgets/range_of_aqi_chart.dart | 48 +++++++++++------- .../widgets/range_of_aqi_chart_box.dart | 17 +++++-- .../widgets/range_of_aqi_chart_title.dart | 22 ++++---- .../params/get_range_of_aqi_param.dart | 6 +-- .../fake_range_of_aqi_service.dart | 14 ++++-- 11 files changed, 170 insertions(+), 65 deletions(-) diff --git a/lib/pages/analytics/models/range_of_aqi.dart b/lib/pages/analytics/models/range_of_aqi.dart index 759666c2..4cee813e 100644 --- a/lib/pages/analytics/models/range_of_aqi.dart +++ b/lib/pages/analytics/models/range_of_aqi.dart @@ -1,18 +1,31 @@ import 'package:equatable/equatable.dart'; class RangeOfAqi extends Equatable { - final double min; - final double avg; - final double max; final DateTime date; + final List data; const RangeOfAqi({ - required this.min, - required this.avg, - required this.max, + required this.data, required this.date, }); @override - List get props => [min, avg, max, date]; + List get props => [data, date]; +} + +class RangeOfAqiValue extends Equatable { + final String type; + final double min; + final double average; + final double max; + + const RangeOfAqiValue({ + required this.type, + required this.min, + required this.average, + required this.max, + }); + + @override + List get props => [type, min, average, max]; } diff --git a/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart b/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart index febbcf58..88c3715e 100644 --- a/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart +++ b/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart @@ -1,6 +1,7 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:syncrow_web/pages/analytics/models/range_of_aqi.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart'; import 'package:syncrow_web/pages/analytics/params/get_range_of_aqi_param.dart'; import 'package:syncrow_web/pages/analytics/services/range_of_aqi/range_of_aqi_service.dart'; @@ -11,6 +12,7 @@ class RangeOfAqiBloc extends Bloc { RangeOfAqiBloc(this._rangeOfAqiService) : super(const RangeOfAqiState()) { on(_onLoadRangeOfAqiEvent); on(_onClearRangeOfAqiEvent); + on(_onUpdateAqiTypeEvent); } final RangeOfAqiService _rangeOfAqiService; @@ -20,19 +22,55 @@ class RangeOfAqiBloc extends Bloc { Emitter emit, ) async { emit( - RangeOfAqiState( - status: RangeOfAqiStatus.loading, - rangeOfAqi: state.rangeOfAqi, - ), + state.copyWith(status: RangeOfAqiStatus.loading), ); try { final rangeOfAqi = await _rangeOfAqiService.load(event.param); - emit(RangeOfAqiState(status: RangeOfAqiStatus.loaded, rangeOfAqi: rangeOfAqi)); + emit( + state.copyWith( + status: RangeOfAqiStatus.loaded, + rangeOfAqi: rangeOfAqi, + filteredRangeOfAqi: _arrangeChartDataByType( + rangeOfAqi, + state.selectedAqiType, + ), + ), + ); } catch (e) { - emit(RangeOfAqiState(status: RangeOfAqiStatus.failure, errorMessage: '$e')); + emit( + state.copyWith( + status: RangeOfAqiStatus.failure, + errorMessage: '$e', + ), + ); } } + void _onUpdateAqiTypeEvent( + UpdateAqiTypeEvent event, + Emitter emit, + ) { + emit( + state.copyWith( + selectedAqiType: event.aqiType, + filteredRangeOfAqi: _arrangeChartDataByType(state.rangeOfAqi, event.aqiType), + ), + ); + } + + List _arrangeChartDataByType( + List rangeOfAqi, + AqiType aqiType, + ) { + final filteredRangeOfAqi = rangeOfAqi.map( + (data) => RangeOfAqi( + date: data.date, + data: data.data.where((value) => value.type == aqiType.code).toList(), + ), + ); + return filteredRangeOfAqi.toList(); + } + void _onClearRangeOfAqiEvent( ClearRangeOfAqiEvent event, Emitter emit, diff --git a/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_event.dart b/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_event.dart index 8a429587..6a08df5b 100644 --- a/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_event.dart +++ b/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_event.dart @@ -16,6 +16,15 @@ class LoadRangeOfAqiEvent extends RangeOfAqiEvent { List get props => [param]; } +class UpdateAqiTypeEvent extends RangeOfAqiEvent { + const UpdateAqiTypeEvent(this.aqiType); + + final AqiType aqiType; + + @override + List get props => [aqiType]; +} + class ClearRangeOfAqiEvent extends RangeOfAqiEvent { const ClearRangeOfAqiEvent(); } diff --git a/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_state.dart b/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_state.dart index 392e98c1..9308020c 100644 --- a/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_state.dart +++ b/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_state.dart @@ -5,14 +5,35 @@ enum RangeOfAqiStatus { initial, loading, loaded, failure } final class RangeOfAqiState extends Equatable { const RangeOfAqiState({ this.rangeOfAqi = const [], + this.filteredRangeOfAqi = const [], this.status = RangeOfAqiStatus.initial, this.errorMessage, + this.selectedAqiType = AqiType.aqi, }); final RangeOfAqiStatus status; final List rangeOfAqi; + final List filteredRangeOfAqi; final String? errorMessage; + final AqiType selectedAqiType; + + RangeOfAqiState copyWith({ + RangeOfAqiStatus? status, + List? rangeOfAqi, + List? filteredRangeOfAqi, + String? errorMessage, + AqiType? selectedAqiType, + }) { + return RangeOfAqiState( + status: status ?? this.status, + rangeOfAqi: rangeOfAqi ?? this.rangeOfAqi, + filteredRangeOfAqi: filteredRangeOfAqi ?? this.filteredRangeOfAqi, + errorMessage: errorMessage ?? this.errorMessage, + selectedAqiType: selectedAqiType ?? this.selectedAqiType, + ); + } @override - List get props => [status, rangeOfAqi, errorMessage]; + List get props => + [status, rangeOfAqi, filteredRangeOfAqi, errorMessage, selectedAqiType]; } 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 55de65d3..1919f518 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 @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_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'; @@ -28,7 +27,6 @@ abstract final class FetchAirQualityDataHelper { context, spaceUuid: spaceUuid, date: date, - aqiType: AqiType.aqi, ); loadAirQualityDistribution( context, @@ -76,14 +74,12 @@ abstract final class FetchAirQualityDataHelper { BuildContext context, { required String spaceUuid, required DateTime date, - required AqiType aqiType, }) { context.read().add( LoadRangeOfAqiEvent( GetRangeOfAqiParam( date: date, spaceUuid: spaceUuid, - aqiType: aqiType, ), ), ); 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 c725d1fa..60a686ff 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 @@ -3,17 +3,18 @@ import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; enum AqiType { - aqi('AQI', ''), - pm25('PM2.5', 'µg/m³'), - pm10('PM10', 'µg/m³'), - hcho('HCHO', 'mg/m³'), - tvoc('TVOC', 'µg/m³'), - co2('CO2', 'ppm'); + aqi('AQI', '', 'aqi'), + pm25('PM2.5', 'µg/m³', 'pm25'), + pm10('PM10', 'µg/m³', 'pm10'), + hcho('HCHO', 'mg/m³', 'hcho'), + tvoc('TVOC', 'µg/m³', 'tvoc'), + co2('CO2', 'ppm', 'co2'); - const AqiType(this.value, this.unit); + const AqiType(this.value, this.unit, this.code); final String value; final String unit; + final String code; } class AqiTypeDropdown extends StatefulWidget { 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 08a036c0..fc63e413 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 @@ -13,23 +13,37 @@ class RangeOfAqiChart extends StatelessWidget { required this.chartData, }); - List<(List values, Color color, Color? dotColor)> get _lines => [ - ( - chartData.map((e) => e.max).toList(), - ColorsManager.maxPurple, - ColorsManager.maxPurpleDot, - ), - ( - chartData.map((e) => e.avg).toList(), - Colors.white, - null, - ), - ( - chartData.map((e) => e.min).toList(), - ColorsManager.minBlue, - ColorsManager.minBlueDot, - ), - ]; + List<(List values, Color color, Color? dotColor)> get _lines { + final sortedData = List.from(chartData) + ..sort((a, b) => a.date.compareTo(b.date)); + + return [ + ( + sortedData.map((e) { + final value = e.data.firstOrNull; + return value?.max ?? 0; + }).toList(), + ColorsManager.maxPurple, + ColorsManager.maxPurpleDot, + ), + ( + sortedData.map((e) { + final value = e.data.firstOrNull; + return value?.average ?? 0; + }).toList(), + Colors.white, + null, + ), + ( + sortedData.map((e) { + final value = e.data.firstOrNull; + return value?.min ?? 0; + }).toList(), + ColorsManager.minBlue, + ColorsManager.minBlueDot, + ), + ]; + } @override Widget build(BuildContext context) { 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 0fe4c4bd..fefb7a9c 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 @@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_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/range_of_aqi_chart.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart'; +import 'package:syncrow_web/pages/analytics/params/get_range_of_aqi_param.dart'; import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart'; import 'package:syncrow_web/utils/style.dart'; @@ -26,13 +27,23 @@ class RangeOfAqiChartBox extends StatelessWidget { AnalyticsErrorWidget(state.errorMessage), const SizedBox(height: 10), ], - RangeOfAqiChartTitle( - isLoading: state.status == RangeOfAqiStatus.loading, + GestureDetector( + onTap: () { + context.read().add(LoadRangeOfAqiEvent( + GetRangeOfAqiParam( + spaceUuid: '123', + date: DateTime.now().subtract(const Duration(days: 30)), + ), + )); + }, + child: RangeOfAqiChartTitle( + isLoading: state.status == RangeOfAqiStatus.loading, + ), ), const SizedBox(height: 10), const Divider(), const SizedBox(height: 20), - Expanded(child: RangeOfAqiChart(chartData: state.rangeOfAqi)), + Expanded(child: RangeOfAqiChart(chartData: state.filteredRangeOfAqi)), ], ), ); diff --git a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart index 04cefd6c..6c7aa235 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart @@ -1,15 +1,18 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.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/widgets/chart_informative_cell.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart'; import 'package:syncrow_web/pages/analytics/widgets/charts_loading_widget.dart'; import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart'; class RangeOfAqiChartTitle extends StatelessWidget { - const RangeOfAqiChartTitle({required this.isLoading, super.key}); + const RangeOfAqiChartTitle({ + required this.isLoading, + super.key, + }); + final bool isLoading; static const List<(Color color, String title, bool hasBorder)> _colors = [ @@ -59,19 +62,16 @@ class RangeOfAqiChartTitle extends StatelessWidget { child: FittedBox( fit: BoxFit.scaleDown, alignment: AlignmentDirectional.centerEnd, - child: AqiTypeDropdown( + child: AqiTypeDropdown( onChanged: (value) { final spaceTreeState = context.read().state; final spaceUuid = spaceTreeState.selectedSpaces.firstOrNull; - if (spaceUuid == null) return; + // if (spaceUuid == null) return; - FetchAirQualityDataHelper.loadRangeOfAqi( - context, - spaceUuid: spaceUuid, - date: context.read().state.monthlyDate, - aqiType: value ?? AqiType.aqi, - ); + if (value != null) { + context.read().add(UpdateAqiTypeEvent(value)); + } }, ), ), diff --git a/lib/pages/analytics/params/get_range_of_aqi_param.dart b/lib/pages/analytics/params/get_range_of_aqi_param.dart index bbf24658..ef53fe76 100644 --- a/lib/pages/analytics/params/get_range_of_aqi_param.dart +++ b/lib/pages/analytics/params/get_range_of_aqi_param.dart @@ -1,16 +1,12 @@ import 'package:equatable/equatable.dart'; -import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart'; class GetRangeOfAqiParam extends Equatable { final DateTime date; final String spaceUuid; - final AqiType aqiType; - const GetRangeOfAqiParam( - { + const GetRangeOfAqiParam({ required this.date, required this.spaceUuid, - required this.aqiType, }); @override diff --git a/lib/pages/analytics/services/range_of_aqi/fake_range_of_aqi_service.dart b/lib/pages/analytics/services/range_of_aqi/fake_range_of_aqi_service.dart index 13173c94..01ad6fa1 100644 --- a/lib/pages/analytics/services/range_of_aqi/fake_range_of_aqi_service.dart +++ b/lib/pages/analytics/services/range_of_aqi/fake_range_of_aqi_service.dart @@ -1,4 +1,5 @@ import 'package:syncrow_web/pages/analytics/models/range_of_aqi.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart'; import 'package:syncrow_web/pages/analytics/params/get_range_of_aqi_param.dart'; import 'package:syncrow_web/pages/analytics/services/range_of_aqi/range_of_aqi_service.dart'; @@ -18,10 +19,15 @@ class FakeRangeOfAqiService implements RangeOfAqiService { final avg = (min + avgDelta).clamp(0.0, 301.0); final max = (avg + maxDelta).clamp(0.0, 301.0); - return RangeOfAqi( - min: min, - avg: avg, - max: max, + return RangeOfAqi( + data: [ + RangeOfAqiValue(type: AqiType.aqi.code, min: min, average: avg, max: max), + RangeOfAqiValue(type: AqiType.pm25.code, min: min, average: avg, max: max), + RangeOfAqiValue(type: AqiType.pm10.code, min: min, average: avg, max: max), + RangeOfAqiValue(type: AqiType.hcho.code, min: min, average: avg, max: max), + RangeOfAqiValue(type: AqiType.tvoc.code, min: min, average: avg, max: max), + RangeOfAqiValue(type: AqiType.co2.code, min: min, average: avg, max: max), + ], date: date, ); }); From fa9210f387be966bf8521c18cde952210bf78908 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 2 Jun 2025 14:28:50 +0300 Subject: [PATCH 40/58] added `fromJson` factory methods to `RangeOfAqi`, and to `RangeOfAqiValue` data models. --- lib/pages/analytics/models/range_of_aqi.dart | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/lib/pages/analytics/models/range_of_aqi.dart b/lib/pages/analytics/models/range_of_aqi.dart index 4cee813e..0308d564 100644 --- a/lib/pages/analytics/models/range_of_aqi.dart +++ b/lib/pages/analytics/models/range_of_aqi.dart @@ -9,6 +9,15 @@ class RangeOfAqi extends Equatable { required this.date, }); + factory RangeOfAqi.fromJson(Map json) { + return RangeOfAqi( + date: DateTime.parse(json['date'] as String), + data: (json['data'] as List) + .map((e) => RangeOfAqiValue.fromJson(e as Map)) + .toList(), + ); + } + @override List get props => [data, date]; } @@ -26,6 +35,15 @@ class RangeOfAqiValue extends Equatable { required this.max, }); + 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(), + ); + } + @override List get props => [type, min, average, max]; } From 97801872e06b9fba22255d8827cb9657f641b77a Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 2 Jun 2025 14:29:04 +0300 Subject: [PATCH 41/58] Implemented an initial remote implementation of `RangeOfAqiService`. --- .../remote_range_of_aqi_service.dart | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 lib/pages/analytics/services/range_of_aqi/remote_range_of_aqi_service.dart diff --git a/lib/pages/analytics/services/range_of_aqi/remote_range_of_aqi_service.dart b/lib/pages/analytics/services/range_of_aqi/remote_range_of_aqi_service.dart new file mode 100644 index 00000000..1a80ef33 --- /dev/null +++ b/lib/pages/analytics/services/range_of_aqi/remote_range_of_aqi_service.dart @@ -0,0 +1,34 @@ +import 'package:syncrow_web/pages/analytics/models/range_of_aqi.dart'; +import 'package:syncrow_web/pages/analytics/params/get_range_of_aqi_param.dart'; +import 'package:syncrow_web/pages/analytics/services/range_of_aqi/range_of_aqi_service.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; + +final class RemoteRangeOfAqiService implements RangeOfAqiService { + const RemoteRangeOfAqiService(this._httpService); + + final HTTPService _httpService; + + @override + Future> load(GetRangeOfAqiParam param) async { + try { + final response = await _httpService.get( + path: 'endpoint', + queryParameters: { + 'spaceUuid': param.spaceUuid, + 'date': param.date.toIso8601String(), + }, + expectedResponseModel: (data) { + final json = data as Map? ?? {}; + final mappedData = json['data'] as List? ?? []; + return mappedData.map((e) { + final jsonData = e as Map; + return RangeOfAqi.fromJson(jsonData); + }).toList(); + }, + ); + return response; + } catch (e) { + throw Exception('Failed to load energy consumption per phase: $e'); + } + } +} From 7bc9079212baa5827e6b683bdbb392f7051dfeed Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 2 Jun 2025 14:29:58 +0300 Subject: [PATCH 42/58] reverted a comment. --- .../modules/air_quality/widgets/range_of_aqi_chart_title.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart index 6c7aa235..1b0da288 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart @@ -62,12 +62,12 @@ class RangeOfAqiChartTitle extends StatelessWidget { child: FittedBox( fit: BoxFit.scaleDown, alignment: AlignmentDirectional.centerEnd, - child: AqiTypeDropdown( + child: AqiTypeDropdown( onChanged: (value) { final spaceTreeState = context.read().state; final spaceUuid = spaceTreeState.selectedSpaces.firstOrNull; - // if (spaceUuid == null) return; + if (spaceUuid == null) return; if (value != null) { context.read().add(UpdateAqiTypeEvent(value)); From 8e11749ed7b7e3e71f0958a1400cc258daaa9430 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 2 Jun 2025 16:13:58 +0300 Subject: [PATCH 43/58] Prepared for aqi distribution API Integration. --- .../models/air_quality_data_model.dart | 52 ++++-- .../air_quality_distribution_bloc.dart | 43 ++++- .../air_quality_distribution_event.dart | 9 + .../air_quality_distribution_state.dart | 30 +++- .../fetch_air_quality_data_helper.dart | 2 + .../widgets/aqi_distribution_chart.dart | 161 +++--------------- .../widgets/aqi_distribution_chart_box.dart | 4 +- .../widgets/aqi_distribution_chart_title.dart | 12 +- .../widgets/range_of_aqi_chart_box.dart | 15 +- .../air_quality_data_loading_strategy.dart | 2 + .../analytics_page_tabs_and_children.dart | 95 ++++++++--- ...fake_air_quality_distribution_service.dart | 48 ++++-- ...mote_air_quality_distribution_service.dart | 36 ++++ 13 files changed, 296 insertions(+), 213 deletions(-) create mode 100644 lib/pages/analytics/services/air_quality_distribution/remote_air_quality_distribution_service.dart diff --git a/lib/pages/analytics/models/air_quality_data_model.dart b/lib/pages/analytics/models/air_quality_data_model.dart index d65f1418..2eab2ddb 100644 --- a/lib/pages/analytics/models/air_quality_data_model.dart +++ b/lib/pages/analytics/models/air_quality_data_model.dart @@ -1,24 +1,24 @@ +import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import 'package:syncrow_web/utils/color_manager.dart'; -class AirQualityDataModel { +class AirQualityDataModel extends Equatable { const AirQualityDataModel({ required this.date, - this.good, - this.moderate, - this.poor, - this.unhealthy, - this.severe, - this.hazardous, + required this.data, }); final DateTime date; - final double? good; - final double? moderate; - final double? poor; - final double? unhealthy; - final double? severe; - final double? hazardous; + final List data; + + factory AirQualityDataModel.fromJson(Map json) { + return AirQualityDataModel( + date: DateTime.parse(json['date'] as String), + data: (json['data'] as List) + .map((e) => AirQualityPercentageData.fromJson(e as Map)) + .toList(), + ); + } static final Map metricColors = { 'good': ColorsManager.goodGreen.withValues(alpha: 0.7), @@ -28,4 +28,30 @@ class AirQualityDataModel { 'severe': ColorsManager.severePink.withValues(alpha: 0.7), 'hazardous': ColorsManager.hazardousPurple.withValues(alpha: 0.7), }; + + @override + List get props => [date, data]; +} + +class AirQualityPercentageData extends Equatable { + const AirQualityPercentageData({ + required this.type, + required this.name, + required this.percentage, + }); + + final String type; + final String name; + final double percentage; + + factory AirQualityPercentageData.fromJson(Map json) { + return AirQualityPercentageData( + type: json['type'] as String? ?? '', + name: json['name'] as String? ?? '', + percentage: (json['percentage'] as num?)?.toDouble() ?? 0, + ); + } + + @override + List get props => [type, name, percentage]; } diff --git a/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart b/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart index a81724a2..fb7e2352 100644 --- a/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart +++ b/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart @@ -1,6 +1,7 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart'; import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart'; import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/air_quality_distribution_service.dart'; @@ -9,13 +10,14 @@ part 'air_quality_distribution_state.dart'; class AirQualityDistributionBloc extends Bloc { - final AirQualityDistributionService _service; + final AirQualityDistributionService _aqiDistributionService; AirQualityDistributionBloc( - this._service, + this._aqiDistributionService, ) : super(const AirQualityDistributionState()) { on(_onLoadAirQualityDistribution); on(_onClearAirQualityDistribution); + on(_onUpdateAqiTypeEvent); } Future _onLoadAirQualityDistribution( @@ -23,16 +25,15 @@ class AirQualityDistributionBloc Emitter emit, ) async { try { - emit( - const AirQualityDistributionState( - status: AirQualityDistributionStatus.loading, - ), + emit(state.copyWith(status: AirQualityDistributionStatus.loading)); + final result = await _aqiDistributionService.getAirQualityDistribution( + event.param, ); - final result = await _service.getAirQualityDistribution(event.param); emit( - AirQualityDistributionState( + state.copyWith( status: AirQualityDistributionStatus.success, chartData: result, + filteredChartData: _arrangeChartDataByType(result, state.selectedAqiType), ), ); } catch (e) { @@ -40,6 +41,7 @@ class AirQualityDistributionBloc AirQualityDistributionState( status: AirQualityDistributionStatus.failure, errorMessage: e.toString(), + selectedAqiType: state.selectedAqiType, ), ); } @@ -51,4 +53,29 @@ class AirQualityDistributionBloc ) async { emit(const AirQualityDistributionState()); } + + void _onUpdateAqiTypeEvent( + UpdateAqiTypeEvent event, + Emitter emit, + ) { + emit( + state.copyWith( + selectedAqiType: event.aqiType, + filteredChartData: _arrangeChartDataByType(state.chartData, event.aqiType), + ), + ); + } + + List _arrangeChartDataByType( + List data, + AqiType aqiType, + ) { + final filteredData = data.map( + (data) => AirQualityDataModel( + date: data.date, + data: data.data.where((value) => value.type == aqiType.code).toList(), + ), + ); + return filteredData.toList(); + } } diff --git a/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_event.dart b/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_event.dart index 2e1d291f..b91dafe5 100644 --- a/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_event.dart +++ b/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_event.dart @@ -16,6 +16,15 @@ final class LoadAirQualityDistribution extends AirQualityDistributionEvent { List get props => [param]; } +final class UpdateAqiTypeEvent extends AirQualityDistributionEvent { + const UpdateAqiTypeEvent(this.aqiType); + + final AqiType aqiType; + + @override + List get props => [aqiType]; +} + final class ClearAirQualityDistribution extends AirQualityDistributionEvent { const ClearAirQualityDistribution(); } diff --git a/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_state.dart b/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_state.dart index 0db95e2d..65665882 100644 --- a/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_state.dart +++ b/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_state.dart @@ -8,16 +8,36 @@ enum AirQualityDistributionStatus { } class AirQualityDistributionState extends Equatable { - final AirQualityDistributionStatus status; - final List chartData; - final String? errorMessage; - const AirQualityDistributionState({ this.status = AirQualityDistributionStatus.initial, this.chartData = const [], + this.filteredChartData = const [], this.errorMessage, + this.selectedAqiType = AqiType.aqi, }); + final AirQualityDistributionStatus status; + final List chartData; + final List filteredChartData; + final String? errorMessage; + final AqiType selectedAqiType; + + AirQualityDistributionState copyWith({ + AirQualityDistributionStatus? status, + List? chartData, + List? filteredChartData, + String? errorMessage, + AqiType? selectedAqiType, + }) { + return AirQualityDistributionState( + status: status ?? this.status, + chartData: chartData ?? this.chartData, + filteredChartData: filteredChartData ?? this.filteredChartData, + errorMessage: errorMessage ?? this.errorMessage, + selectedAqiType: selectedAqiType ?? this.selectedAqiType, + ); + } + @override - List get props => [status, chartData, errorMessage]; + List get props => [status, chartData, errorMessage, selectedAqiType]; } 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 1919f518..e212dedf 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 @@ -14,8 +14,10 @@ abstract final class FetchAirQualityDataHelper { static void loadAirQualityData( BuildContext context, { + required DateTime date, required String communityUuid, required String spaceUuid, + bool shouldFetchAnalyticsDevices = true, }) { final date = context.read().state.monthlyDate; loadAnalyticsDevices( 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 89b6dd1d..373e36ca 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 @@ -16,6 +16,11 @@ class AqiDistributionChart extends StatelessWidget { @override Widget build(BuildContext context) { + final sortedData = List.from(chartData) + ..sort( + (a, b) => a.date.compareTo(b.date), + ); + return BarChart( BarChartData( maxY: 100.1, @@ -25,45 +30,29 @@ class AqiDistributionChart extends StatelessWidget { borderData: EnergyManagementChartsHelper.borderData(), barTouchData: _barTouchData(context), titlesData: _titlesData(context), - barGroups: _buildBarGroups(), + barGroups: _buildBarGroups(sortedData), ), duration: Duration.zero, ); } - List _buildBarGroups() { - return List.generate(chartData.length, (index) { - final data = chartData[index]; + List _buildBarGroups(List sortedData) { + return List.generate(sortedData.length, (index) { + final data = sortedData[index]; final stackItems = []; double currentY = 0; bool isFirstElement = true; - if (data.good != null) { - stackItems.add( - BarChartRodData( - fromY: currentY, - toY: currentY + data.good!, - color: AirQualityDataModel.metricColors['good']!, - borderRadius: isFirstElement - ? const BorderRadius.only( - topLeft: Radius.circular(22), - topRight: Radius.circular(22), - ) - // ignore: dead_code - : _barBorderRadius, - width: _barWidth, - ), - ); - currentY += data.good! + _rodStackItemsSpacing; - isFirstElement = false; - } + // Sort data by type to ensure consistent order + final sortedPercentageData = List.from(data.data) + ..sort((a, b) => a.type.compareTo(b.type)); - if (data.moderate != null) { + for (final percentageData in sortedPercentageData) { stackItems.add( BarChartRodData( fromY: currentY, - toY: currentY + data.moderate!, - color: AirQualityDataModel.metricColors['moderate']!, + toY: currentY + percentageData.percentage , + color: AirQualityDataModel.metricColors[percentageData.name]!, borderRadius: isFirstElement ? const BorderRadius.only( topLeft: Radius.circular(22), @@ -73,83 +62,7 @@ class AqiDistributionChart extends StatelessWidget { width: _barWidth, ), ); - currentY += data.moderate! + _rodStackItemsSpacing; - isFirstElement = false; - } - - if (data.poor != null) { - stackItems.add( - BarChartRodData( - fromY: currentY, - toY: currentY + data.poor!, - color: AirQualityDataModel.metricColors['poor']!, - borderRadius: isFirstElement - ? const BorderRadius.only( - topLeft: Radius.circular(22), - topRight: Radius.circular(22), - ) - : _barBorderRadius, - width: _barWidth, - ), - ); - currentY += data.poor! + _rodStackItemsSpacing; - isFirstElement = false; - } - - if (data.unhealthy != null) { - stackItems.add( - BarChartRodData( - fromY: currentY, - toY: currentY + data.unhealthy!, - color: AirQualityDataModel.metricColors['unhealthy']!, - borderRadius: isFirstElement - ? const BorderRadius.only( - topLeft: Radius.circular(22), - topRight: Radius.circular(22), - ) - : _barBorderRadius, - width: _barWidth, - ), - ); - currentY += data.unhealthy! + _rodStackItemsSpacing; - isFirstElement = false; - } - - if (data.severe != null) { - stackItems.add( - BarChartRodData( - fromY: currentY, - toY: currentY + data.severe!, - color: AirQualityDataModel.metricColors['severe']!, - borderRadius: isFirstElement - ? const BorderRadius.only( - topLeft: Radius.circular(22), - topRight: Radius.circular(22), - ) - : _barBorderRadius, - width: _barWidth, - ), - ); - currentY += data.severe! + _rodStackItemsSpacing; - isFirstElement = false; - } - - if (data.hazardous != null) { - stackItems.add( - BarChartRodData( - fromY: currentY, - toY: currentY + data.hazardous!, - color: AirQualityDataModel.metricColors['hazardous']!, - borderRadius: isFirstElement - ? const BorderRadius.only( - topLeft: Radius.circular(22), - topRight: Radius.circular(22), - ) - : _barBorderRadius, - width: _barWidth, - ), - ); - currentY += data.hazardous! + _rodStackItemsSpacing; + currentY += percentageData.percentage + _rodStackItemsSpacing; isFirstElement = false; } @@ -180,44 +93,14 @@ class AqiDistributionChart extends StatelessWidget { fontSize: 12, ); - if (data.good != null) { - children.add(TextSpan( - text: '\nGOOD: ${data.good!.toStringAsFixed(1)}%', - style: textStyle, - )); - } + // Sort data by type to ensure consistent order + final sortedPercentageData = List.from(data.data) + ..sort((a, b) => a.type.compareTo(b.type)); - if (data.moderate != null) { + for (final percentageData in sortedPercentageData) { children.add(TextSpan( - text: '\nMODERATE: ${data.moderate!.toStringAsFixed(1)}%', - style: textStyle, - )); - } - - if (data.poor != null) { - children.add(TextSpan( - text: '\nPOOR: ${data.poor!.toStringAsFixed(1)}%', - style: textStyle, - )); - } - - if (data.unhealthy != null) { - children.add(TextSpan( - text: '\nUNHEALTHY: ${data.unhealthy!.toStringAsFixed(1)}%', - style: textStyle, - )); - } - - if (data.severe != null) { - children.add(TextSpan( - text: '\nSEVERE: ${data.severe!.toStringAsFixed(1)}%', - style: textStyle, - )); - } - - if (data.hazardous != null) { - children.add(TextSpan( - text: '\nHAZARDOUS: ${data.hazardous!.toStringAsFixed(1)}%', + text: + '\n${percentageData.type.toUpperCase()}: ${percentageData.percentage.toStringAsFixed(1)}%', style: textStyle, )); } diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart index 8347a15b..8a57fe0b 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart @@ -32,7 +32,9 @@ class AqiDistributionChartBox extends StatelessWidget { const SizedBox(height: 10), const Divider(), const SizedBox(height: 20), - Expanded(child: AqiDistributionChart(chartData: state.chartData)), + Expanded( + child: AqiDistributionChart(chartData: state.filteredChartData), + ), ], ), ); 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 5045316b..e32043c5 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 @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart'; import 'package:syncrow_web/pages/analytics/widgets/charts_loading_widget.dart'; @@ -16,7 +18,7 @@ class AqiDistributionChartTitle extends StatelessWidget { const Expanded( flex: 3, child: FittedBox( - fit: BoxFit.scaleDown, + fit: BoxFit.scaleDown, alignment: AlignmentDirectional.centerStart, child: ChartTitle( title: Text('Distribution over Air Quality Index'), @@ -27,7 +29,13 @@ class AqiDistributionChartTitle extends StatelessWidget { alignment: AlignmentDirectional.centerEnd, fit: BoxFit.scaleDown, child: AqiTypeDropdown( - onChanged: (value) {}, + onChanged: (value) { + if (value != null) { + context + .read() + .add(UpdateAqiTypeEvent(value)); + } + }, ), ), ], 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 fefb7a9c..6548c696 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 @@ -3,7 +3,6 @@ import 'package:flutter_bloc/flutter_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/range_of_aqi_chart.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart'; -import 'package:syncrow_web/pages/analytics/params/get_range_of_aqi_param.dart'; import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart'; import 'package:syncrow_web/utils/style.dart'; @@ -27,18 +26,8 @@ class RangeOfAqiChartBox extends StatelessWidget { AnalyticsErrorWidget(state.errorMessage), const SizedBox(height: 10), ], - GestureDetector( - onTap: () { - context.read().add(LoadRangeOfAqiEvent( - GetRangeOfAqiParam( - spaceUuid: '123', - date: DateTime.now().subtract(const Duration(days: 30)), - ), - )); - }, - child: RangeOfAqiChartTitle( - isLoading: state.status == RangeOfAqiStatus.loading, - ), + RangeOfAqiChartTitle( + isLoading: state.status == RangeOfAqiStatus.loading, ), const SizedBox(height: 10), const Divider(), diff --git a/lib/pages/analytics/modules/analytics/strategies/air_quality_data_loading_strategy.dart b/lib/pages/analytics/modules/analytics/strategies/air_quality_data_loading_strategy.dart index dc3b1c5e..5d62029f 100644 --- a/lib/pages/analytics/modules/analytics/strategies/air_quality_data_loading_strategy.dart +++ b/lib/pages/analytics/modules/analytics/strategies/air_quality_data_loading_strategy.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.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/strategies/analytics_data_loading_strategy.dart'; import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart'; import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart'; @@ -39,6 +40,7 @@ final class AirQualityDataLoadingStrategy implements AnalyticsDataLoadingStrateg context, communityUuid: community.uuid, spaceUuid: space.uuid ?? '', + date: context.read().state.monthlyDate, ); } diff --git a/lib/pages/analytics/modules/analytics/widgets/analytics_page_tabs_and_children.dart b/lib/pages/analytics/modules/analytics/widgets/analytics_page_tabs_and_children.dart index 5e9e347a..f6197e46 100644 --- a/lib/pages/analytics/modules/analytics/widgets/analytics_page_tabs_and_children.dart +++ b/lib/pages/analytics/modules/analytics/widgets/analytics_page_tabs_and_children.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.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_tab/analytics_tab_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/analytics/enums/analytics_page_tab.dart'; @@ -56,33 +57,16 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget { const Spacer(), Visibility( key: ValueKey(selectedTab), - visible: selectedTab == AnalyticsPageTab.energyManagement, + visible: selectedTab == AnalyticsPageTab.energyManagement || + selectedTab == AnalyticsPageTab.airQuality, child: Expanded( flex: 2, child: FittedBox( fit: BoxFit.scaleDown, alignment: AlignmentDirectional.centerEnd, child: AnalyticsDateFilterButton( - onDateSelected: (DateTime value) { - context.read().add( - UpdateAnalyticsDatePickerEvent(montlyDate: value), - ); - - final spaceTreeState = - context.read().state; - if (spaceTreeState.selectedSpaces.isNotEmpty) { - FetchEnergyManagementDataHelper - .loadEnergyManagementData( - context, - shouldFetchAnalyticsDevices: false, - selectedDate: value, - communityId: - spaceTreeState.selectedCommunities.firstOrNull ?? - '', - spaceId: - spaceTreeState.selectedSpaces.firstOrNull ?? '', - ); - } + onDateSelected: (value) { + _onDateChanged(context, value, selectedTab); }, selectedDate: context .watch() @@ -112,4 +96,73 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget { child: child, ); } + + void _onDateChanged( + BuildContext context, + DateTime date, + AnalyticsPageTab selectedTab, + ) { + context.read().add( + UpdateAnalyticsDatePickerEvent(montlyDate: date), + ); + + final spaceTreeState = context.read().state; + final communities = spaceTreeState.selectedCommunities; + final spaces = spaceTreeState.selectedSpaces; + if (spaceTreeState.selectedSpaces.isNotEmpty) { + switch (selectedTab) { + case AnalyticsPageTab.energyManagement: + _onEnergyManagementDateChanged( + context, + date: date, + communityUuid: communities.firstOrNull ?? '', + spaceUuid: spaces.firstOrNull ?? '', + ); + break; + case AnalyticsPageTab.airQuality: + _onAirQualityDateChanged( + context, + date: date, + communityUuid: communities.firstOrNull ?? '', + spaceUuid: spaces.firstOrNull ?? '', + ); + default: + break; + } + } + } + + void _onEnergyManagementDateChanged( + BuildContext context, { + required DateTime date, + required String communityUuid, + required String spaceUuid, + }) { + context.read().add( + UpdateAnalyticsDatePickerEvent(montlyDate: date), + ); + + FetchEnergyManagementDataHelper.loadEnergyManagementData( + context, + shouldFetchAnalyticsDevices: false, + selectedDate: date, + communityId: communityUuid, + spaceId: spaceUuid, + ); + } + + void _onAirQualityDateChanged( + BuildContext context, { + required DateTime date, + required String communityUuid, + required String spaceUuid, + }) { + FetchAirQualityDataHelper.loadAirQualityData( + context, + date: date, + communityUuid: communityUuid, + spaceUuid: spaceUuid, + shouldFetchAnalyticsDevices: false, + ); + } } diff --git a/lib/pages/analytics/services/air_quality_distribution/fake_air_quality_distribution_service.dart b/lib/pages/analytics/services/air_quality_distribution/fake_air_quality_distribution_service.dart index 264addab..e0023f53 100644 --- a/lib/pages/analytics/services/air_quality_distribution/fake_air_quality_distribution_service.dart +++ b/lib/pages/analytics/services/air_quality_distribution/fake_air_quality_distribution_service.dart @@ -1,6 +1,7 @@ import 'dart:math'; import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart'; import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart'; import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/air_quality_distribution_service.dart'; @@ -19,30 +20,56 @@ class FakeAirQualityDistributionService implements AirQualityDistributionService final values = _generateRandomPercentages(); final nullMask = List.generate(6, (_) => _shouldBeNull()); - // If all values are null, force at least one to be non-null if (nullMask.every((isNull) => isNull)) { nullMask[_random.nextInt(6)] = false; } - // Redistribute percentages among non-null values final nonNullValues = _redistributePercentages(values, nullMask); return AirQualityDataModel( date: date, - good: nullMask[0] ? null : nonNullValues[0], - moderate: nullMask[1] ? null : nonNullValues[1], - poor: nullMask[2] ? null : nonNullValues[2], - unhealthy: nullMask[3] ? null : nonNullValues[3], - severe: nullMask[4] ? null : nonNullValues[4], - hazardous: nullMask[5] ? null : nonNullValues[5], + data: [ + AirQualityPercentageData( + type: AqiType.aqi.code, + percentage: nonNullValues[0], + name: 'good', + ), + AirQualityPercentageData( + name: 'moderate', + type: AqiType.co2.code, + percentage: nonNullValues[1], + ), + AirQualityPercentageData( + name: 'poor', + percentage: nonNullValues[2], + type: AqiType.hcho.code, + + ), + AirQualityPercentageData( + name: 'unhealthy', + percentage: nonNullValues[3], + type: AqiType.pm10.code, + ), + AirQualityPercentageData( + name: 'severe', + type: AqiType.pm25.code, + percentage: nonNullValues[4], + ), + AirQualityPercentageData( + name: 'hazardous', + percentage: nonNullValues[5], + type: AqiType.co2.code, + ), + ], ); }), ); } List _redistributePercentages( - List originalValues, List nullMask) { - // Calculate total of non-null values + List originalValues, + List nullMask, + ) { double nonNullSum = 0; for (int i = 0; i < originalValues.length; i++) { if (!nullMask[i]) { @@ -50,7 +77,6 @@ class FakeAirQualityDistributionService implements AirQualityDistributionService } } - // Redistribute percentages to maintain 100% total return List.generate(originalValues.length, (i) { if (nullMask[i]) return 0; return (originalValues[i] / nonNullSum * 100).roundToDouble(); diff --git a/lib/pages/analytics/services/air_quality_distribution/remote_air_quality_distribution_service.dart b/lib/pages/analytics/services/air_quality_distribution/remote_air_quality_distribution_service.dart new file mode 100644 index 00000000..dcf00600 --- /dev/null +++ b/lib/pages/analytics/services/air_quality_distribution/remote_air_quality_distribution_service.dart @@ -0,0 +1,36 @@ +import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart'; +import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart'; +import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/air_quality_distribution_service.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; + +class RemoteAirQualityDistributionService implements AirQualityDistributionService { + RemoteAirQualityDistributionService(this._httpService); + + final HTTPService _httpService; + + @override + Future> getAirQualityDistribution( + GetAirQualityDistributionParam param, + ) async { + try { + final response = await _httpService.get( + path: 'endpoint', + queryParameters: { + 'spaceUuid': param.spaceUuid, + 'date': param.date.toIso8601String(), + }, + expectedResponseModel: (data) { + final json = data as Map? ?? {}; + final mappedData = json['data'] as List? ?? []; + return mappedData.map((e) { + final jsonData = e as Map; + return AirQualityDataModel.fromJson(jsonData); + }).toList(); + }, + ); + return response; + } catch (e) { + throw Exception('Failed to load energy consumption per phase: $e'); + } + } +} From 5595bb7f250212b9a6bd9980f9fedb9c52723775 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 2 Jun 2025 16:35:55 +0300 Subject: [PATCH 44/58] Refactor `OneGangGlassSwitchBloc` to utilize new service dependencies and implement a factory for instantiation. Enhanced event handling methods for improved error management and state updates. --- .../bloc/one_gang_glass_switch_bloc.dart | 234 ++++++++---------- .../one_gang_glass_switch_bloc_factory.dart | 18 ++ .../one_gang_glass_batch_control_view.dart | 4 +- .../one_gang_glass_switch_control_view.dart | 5 +- 4 files changed, 125 insertions(+), 136 deletions(-) create mode 100644 lib/pages/device_managment/one_g_glass_switch/factories/one_gang_glass_switch_bloc_factory.dart diff --git a/lib/pages/device_managment/one_g_glass_switch/bloc/one_gang_glass_switch_bloc.dart b/lib/pages/device_managment/one_g_glass_switch/bloc/one_gang_glass_switch_bloc.dart index 12aeaa88..c1e976ab 100644 --- a/lib/pages/device_managment/one_g_glass_switch/bloc/one_gang_glass_switch_bloc.dart +++ b/lib/pages/device_managment/one_g_glass_switch/bloc/one_gang_glass_switch_bloc.dart @@ -1,11 +1,13 @@ import 'dart:async'; -import 'package:bloc/bloc.dart'; import 'package:firebase_database/firebase_database.dart'; -import 'package:meta/meta.dart'; +import 'package:flutter/foundation.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/all_devices/models/factory_reset_model.dart'; import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/models/once_gang_glass_status_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'; part 'one_gang_glass_switch_event.dart'; @@ -13,13 +15,16 @@ part 'one_gang_glass_switch_state.dart'; class OneGangGlassSwitchBloc extends Bloc { - OneGangGlassStatusModel deviceStatus; - Timer? _timer; + late OneGangGlassStatusModel deviceStatus; + final String deviceId; + final ControlDeviceService controlDeviceService; + final BatchControlDevicesService batchControlDevicesService; - OneGangGlassSwitchBloc({required String deviceId}) - : deviceStatus = OneGangGlassStatusModel( - uuid: deviceId, switch1: false, countDown: 0), - super(OneGangGlassSwitchInitial()) { + OneGangGlassSwitchBloc({ + required this.deviceId, + required this.controlDeviceService, + required this.batchControlDevicesService, + }) : super(OneGangGlassSwitchInitial()) { on(_onFetchDeviceStatus); on(_onControl); on(_onBatchControl); @@ -28,160 +33,140 @@ class OneGangGlassSwitchBloc on(_onStatusUpdated); } - Future _onFetchDeviceStatus(OneGangGlassSwitchFetchDeviceEvent event, - Emitter emit) async { + Future _onFetchDeviceStatus( + OneGangGlassSwitchFetchDeviceEvent event, + Emitter emit, + ) async { emit(OneGangGlassSwitchLoading()); try { - final status = - await DevicesManagementApi().getDeviceStatus(event.deviceId); - _listenToChanges(event.deviceId); - deviceStatus = - OneGangGlassStatusModel.fromJson(event.deviceId, status.status); + final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); + _listenToChanges(event.deviceId, emit); + deviceStatus = OneGangGlassStatusModel.fromJson(event.deviceId, status.status); emit(OneGangGlassSwitchStatusLoaded(deviceStatus)); } catch (e) { emit(OneGangGlassSwitchError(e.toString())); } } - _listenToChanges(deviceId) { + void _listenToChanges( + String deviceId, + Emitter emit, + ) { try { - DatabaseReference ref = - FirebaseDatabase.instance.ref('device-status/$deviceId'); - Stream stream = ref.onValue; + final ref = FirebaseDatabase.instance.ref('device-status/$deviceId'); + final stream = ref.onValue; stream.listen((DatabaseEvent event) { - Map usersMap = - event.snapshot.value as Map; + final data = event.snapshot.value as Map?; + if (data == null) return; - List statusList = []; - usersMap['status'].forEach((element) { - statusList - .add(Status(code: element['code'], value: element['value'])); - }); - - deviceStatus = OneGangGlassStatusModel.fromJson( - usersMap['productUuid'], statusList); - if (!isClosed) { - add(StatusUpdated(deviceStatus)); + final statusList = []; + if (data['status'] != null) { + for (var element in data['status']) { + statusList.add( + Status( + code: element['code'].toString(), + value: element['value'].toString(), + ), + ); + } + } + if (statusList.isNotEmpty) { + final newStatus = OneGangGlassStatusModel.fromJson(deviceId, statusList); + if (newStatus != deviceStatus) { + deviceStatus = newStatus; + if (!isClosed) { + add(StatusUpdated(deviceStatus)); + } + } } }); - } catch (_) {} + } catch (e) { + emit(OneGangGlassSwitchError('Failed to listen to changes: $e')); + } } void _onStatusUpdated( - StatusUpdated event, Emitter emit) { + StatusUpdated event, + Emitter emit, + ) { + emit(OneGangGlassSwitchLoading()); deviceStatus = event.deviceStatus; emit(OneGangGlassSwitchStatusLoaded(deviceStatus)); } - Future _onControl(OneGangGlassSwitchControl event, - Emitter emit) async { - final oldValue = _getValueByCode(event.code); - + Future _onControl( + OneGangGlassSwitchControl event, + Emitter emit, + ) async { + emit(OneGangGlassSwitchLoading()); _updateLocalValue(event.code, event.value); emit(OneGangGlassSwitchStatusLoaded(deviceStatus)); - await _runDebounce( - deviceId: event.deviceId, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - isBatch: false, - ); - } - - Future _onFactoryReset(OneGangGlassFactoryResetEvent event, - Emitter emit) async { - emit(OneGangGlassSwitchLoading()); try { - final response = await DevicesManagementApi() - .factoryReset(event.factoryReset, event.deviceId); - if (!response) { - emit(OneGangGlassSwitchError('Failed to reset device')); - } else { - emit(OneGangGlassSwitchStatusLoaded(deviceStatus)); - } + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status(code: event.code, value: event.value), + ); } catch (e) { + _updateLocalValue(event.code, !event.value); emit(OneGangGlassSwitchError(e.toString())); } } - Future _onBatchControl(OneGangGlassSwitchBatchControl event, - Emitter emit) async { - final oldValue = _getValueByCode(event.code); - + Future _onBatchControl( + OneGangGlassSwitchBatchControl event, + Emitter emit, + ) async { + emit(OneGangGlassSwitchLoading()); _updateLocalValue(event.code, event.value); emit(OneGangGlassSwitchStatusLoaded(deviceStatus)); - await _runDebounce( - deviceId: event.deviceIds, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - isBatch: true, - ); + try { + await batchControlDevicesService.batchControlDevices( + uuids: event.deviceIds, + code: event.code, + value: event.value, + ); + } catch (e) { + _updateLocalValue(event.code, !event.value); + emit(OneGangGlassSwitchError(e.toString())); + } } Future _onFetchBatchStatus( - OneGangGlassSwitchFetchBatchStatusEvent event, - Emitter emit) async { + OneGangGlassSwitchFetchBatchStatusEvent event, + Emitter emit, + ) async { emit(OneGangGlassSwitchLoading()); try { - final status = - await DevicesManagementApi().getBatchStatus(event.deviceIds); - deviceStatus = OneGangGlassStatusModel.fromJson( - event.deviceIds.first, status.status); + final status = await DevicesManagementApi().getBatchStatus(event.deviceIds); + deviceStatus = + OneGangGlassStatusModel.fromJson(event.deviceIds.first, status.status); emit(OneGangGlassSwitchStatusLoaded(deviceStatus)); } catch (e) { emit(OneGangGlassSwitchError(e.toString())); } } - Future _runDebounce({ - required dynamic deviceId, - required String code, - required bool value, - required bool oldValue, - required Emitter emit, - required bool isBatch, - }) async { - late String id; - if (deviceId is List) { - id = deviceId.first; - } else { - id = deviceId; - } - - if (_timer != null) { - _timer!.cancel(); - } - - _timer = Timer(const Duration(milliseconds: 500), () async { - try { - late bool response; - if (isBatch) { - response = await DevicesManagementApi() - .deviceBatchControl(deviceId, code, value); - } else { - response = await DevicesManagementApi() - .deviceControl(deviceId, Status(code: code, value: value)); - } - - if (!response) { - _revertValueAndEmit(id, code, oldValue, emit); - } - } catch (e) { - _revertValueAndEmit(id, code, oldValue, emit); + Future _onFactoryReset( + OneGangGlassFactoryResetEvent event, + Emitter emit, + ) async { + emit(OneGangGlassSwitchLoading()); + try { + final response = await DevicesManagementApi().factoryReset( + event.factoryReset, + event.deviceId, + ); + if (!response) { + emit(OneGangGlassSwitchError('Failed to reset device')); + } else { + add(OneGangGlassSwitchFetchDeviceEvent(event.deviceId)); } - }); - } - - void _revertValueAndEmit(String deviceId, String code, bool oldValue, - Emitter emit) { - _updateLocalValue(code, oldValue); - emit(OneGangGlassSwitchStatusLoaded(deviceStatus)); + } catch (e) { + emit(OneGangGlassSwitchError(e.toString())); + } } void _updateLocalValue(String code, bool value) { @@ -189,19 +174,4 @@ class OneGangGlassSwitchBloc deviceStatus = deviceStatus.copyWith(switch1: value); } } - - bool _getValueByCode(String code) { - switch (code) { - case 'switch_1': - return deviceStatus.switch1; - default: - return false; - } - } - - @override - Future close() { - _timer?.cancel(); - return super.close(); - } } diff --git a/lib/pages/device_managment/one_g_glass_switch/factories/one_gang_glass_switch_bloc_factory.dart b/lib/pages/device_managment/one_g_glass_switch/factories/one_gang_glass_switch_bloc_factory.dart new file mode 100644 index 00000000..97bcab81 --- /dev/null +++ b/lib/pages/device_managment/one_g_glass_switch/factories/one_gang_glass_switch_bloc_factory.dart @@ -0,0 +1,18 @@ +import 'package:syncrow_web/pages/device_managment/factories/device_bloc_dependencies_factory.dart'; +import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/bloc/one_gang_glass_switch_bloc.dart'; + +abstract final class OneGangGlassSwitchBlocFactory { + const OneGangGlassSwitchBlocFactory._(); + + static OneGangGlassSwitchBloc create({ + required String deviceId, + }) { + return OneGangGlassSwitchBloc( + deviceId: deviceId, + controlDeviceService: + DeviceBlocDependenciesFactory.createControlDeviceService(), + batchControlDevicesService: + DeviceBlocDependenciesFactory.createBatchControlDevicesService(), + ); + } +} diff --git a/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_batch_control_view.dart b/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_batch_control_view.dart index 9b89e876..307e61da 100644 --- a/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_batch_control_view.dart +++ b/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_batch_control_view.dart @@ -2,9 +2,9 @@ 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/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/shared/batch_control/factory_reset.dart'; -// import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; @@ -16,7 +16,7 @@ class OneGangGlassSwitchBatchControlView extends StatelessWidget with HelperResp @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => OneGangGlassSwitchBloc(deviceId: deviceIds.first) + create: (context) => OneGangGlassSwitchBlocFactory.create(deviceId: deviceIds.first) ..add(OneGangGlassSwitchFetchBatchStatusEvent(deviceIds)), child: BlocBuilder( builder: (context, state) { 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 8914b786..997be513 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,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.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/shared/toggle_widget.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; @@ -9,13 +10,13 @@ import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_la class OneGangGlassSwitchControlView extends StatelessWidget with HelperResponsiveLayout { final String deviceId; - const OneGangGlassSwitchControlView({required this.deviceId, Key? key}) : super(key: key); + const OneGangGlassSwitchControlView({required this.deviceId, super.key}); @override Widget build(BuildContext context) { return BlocProvider( create: (context) => - OneGangGlassSwitchBloc(deviceId: deviceId)..add(OneGangGlassSwitchFetchDeviceEvent(deviceId)), + OneGangGlassSwitchBlocFactory.create(deviceId: deviceId)..add(OneGangGlassSwitchFetchDeviceEvent(deviceId)), child: BlocBuilder( builder: (context, state) { if (state is OneGangGlassSwitchLoading) { From b06a23cc60332a11353abd4be454894871ab083e Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 2 Jun 2025 16:40:13 +0300 Subject: [PATCH 45/58] Refactor `WallLightSwitchBloc` to integrate new service dependencies and utilize a factory for instantiation. Improved event handling methods for better error management and state updates. --- .../bloc/wall_light_switch_bloc.dart | 236 ++++++++---------- .../wall_light_switch_bloc_factory.dart | 18 ++ .../view/wall_light_batch_control.dart | 4 +- .../view/wall_light_device_control.dart | 3 +- 4 files changed, 122 insertions(+), 139 deletions(-) create mode 100644 lib/pages/device_managment/one_gang_switch/factories/wall_light_switch_bloc_factory.dart diff --git a/lib/pages/device_managment/one_gang_switch/bloc/wall_light_switch_bloc.dart b/lib/pages/device_managment/one_gang_switch/bloc/wall_light_switch_bloc.dart index c2038330..59eccfe9 100644 --- a/lib/pages/device_managment/one_gang_switch/bloc/wall_light_switch_bloc.dart +++ b/lib/pages/device_managment/one_gang_switch/bloc/wall_light_switch_bloc.dart @@ -6,12 +6,21 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/device_sta 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/models/wall_light_status_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'; -class WallLightSwitchBloc - extends Bloc { - WallLightSwitchBloc({required this.deviceId}) - : super(WallLightSwitchInitial()) { +class WallLightSwitchBloc extends Bloc { + late WallLightStatusModel deviceStatus; + final String deviceId; + final ControlDeviceService controlDeviceService; + final BatchControlDevicesService batchControlDevicesService; + + WallLightSwitchBloc({ + required this.deviceId, + required this.controlDeviceService, + required this.batchControlDevicesService, + }) : super(WallLightSwitchInitial()) { on(_onFetchDeviceStatus); on(_onControl); on(_onFetchBatchStatus); @@ -20,143 +29,114 @@ class WallLightSwitchBloc on(_onStatusUpdated); } - late WallLightStatusModel deviceStatus; - final String deviceId; - Timer? _timer; - - FutureOr _onFetchDeviceStatus(WallLightSwitchFetchDeviceEvent event, - Emitter emit) async { + Future _onFetchDeviceStatus( + WallLightSwitchFetchDeviceEvent event, + Emitter emit, + ) async { emit(WallLightSwitchLoading()); try { - final status = - await DevicesManagementApi().getDeviceStatus(event.deviceId); - - deviceStatus = - WallLightStatusModel.fromJson(event.deviceId, status.status); - _listenToChanges(event.deviceId); + final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); + _listenToChanges(event.deviceId, emit); + deviceStatus = WallLightStatusModel.fromJson(event.deviceId, status.status); emit(WallLightSwitchStatusLoaded(deviceStatus)); } catch (e) { emit(WallLightSwitchError(e.toString())); } } - _listenToChanges(deviceId) { + void _listenToChanges( + String deviceId, + Emitter emit, + ) { try { - DatabaseReference ref = - FirebaseDatabase.instance.ref('device-status/$deviceId'); - Stream stream = ref.onValue; + final ref = FirebaseDatabase.instance.ref('device-status/$deviceId'); + final stream = ref.onValue; stream.listen((DatabaseEvent event) { - Map usersMap = - event.snapshot.value as Map; + final data = event.snapshot.value as Map?; + if (data == null) return; - List statusList = []; - usersMap['status'].forEach((element) { - statusList - .add(Status(code: element['code'], value: element['value'])); - }); - - deviceStatus = - WallLightStatusModel.fromJson(usersMap['productUuid'], statusList); - if (!isClosed) { - add(StatusUpdated(deviceStatus)); + final statusList = []; + if (data['status'] != null) { + for (var element in data['status']) { + statusList.add( + Status( + code: element['code'].toString(), + value: element['value'].toString(), + ), + ); + } + } + if (statusList.isNotEmpty) { + final newStatus = WallLightStatusModel.fromJson(deviceId, statusList); + if (newStatus != deviceStatus) { + deviceStatus = newStatus; + if (!isClosed) { + add(StatusUpdated(deviceStatus)); + } + } } }); - } catch (_) {} + } catch (e) { + emit(WallLightSwitchError('Failed to listen to changes: $e')); + } } void _onStatusUpdated( - StatusUpdated event, Emitter emit) { + StatusUpdated event, + Emitter emit, + ) { + emit(WallLightSwitchLoading()); deviceStatus = event.deviceStatus; emit(WallLightSwitchStatusLoaded(deviceStatus)); } - FutureOr _onControl( - WallLightSwitchControl event, Emitter emit) async { - final oldValue = _getValueByCode(event.code); - + Future _onControl( + WallLightSwitchControl event, + Emitter emit, + ) async { + emit(WallLightSwitchLoading()); _updateLocalValue(event.code, event.value); - emit(WallLightSwitchStatusLoaded(deviceStatus)); - await _runDebounce( - deviceId: event.deviceId, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - isBatch: false, - ); + try { + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status(code: event.code, value: event.value), + ); + } catch (e) { + _updateLocalValue(event.code, !event.value); + emit(WallLightSwitchError(e.toString())); + } } - Future _runDebounce({ - required dynamic deviceId, - required String code, - required bool value, - required bool oldValue, - required Emitter emit, - required bool isBatch, - }) async { - late String id; - - if (deviceId is List) { - id = deviceId.first; - } else { - id = deviceId; - } - - if (_timer != null) { - _timer!.cancel(); - } - - _timer = Timer(const Duration(milliseconds: 500), () async { - try { - late bool response; - - if (isBatch) { - response = await DevicesManagementApi() - .deviceBatchControl(deviceId, code, value); - } else { - response = await DevicesManagementApi() - .deviceControl(deviceId, Status(code: code, value: value)); - } - - if (!response) { - _revertValueAndEmit(id, code, oldValue, emit); - } - } catch (e) { - _revertValueAndEmit(id, code, oldValue, emit); - } - }); - } - - void _revertValueAndEmit(String deviceId, String code, bool oldValue, - Emitter emit) { - _updateLocalValue(code, oldValue); + Future _onBatchControl( + WallLightSwitchBatchControl event, + Emitter emit, + ) async { + emit(WallLightSwitchLoading()); + _updateLocalValue(event.code, event.value); emit(WallLightSwitchStatusLoaded(deviceStatus)); - } - void _updateLocalValue(String code, bool value) { - if (code == 'switch_1') { - deviceStatus = deviceStatus.copyWith(switch1: value); + try { + await batchControlDevicesService.batchControlDevices( + uuids: event.devicesIds, + code: event.code, + value: event.value, + ); + } catch (e) { + _updateLocalValue(event.code, !event.value); + emit(WallLightSwitchError(e.toString())); } } - bool _getValueByCode(String code) { - switch (code) { - case 'switch_1': - return deviceStatus.switch1; - default: - return false; - } - } - - Future _onFetchBatchStatus(WallLightSwitchFetchBatchEvent event, - Emitter emit) async { + Future _onFetchBatchStatus( + WallLightSwitchFetchBatchEvent event, + Emitter emit, + ) async { emit(WallLightSwitchLoading()); try { - final status = - await DevicesManagementApi().getBatchStatus(event.devicesIds); + final status = await DevicesManagementApi().getBatchStatus(event.devicesIds); deviceStatus = WallLightStatusModel.fromJson(event.devicesIds.first, status.status); emit(WallLightSwitchStatusLoaded(deviceStatus)); @@ -165,32 +145,10 @@ class WallLightSwitchBloc } } - @override - Future close() { - _timer?.cancel(); - return super.close(); - } - - FutureOr _onBatchControl(WallLightSwitchBatchControl event, - Emitter emit) async { - final oldValue = _getValueByCode(event.code); - - _updateLocalValue(event.code, event.value); - - emit(WallLightSwitchStatusLoaded(deviceStatus)); - - await _runDebounce( - deviceId: event.devicesIds, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - isBatch: true, - ); - } - - FutureOr _onFactoryReset( - WallLightFactoryReset event, Emitter emit) async { + Future _onFactoryReset( + WallLightFactoryReset event, + Emitter emit, + ) async { emit(WallLightSwitchLoading()); try { final response = await DevicesManagementApi().factoryReset( @@ -198,12 +156,18 @@ class WallLightSwitchBloc event.deviceId, ); if (!response) { - emit(WallLightSwitchError('Failed')); + emit(WallLightSwitchError('Failed to reset device')); } else { - emit(WallLightSwitchStatusLoaded(deviceStatus)); + add(WallLightSwitchFetchDeviceEvent(event.deviceId)); } } catch (e) { emit(WallLightSwitchError(e.toString())); } } + + void _updateLocalValue(String code, bool value) { + if (code == 'switch_1') { + deviceStatus = deviceStatus.copyWith(switch1: value); + } + } } diff --git a/lib/pages/device_managment/one_gang_switch/factories/wall_light_switch_bloc_factory.dart b/lib/pages/device_managment/one_gang_switch/factories/wall_light_switch_bloc_factory.dart new file mode 100644 index 00000000..fbbe13dc --- /dev/null +++ b/lib/pages/device_managment/one_gang_switch/factories/wall_light_switch_bloc_factory.dart @@ -0,0 +1,18 @@ +import 'package:syncrow_web/pages/device_managment/factories/device_bloc_dependencies_factory.dart'; +import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_bloc.dart'; + +abstract final class WallLightSwitchBlocFactory { + const WallLightSwitchBlocFactory._(); + + static WallLightSwitchBloc create({ + required String deviceId, + }) { + return WallLightSwitchBloc( + deviceId: deviceId, + controlDeviceService: + DeviceBlocDependenciesFactory.createControlDeviceService(), + batchControlDevicesService: + DeviceBlocDependenciesFactory.createBatchControlDevicesService(), + ); + } +} diff --git a/lib/pages/device_managment/one_gang_switch/view/wall_light_batch_control.dart b/lib/pages/device_managment/one_gang_switch/view/wall_light_batch_control.dart index 7094b506..7fe57429 100644 --- a/lib/pages/device_managment/one_gang_switch/view/wall_light_batch_control.dart +++ b/lib/pages/device_managment/one_gang_switch/view/wall_light_batch_control.dart @@ -4,9 +4,9 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_re 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/shared/batch_control/factory_reset.dart'; -// import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; @@ -18,7 +18,7 @@ class WallLightBatchControlView extends StatelessWidget with HelperResponsiveLay @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => WallLightSwitchBloc(deviceId: deviceIds.first) + create: (context) => WallLightSwitchBlocFactory.create(deviceId: deviceIds.first) ..add(WallLightSwitchFetchBatchEvent(deviceIds)), child: BlocBuilder( builder: (context, state) { 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 a9e6ebbb..f1861c55 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 @@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.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/shared/toggle_widget.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; @@ -15,7 +16,7 @@ class WallLightDeviceControl extends StatelessWidget @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => WallLightSwitchBloc(deviceId: deviceId) + create: (context) => WallLightSwitchBlocFactory.create(deviceId: deviceId) ..add(WallLightSwitchFetchDeviceEvent(deviceId)), child: BlocBuilder( builder: (context, state) { From a71a66034c8e35cc4889b130063fed5f9e459855 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 3 Jun 2025 09:49:26 +0300 Subject: [PATCH 46/58] Refactor `ThreeGangGlassSwitchBloc` to integrate new service dependencies and utilize a factory for instantiation. Enhanced event handling methods for improved error management and state updates. --- .../bloc/three_gang_glass_switch_bloc.dart | 244 ++++++++---------- .../bloc/three_gang_glass_switch_event.dart | 17 +- .../three_gang_glass_switch_bloc_factory.dart | 18 ++ ..._gang_glass_switch_batch_control_view.dart | 3 +- .../three_gang_glass_switch_control_view.dart | 3 +- 5 files changed, 143 insertions(+), 142 deletions(-) create mode 100644 lib/pages/device_managment/three_g_glass_switch/factories/three_gang_glass_switch_bloc_factory.dart diff --git a/lib/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_bloc.dart b/lib/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_bloc.dart index 174cd167..766c3163 100644 --- a/lib/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_bloc.dart +++ b/lib/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_bloc.dart @@ -1,11 +1,14 @@ import 'dart:async'; -import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; import 'package:firebase_database/firebase_database.dart'; -import 'package:meta/meta.dart'; +import 'package:flutter/foundation.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/all_devices/models/factory_reset_model.dart'; import 'package:syncrow_web/pages/device_managment/three_g_glass_switch/models/three_gang_glass_switch.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'; part 'three_gang_glass_switch_event.dart'; @@ -13,19 +16,16 @@ part 'three_gang_glass_switch_state.dart'; class ThreeGangGlassSwitchBloc extends Bloc { - ThreeGangGlassStatusModel deviceStatus; - Timer? _timer; + late ThreeGangGlassStatusModel deviceStatus; + final String deviceId; + final ControlDeviceService controlDeviceService; + final BatchControlDevicesService batchControlDevicesService; - ThreeGangGlassSwitchBloc({required String deviceId}) - : deviceStatus = ThreeGangGlassStatusModel( - uuid: deviceId, - switch1: false, - countDown1: 0, - switch2: false, - countDown2: 0, - switch3: false, - countDown3: 0), - super(ThreeGangGlassSwitchInitial()) { + ThreeGangGlassSwitchBloc({ + required this.deviceId, + required this.controlDeviceService, + required this.batchControlDevicesService, + }) : super(ThreeGangGlassSwitchInitial()) { on(_onFetchDeviceStatus); on(_onControl); on(_onBatchControl); @@ -34,188 +34,154 @@ class ThreeGangGlassSwitchBloc on(_onStatusUpdated); } - Future _onFetchDeviceStatus(ThreeGangGlassSwitchFetchDeviceEvent event, - Emitter emit) async { + Future _onFetchDeviceStatus( + ThreeGangGlassSwitchFetchDeviceEvent event, + Emitter emit, + ) async { emit(ThreeGangGlassSwitchLoading()); try { - final status = - await DevicesManagementApi().getDeviceStatus(event.deviceId); + final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); + _listenToChanges(event.deviceId, emit); deviceStatus = ThreeGangGlassStatusModel.fromJson(event.deviceId, status.status); - _listenToChanges(event.deviceId); emit(ThreeGangGlassSwitchStatusLoaded(deviceStatus)); } catch (e) { emit(ThreeGangGlassSwitchError(e.toString())); } } - _listenToChanges(deviceId) { + void _listenToChanges( + String deviceId, + Emitter emit, + ) { try { - DatabaseReference ref = - FirebaseDatabase.instance.ref('device-status/$deviceId'); - Stream stream = ref.onValue; + final ref = FirebaseDatabase.instance.ref('device-status/$deviceId'); + final stream = ref.onValue; stream.listen((DatabaseEvent event) { - Map usersMap = - event.snapshot.value as Map; + final data = event.snapshot.value as Map?; + if (data == null) return; - List statusList = []; - usersMap['status'].forEach((element) { - statusList - .add(Status(code: element['code'], value: element['value'])); - }); - - deviceStatus = ThreeGangGlassStatusModel.fromJson( - usersMap['productUuid'], statusList); - if (!isClosed) { - add(StatusUpdated(deviceStatus)); + final statusList = []; + if (data['status'] != null) { + for (var element in data['status']) { + statusList.add( + Status( + code: element['code'].toString(), + value: element['value'].toString(), + ), + ); + } + } + if (statusList.isNotEmpty) { + final newStatus = ThreeGangGlassStatusModel.fromJson(deviceId, statusList); + if (newStatus != deviceStatus) { + deviceStatus = newStatus; + if (!isClosed) { + add(StatusUpdated(deviceStatus)); + } + } } }); - } catch (_) {} + } catch (e) { + emit(ThreeGangGlassSwitchError('Failed to listen to changes: $e')); + } } void _onStatusUpdated( - StatusUpdated event, Emitter emit) { + StatusUpdated event, + Emitter emit, + ) { + emit(ThreeGangGlassSwitchLoading()); deviceStatus = event.deviceStatus; emit(ThreeGangGlassSwitchStatusLoaded(deviceStatus)); } - Future _onControl(ThreeGangGlassSwitchControl event, - Emitter emit) async { - final oldValue = _getValueByCode(event.code); - + Future _onControl( + ThreeGangGlassSwitchControl event, + Emitter emit, + ) async { + emit(ThreeGangGlassSwitchLoading()); _updateLocalValue(event.code, event.value); emit(ThreeGangGlassSwitchStatusLoaded(deviceStatus)); - await _runDebounce( - deviceId: event.deviceId, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - isBatch: false, - ); + try { + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status(code: event.code, value: event.value), + ); + } catch (e) { + _updateLocalValue(event.code, !event.value); + emit(ThreeGangGlassSwitchError(e.toString())); + } } - Future _onBatchControl(ThreeGangGlassSwitchBatchControl event, - Emitter emit) async { - final oldValue = _getValueByCode(event.code); - + Future _onBatchControl( + ThreeGangGlassSwitchBatchControl event, + Emitter emit, + ) async { + emit(ThreeGangGlassSwitchLoading()); _updateLocalValue(event.code, event.value); emit(ThreeGangGlassSwitchBatchStatusLoaded(deviceStatus)); - await _runDebounce( - deviceId: event.deviceIds, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - isBatch: true, - ); + try { + await batchControlDevicesService.batchControlDevices( + uuids: event.deviceIds, + code: event.code, + value: event.value, + ); + } catch (e) { + _updateLocalValue(event.code, !event.value); + emit(ThreeGangGlassSwitchError(e.toString())); + } } Future _onFetchBatchStatus( - ThreeGangGlassSwitchFetchBatchStatusEvent event, - Emitter emit) async { + ThreeGangGlassSwitchFetchBatchStatusEvent event, + Emitter emit, + ) async { emit(ThreeGangGlassSwitchLoading()); try { - final status = - await DevicesManagementApi().getBatchStatus(event.deviceIds); - deviceStatus = ThreeGangGlassStatusModel.fromJson( - event.deviceIds.first, status.status); + final status = await DevicesManagementApi().getBatchStatus(event.deviceIds); + deviceStatus = + ThreeGangGlassStatusModel.fromJson(event.deviceIds.first, status.status); emit(ThreeGangGlassSwitchBatchStatusLoaded(deviceStatus)); } catch (e) { emit(ThreeGangGlassSwitchError(e.toString())); } } - Future _onFactoryReset(ThreeGangGlassFactoryReset event, - Emitter emit) async { + Future _onFactoryReset( + ThreeGangGlassFactoryReset event, + Emitter emit, + ) async { emit(ThreeGangGlassSwitchLoading()); try { - final response = await DevicesManagementApi() - .factoryReset(event.factoryReset, event.deviceId); + final response = await DevicesManagementApi().factoryReset( + event.factoryReset, + event.deviceId, + ); if (!response) { - emit(ThreeGangGlassSwitchError('Failed')); + emit(ThreeGangGlassSwitchError('Failed to reset device')); } else { - emit(ThreeGangGlassSwitchStatusLoaded(deviceStatus)); + add(ThreeGangGlassSwitchFetchDeviceEvent(event.deviceId)); } } catch (e) { emit(ThreeGangGlassSwitchError(e.toString())); } } - Future _runDebounce({ - required dynamic deviceId, - required String code, - required bool value, - required bool oldValue, - required Emitter emit, - required bool isBatch, - }) async { - late String id; - if (deviceId is List) { - id = deviceId.first; - } else { - id = deviceId; - } - - if (_timer != null) { - _timer!.cancel(); - } - - _timer = Timer(const Duration(milliseconds: 500), () async { - try { - late bool response; - if (isBatch) { - response = await DevicesManagementApi() - .deviceBatchControl(deviceId, code, value); - } else { - response = await DevicesManagementApi() - .deviceControl(deviceId, Status(code: code, value: value)); - } - - if (!response) { - _revertValueAndEmit(id, code, oldValue, emit); - } - } catch (e) { - _revertValueAndEmit(id, code, oldValue, emit); - } - }); - } - - void _revertValueAndEmit(String deviceId, String code, bool oldValue, - Emitter emit) { - _updateLocalValue(code, oldValue); - emit(ThreeGangGlassSwitchStatusLoaded(deviceStatus)); - } - void _updateLocalValue(String code, bool value) { - if (code == 'switch_1') { - deviceStatus = deviceStatus.copyWith(switch1: value); - } else if (code == 'switch_2') { - deviceStatus = deviceStatus.copyWith(switch2: value); - } else if (code == 'switch_3') { - deviceStatus = deviceStatus.copyWith(switch3: value); - } - } - - bool _getValueByCode(String code) { switch (code) { case 'switch_1': - return deviceStatus.switch1; + deviceStatus = deviceStatus.copyWith(switch1: value); + break; case 'switch_2': - return deviceStatus.switch2; + deviceStatus = deviceStatus.copyWith(switch2: value); + break; case 'switch_3': - return deviceStatus.switch3; - default: - return false; + deviceStatus = deviceStatus.copyWith(switch3: value); + break; } } - - @override - Future close() { - _timer?.cancel(); - return super.close(); - } } diff --git a/lib/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_event.dart b/lib/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_event.dart index 82b93fba..991de938 100644 --- a/lib/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_event.dart +++ b/lib/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_event.dart @@ -1,7 +1,10 @@ part of 'three_gang_glass_switch_bloc.dart'; @immutable -abstract class ThreeGangGlassSwitchEvent {} +abstract class ThreeGangGlassSwitchEvent extends Equatable { + @override + List get props => []; +} class ThreeGangGlassSwitchFetchDeviceEvent extends ThreeGangGlassSwitchEvent { final String deviceId; @@ -19,6 +22,9 @@ class ThreeGangGlassSwitchControl extends ThreeGangGlassSwitchEvent { required this.code, required this.value, }); + + @override + List get props => [deviceId, code, value]; } class ThreeGangGlassSwitchBatchControl extends ThreeGangGlassSwitchEvent { @@ -31,6 +37,9 @@ class ThreeGangGlassSwitchBatchControl extends ThreeGangGlassSwitchEvent { required this.code, required this.value, }); + + @override + List get props => [deviceIds, code, value]; } class ThreeGangGlassSwitchFetchBatchStatusEvent @@ -38,6 +47,9 @@ class ThreeGangGlassSwitchFetchBatchStatusEvent final List deviceIds; ThreeGangGlassSwitchFetchBatchStatusEvent(this.deviceIds); + + @override + List get props => [deviceIds]; } class ThreeGangGlassFactoryReset extends ThreeGangGlassSwitchEvent { @@ -48,6 +60,9 @@ class ThreeGangGlassFactoryReset extends ThreeGangGlassSwitchEvent { required this.deviceId, required this.factoryReset, }); + + @override + List get props => [deviceId, factoryReset]; } class StatusUpdated extends ThreeGangGlassSwitchEvent { diff --git a/lib/pages/device_managment/three_g_glass_switch/factories/three_gang_glass_switch_bloc_factory.dart b/lib/pages/device_managment/three_g_glass_switch/factories/three_gang_glass_switch_bloc_factory.dart new file mode 100644 index 00000000..9f66773a --- /dev/null +++ b/lib/pages/device_managment/three_g_glass_switch/factories/three_gang_glass_switch_bloc_factory.dart @@ -0,0 +1,18 @@ +import 'package:syncrow_web/pages/device_managment/factories/device_bloc_dependencies_factory.dart'; +import 'package:syncrow_web/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_bloc.dart'; + +abstract final class ThreeGangGlassSwitchBlocFactory { + const ThreeGangGlassSwitchBlocFactory._(); + + static ThreeGangGlassSwitchBloc create({ + required String deviceId, + }) { + return ThreeGangGlassSwitchBloc( + deviceId: deviceId, + controlDeviceService: + DeviceBlocDependenciesFactory.createControlDeviceService(), + batchControlDevicesService: + DeviceBlocDependenciesFactory.createBatchControlDevicesService(), + ); + } +} diff --git a/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_batch_control_view.dart b/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_batch_control_view.dart index 071d6ca0..93fbe53e 100644 --- a/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_batch_control_view.dart +++ b/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_batch_control_view.dart @@ -5,6 +5,7 @@ import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_ // import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.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/pages/device_managment/three_g_glass_switch/models/three_gang_glass_switch.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; @@ -16,7 +17,7 @@ class ThreeGangGlassSwitchBatchControlView extends StatelessWidget with HelperRe @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => ThreeGangGlassSwitchBloc(deviceId: deviceIds.first) + create: (context) => ThreeGangGlassSwitchBlocFactory.create(deviceId: deviceIds.first) ..add(ThreeGangGlassSwitchFetchBatchStatusEvent(deviceIds)), child: BlocBuilder( builder: (context, state) { 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 433e5408..21a81df0 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 @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.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/constants/assets.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; @@ -16,7 +17,7 @@ class ThreeGangGlassSwitchControlView extends StatelessWidget with HelperRespons Widget build(BuildContext context) { return BlocProvider( create: (context) => - ThreeGangGlassSwitchBloc(deviceId: deviceId)..add(ThreeGangGlassSwitchFetchDeviceEvent(deviceId)), + ThreeGangGlassSwitchBlocFactory.create(deviceId: deviceId)..add(ThreeGangGlassSwitchFetchDeviceEvent(deviceId)), child: BlocBuilder( builder: (context, state) { if (state is ThreeGangGlassSwitchLoading) { From f58ddf76dad9ce1ea4db7431dcdc4690d839f1d4 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 3 Jun 2025 10:19:10 +0300 Subject: [PATCH 47/58] Refactor `LivingRoomBloc` to integrate new service dependencies and utilize a factory for instantiation. Enhanced event handling methods for improved error management and state updates, including real-time status listening from Firebase. --- .../bloc/living_room_bloc.dart | 253 +++++++----------- .../factories/living_room_bloc_factory.dart | 18 ++ .../view/living_room_batch_controls.dart | 3 +- .../view/living_room_device_control.dart | 3 +- 4 files changed, 121 insertions(+), 156 deletions(-) create mode 100644 lib/pages/device_managment/three_gang_switch/factories/living_room_bloc_factory.dart diff --git a/lib/pages/device_managment/three_gang_switch/bloc/living_room_bloc.dart b/lib/pages/device_managment/three_gang_switch/bloc/living_room_bloc.dart index a7a03a7f..bec1314c 100644 --- a/lib/pages/device_managment/three_gang_switch/bloc/living_room_bloc.dart +++ b/lib/pages/device_managment/three_gang_switch/bloc/living_room_bloc.dart @@ -1,12 +1,14 @@ -// ignore_for_file: invalid_use_of_visible_for_testing_member - import 'dart:async'; -import 'package:bloc/bloc.dart'; +import 'dart:developer'; + import 'package:equatable/equatable.dart'; import 'package:firebase_database/firebase_database.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/all_devices/models/factory_reset_model.dart'; import 'package:syncrow_web/pages/device_managment/three_gang_switch/models/living_room_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'; part 'living_room_event.dart'; @@ -15,9 +17,14 @@ part 'living_room_state.dart'; class LivingRoomBloc extends Bloc { late LivingRoomStatusModel deviceStatus; final String deviceId; - Timer? _timer; + final ControlDeviceService controlDeviceService; + final BatchControlDevicesService batchControlDevicesService; - LivingRoomBloc({required this.deviceId}) : super(LivingRoomInitial()) { + LivingRoomBloc({ + required this.deviceId, + required this.controlDeviceService, + required this.batchControlDevicesService, + }) : super(LivingRoomInitial()) { on(_onFetchDeviceStatus); on(_livingRoomControl); on(_livingRoomBatchControl); @@ -26,156 +33,108 @@ class LivingRoomBloc extends Bloc { on(_onStatusUpdated); } - FutureOr _onFetchDeviceStatus(LivingRoomFetchDeviceStatusEvent event, - Emitter emit) async { + Future _onFetchDeviceStatus( + LivingRoomFetchDeviceStatusEvent event, + Emitter emit, + ) async { emit(LivingRoomDeviceStatusLoading()); try { - final status = - await DevicesManagementApi().getDeviceStatus(event.deviceId); - deviceStatus = - LivingRoomStatusModel.fromJson(event.deviceId, status.status); + final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); _listenToChanges(deviceId); + deviceStatus = LivingRoomStatusModel.fromJson(event.deviceId, status.status); emit(LivingRoomDeviceStatusLoaded(deviceStatus)); } catch (e) { emit(LivingRoomDeviceManagementError(e.toString())); } } - FutureOr _livingRoomControl( - LivingRoomControl event, Emitter emit) async { - final oldValue = _getValueByCode(event.code); + void _listenToChanges(String deviceId) { + try { + final ref = FirebaseDatabase.instance.ref('device-status/$deviceId'); + ref.onValue.listen((event) { + final eventsMap = event.snapshot.value as Map; + List statusList = []; + eventsMap['status'].forEach((element) { + statusList.add( + Status(code: element['code'], value: element['value']), + ); + }); + + deviceStatus = LivingRoomStatusModel.fromJson(deviceId, statusList); + add(StatusUpdated(deviceStatus)); + }); + } catch (_) { + log('Error listening to changes'); + } + } + + void _onStatusUpdated( + StatusUpdated event, + Emitter emit, + ) { + deviceStatus = event.deviceStatus; + emit(LivingRoomDeviceStatusLoaded(deviceStatus)); + } + + Future _livingRoomControl( + LivingRoomControl event, + Emitter emit, + ) async { + emit(LivingRoomDeviceStatusLoading()); _updateLocalValue(event.code, event.value); - emit(LivingRoomDeviceStatusLoaded(deviceStatus)); - await _runDebounce( - deviceId: event.deviceId, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - isBatch: false, - ); + try { + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status(code: event.code, value: event.value), + ); + } catch (e) { + _updateLocalValue(event.code, !event.value); + emit(LivingRoomDeviceManagementError(e.toString())); + } } - Future _runDebounce({ - required dynamic deviceId, - required String code, - required dynamic value, - required dynamic oldValue, - required Emitter emit, - required bool isBatch, - }) async { - late String id; - - if (deviceId is List) { - id = deviceId.first; - } else { - id = deviceId; - } - - if (_timer != null) { - _timer!.cancel(); - } - _timer = Timer(const Duration(seconds: 1), () async { - try { - late bool response; - if (isBatch) { - response = await DevicesManagementApi() - .deviceBatchControl(deviceId, code, value); - } else { - response = await DevicesManagementApi() - .deviceControl(deviceId, Status(code: code, value: value)); - } - if (!response) { - _revertValueAndEmit(id, code, oldValue, emit); - } - } catch (e) { - _revertValueAndEmit(id, code, oldValue, emit); - } - }); - } - - void _revertValueAndEmit(String deviceId, String code, dynamic oldValue, - Emitter emit) { - _updateLocalValue(code, oldValue); + Future _livingRoomBatchControl( + LivingRoomBatchControl event, + Emitter emit, + ) async { + emit(LivingRoomDeviceStatusLoading()); + _updateLocalValue(event.code, event.value); emit(LivingRoomDeviceStatusLoaded(deviceStatus)); - } - void _updateLocalValue(String code, dynamic value) { - switch (code) { - case 'switch_1': - if (value is bool) { - deviceStatus = deviceStatus.copyWith(switch1: value); - } - break; - case 'switch_2': - if (value is bool) { - deviceStatus = deviceStatus.copyWith(switch2: value); - } - break; - case 'switch_3': - if (value is bool) { - deviceStatus = deviceStatus.copyWith(switch3: value); - } - break; - default: - break; - } - emit(LivingRoomDeviceStatusLoaded(deviceStatus)); - } - - dynamic _getValueByCode(String code) { - switch (code) { - case 'switch_1': - return deviceStatus.switch1; - case 'switch_2': - return deviceStatus.switch2; - case 'switch_3': - return deviceStatus.switch3; - default: - return null; + try { + await batchControlDevicesService.batchControlDevices( + uuids: event.devicesIds, + code: event.code, + value: event.value, + ); + } catch (e) { + _updateLocalValue(event.code, !event.value); + emit(LivingRoomDeviceManagementError(e.toString())); } } - FutureOr _livingRoomFetchBatchControl( - LivingRoomFetchBatchEvent event, Emitter emit) async { + Future _livingRoomFetchBatchControl( + LivingRoomFetchBatchEvent event, + Emitter emit, + ) async { emit(LivingRoomDeviceStatusLoading()); try { - final status = - await DevicesManagementApi().getBatchStatus(event.devicesIds); + final status = await DevicesManagementApi().getBatchStatus(event.devicesIds); deviceStatus = LivingRoomStatusModel.fromJson(event.devicesIds.first, status.status); - // for (var deviceId in event.devicesIds) { - // _listenToChanges(deviceId); - // } emit(LivingRoomDeviceStatusLoaded(deviceStatus)); } catch (e) { emit(LivingRoomDeviceManagementError(e.toString())); } } - FutureOr _livingRoomBatchControl( - LivingRoomBatchControl event, Emitter emit) async { - final oldValue = _getValueByCode(event.code); - - _updateLocalValue(event.code, event.value); - - emit(LivingRoomDeviceStatusLoaded(deviceStatus)); - - await _runDebounce( - deviceId: event.devicesIds, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - isBatch: true, - ); - } - - FutureOr _livingRoomFactoryReset( - LivingRoomFactoryResetEvent event, Emitter emit) async { + Future _livingRoomFactoryReset( + LivingRoomFactoryResetEvent event, + Emitter emit, + ) async { emit(LivingRoomDeviceStatusLoading()); try { final response = await DevicesManagementApi().factoryReset( @@ -183,42 +142,28 @@ class LivingRoomBloc extends Bloc { event.uuid, ); if (!response) { - emit(const LivingRoomDeviceManagementError('Failed')); + emit(const LivingRoomDeviceManagementError('Failed to reset device')); } else { - emit(LivingRoomDeviceStatusLoaded(deviceStatus)); + add(LivingRoomFetchDeviceStatusEvent(event.uuid)); } } catch (e) { emit(LivingRoomDeviceManagementError(e.toString())); } } - _listenToChanges(deviceId) { - try { - DatabaseReference ref = - FirebaseDatabase.instance.ref('device-status/$deviceId'); - Stream stream = ref.onValue; + void _updateLocalValue(String code, dynamic value) { + if (value is! bool) return; - 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 = - LivingRoomStatusModel.fromJson(usersMap['productUuid'], statusList); - if (!isClosed) { - add(StatusUpdated(deviceStatus)); - } - }); - } catch (_) {} - } - - void _onStatusUpdated(StatusUpdated event, Emitter emit) { - deviceStatus = event.deviceStatus; - emit(LivingRoomDeviceStatusLoaded(deviceStatus)); + switch (code) { + case 'switch_1': + deviceStatus = deviceStatus.copyWith(switch1: value); + break; + case 'switch_2': + deviceStatus = deviceStatus.copyWith(switch2: value); + break; + case 'switch_3': + deviceStatus = deviceStatus.copyWith(switch3: value); + break; + } } } diff --git a/lib/pages/device_managment/three_gang_switch/factories/living_room_bloc_factory.dart b/lib/pages/device_managment/three_gang_switch/factories/living_room_bloc_factory.dart new file mode 100644 index 00000000..94c2b72f --- /dev/null +++ b/lib/pages/device_managment/three_gang_switch/factories/living_room_bloc_factory.dart @@ -0,0 +1,18 @@ +import 'package:syncrow_web/pages/device_managment/factories/device_bloc_dependencies_factory.dart'; +import 'package:syncrow_web/pages/device_managment/three_gang_switch/bloc/living_room_bloc.dart'; + +abstract final class LivingRoomBlocFactory { + const LivingRoomBlocFactory._(); + + static LivingRoomBloc create({ + required String deviceId, + }) { + return LivingRoomBloc( + deviceId: deviceId, + controlDeviceService: + DeviceBlocDependenciesFactory.createControlDeviceService(), + batchControlDevicesService: + DeviceBlocDependenciesFactory.createBatchControlDevicesService(), + ); + } +} diff --git a/lib/pages/device_managment/three_gang_switch/view/living_room_batch_controls.dart b/lib/pages/device_managment/three_gang_switch/view/living_room_batch_controls.dart index 97c25287..0b1a2f06 100644 --- a/lib/pages/device_managment/three_gang_switch/view/living_room_batch_controls.dart +++ b/lib/pages/device_managment/three_gang_switch/view/living_room_batch_controls.dart @@ -4,6 +4,7 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_re import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; // import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.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/helpers/responsice_layout_helper/responsive_layout_helper.dart'; @@ -17,7 +18,7 @@ class LivingRoomBatchControlsView extends StatelessWidget with HelperResponsiveL Widget build(BuildContext context) { return BlocProvider( create: (context) => - LivingRoomBloc(deviceId: deviceIds.first)..add(LivingRoomFetchBatchEvent(deviceIds)), + LivingRoomBlocFactory.create(deviceId: deviceIds.first)..add(LivingRoomFetchBatchEvent(deviceIds)), child: BlocBuilder( builder: (context, state) { if (state is LivingRoomDeviceStatusLoading) { 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 b7f97776..731b354c 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,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.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/helpers/responsice_layout_helper/responsive_layout_helper.dart'; @@ -14,7 +15,7 @@ class LivingRoomDeviceControlsView extends StatelessWidget @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => LivingRoomBloc(deviceId: deviceId) + create: (context) => LivingRoomBlocFactory.create(deviceId: deviceId) ..add(LivingRoomFetchDeviceStatusEvent(deviceId)), child: BlocBuilder( builder: (context, state) { From 88a76073954a97a4fcf3be1746b14408677a32bd Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 3 Jun 2025 10:33:33 +0300 Subject: [PATCH 48/58] Refactor `TwoGangGlassSwitchBloc` to integrate new service dependencies and utilize a factory for instantiation. Enhanced event handling methods for improved error management and state updates, including real-time status listening from Firebase. --- .../bloc/two_gang_glass_switch_bloc.dart | 260 +++++++----------- .../bloc/two_gang_glass_switch_event.dart | 34 ++- .../two_gang_glass_switch_bloc_factory.dart | 18 ++ ..._gang_glass_switch_batch_control_view.dart | 3 +- .../two_gang_glass_switch_control_view.dart | 3 +- 5 files changed, 142 insertions(+), 176 deletions(-) create mode 100644 lib/pages/device_managment/two_g_glass_switch/factories/two_gang_glass_switch_bloc_factory.dart diff --git a/lib/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_bloc.dart b/lib/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_bloc.dart index 406821da..8f82c198 100644 --- a/lib/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_bloc.dart +++ b/lib/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_bloc.dart @@ -1,26 +1,33 @@ import 'dart:async'; +import 'dart:developer'; + import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; import 'package:firebase_database/firebase_database.dart'; -import 'package:meta/meta.dart'; +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/all_devices/models/factory_reset_model.dart'; import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/models/two_gang_glass_status_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'; + part 'two_gang_glass_switch_event.dart'; part 'two_gang_glass_switch_state.dart'; class TwoGangGlassSwitchBloc extends Bloc { - TwoGangGlassStatusModel deviceStatus; - Timer? _timer; - TwoGangGlassSwitchBloc({required String deviceId}) - : deviceStatus = TwoGangGlassStatusModel( - uuid: deviceId, - switch1: false, - countDown1: 0, - switch2: false, - countDown2: 0), - super(TwoGangGlassSwitchInitial()) { + final String deviceId; + final ControlDeviceService controlDeviceService; + final BatchControlDevicesService batchControlDevicesService; + + late TwoGangGlassStatusModel deviceStatus; + + TwoGangGlassSwitchBloc({ + required this.deviceId, + required this.controlDeviceService, + required this.batchControlDevicesService, + }) : super(TwoGangGlassSwitchInitial()) { on(_onFetchDeviceStatus); on(_onControl); on(_onBatchControl); @@ -29,14 +36,14 @@ class TwoGangGlassSwitchBloc on(_onStatusUpdated); } - Future _onFetchDeviceStatus(TwoGangGlassSwitchFetchDeviceEvent event, - Emitter emit) async { + Future _onFetchDeviceStatus( + TwoGangGlassSwitchFetchDeviceEvent event, + Emitter emit, + ) async { emit(TwoGangGlassSwitchLoading()); try { - final status = - await DevicesManagementApi().getDeviceStatus(event.deviceId); - deviceStatus = - TwoGangGlassStatusModel.fromJson(event.deviceId, status.status); + final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); + deviceStatus = TwoGangGlassStatusModel.fromJson(event.deviceId, status.status); _listenToChanges(event.deviceId); emit(TwoGangGlassSwitchStatusLoaded(deviceStatus)); } catch (e) { @@ -46,200 +53,121 @@ class TwoGangGlassSwitchBloc void _listenToChanges(String deviceId) { try { - DatabaseReference ref = - FirebaseDatabase.instance.ref('device-status/$deviceId'); - ref.onValue.listen((DatabaseEvent event) { - if (event.snapshot.value == null) return; + final ref = FirebaseDatabase.instance.ref( + 'device-status/$deviceId', + ); + + ref.onValue.listen((event) { + final eventsMap = event.snapshot.value as Map; - Map data = - event.snapshot.value as Map; List statusList = []; - - data['status'].forEach((element) { - statusList - .add(Status(code: element['code'], value: element['value'])); + eventsMap['status'].forEach((element) { + statusList.add(Status(code: element['code'], value: element['value'])); }); - // Parse the new status and add the event - final updatedStatus = - TwoGangGlassStatusModel.fromJson(data['productUuid'], statusList); - if (!isClosed) { - add(StatusUpdated(updatedStatus)); - } + deviceStatus = TwoGangGlassStatusModel.fromJson(deviceId, statusList); + add(StatusUpdated(deviceStatus)); }); - } catch (e) { - // Handle errors and emit an error state if necessary - if (!isClosed) { - // add(TwoGangGlassSwitchError('Error listening to updates: $e')); - } + } catch (_) { + log( + 'Error listening to changes', + name: 'TwoGangGlassSwitchBloc._listenToChanges', + ); } } - Future _onControl(TwoGangGlassSwitchControl event, - Emitter emit) async { - final oldValue = _getValueByCode(event.code); - + Future _onControl( + TwoGangGlassSwitchControl event, + Emitter emit, + ) async { + emit(TwoGangGlassSwitchLoading()); _updateLocalValue(event.code, event.value); emit(TwoGangGlassSwitchStatusLoaded(deviceStatus)); - await _runDebounce( - deviceId: event.deviceId, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - isBatch: false, - ); + try { + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status(code: event.code, value: event.value), + ); + } catch (e) { + _updateLocalValue(event.code, !event.value); + emit(TwoGangGlassSwitchError(e.toString())); + } } - Future _onBatchControl(TwoGangGlassSwitchBatchControl event, - Emitter emit) async { - final oldValue = _getValueByCode(event.code); - + Future _onBatchControl( + TwoGangGlassSwitchBatchControl event, + Emitter emit, + ) async { + emit(TwoGangGlassSwitchLoading()); _updateLocalValue(event.code, event.value); emit(TwoGangGlassSwitchBatchStatusLoaded(deviceStatus)); - await _runDebounce( - deviceId: event.deviceIds, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - isBatch: true, - ); + try { + await batchControlDevicesService.batchControlDevices( + uuids: event.deviceIds, + code: event.code, + value: event.value, + ); + } catch (e) { + _updateLocalValue(event.code, !event.value); + emit(TwoGangGlassSwitchError(e.toString())); + } } Future _onFetchBatchStatus( - TwoGangGlassSwitchFetchBatchStatusEvent event, - Emitter emit) async { + TwoGangGlassSwitchFetchBatchStatusEvent event, + Emitter emit, + ) async { emit(TwoGangGlassSwitchLoading()); try { - final status = - await DevicesManagementApi().getBatchStatus(event.deviceIds); + final status = await DevicesManagementApi().getBatchStatus(event.deviceIds); deviceStatus = TwoGangGlassStatusModel.fromJson( - event.deviceIds.first, status.status); + event.deviceIds.first, + status.status, + ); emit(TwoGangGlassSwitchBatchStatusLoaded(deviceStatus)); } catch (e) { emit(TwoGangGlassSwitchError(e.toString())); } } - Future _onFactoryReset(TwoGangGlassFactoryReset event, - Emitter emit) async { + Future _onFactoryReset( + TwoGangGlassFactoryReset event, + Emitter emit, + ) async { emit(TwoGangGlassSwitchLoading()); try { - final response = await DevicesManagementApi() - .factoryReset(event.factoryReset, event.deviceId); + final response = await DevicesManagementApi().factoryReset( + event.factoryReset, + event.deviceId, + ); if (!response) { - emit(TwoGangGlassSwitchError('Failed')); + emit(TwoGangGlassSwitchError('Failed to reset device')); } else { - emit(TwoGangGlassSwitchStatusLoaded(deviceStatus)); + add(TwoGangGlassSwitchFetchDeviceEvent(event.deviceId)); } } catch (e) { emit(TwoGangGlassSwitchError(e.toString())); } } - Future _runDebounce({ - required dynamic deviceId, - required String code, - required bool value, - required bool oldValue, - required Emitter emit, - required bool isBatch, - }) async { - late String id; - if (deviceId is List) { - id = deviceId.first; - } else { - id = deviceId; - } - - if (_timer != null) { - _timer!.cancel(); - } - - _timer = Timer(const Duration(milliseconds: 500), () async { - try { - late bool response; - if (isBatch) { - response = await DevicesManagementApi() - .deviceBatchControl(deviceId, code, value); - } else { - response = await DevicesManagementApi() - .deviceControl(deviceId, Status(code: code, value: value)); - } - - if (!response) { - _revertValueAndEmit(id, code, oldValue, emit); - } - } catch (e) { - _revertValueAndEmit(id, code, oldValue, emit); - } - }); - } - - void _revertValueAndEmit(String deviceId, String code, bool oldValue, - Emitter emit) { - _updateLocalValue(code, oldValue); + void _onStatusUpdated( + StatusUpdated event, + Emitter emit, + ) { + deviceStatus = event.deviceStatus; emit(TwoGangGlassSwitchStatusLoaded(deviceStatus)); } void _updateLocalValue(String code, bool value) { - if (code == 'switch_1') { - deviceStatus = deviceStatus.copyWith(switch1: value); - } else if (code == 'switch_2') { - deviceStatus = deviceStatus.copyWith(switch2: value); - } - } - - bool _getValueByCode(String code) { switch (code) { case 'switch_1': - return deviceStatus.switch1; + deviceStatus = deviceStatus.copyWith(switch1: value); + break; case 'switch_2': - return deviceStatus.switch2; - default: - return false; + deviceStatus = deviceStatus.copyWith(switch2: value); + break; } } - - @override - Future close() { - _timer?.cancel(); - return super.close(); - } - - // _listenToChanges(deviceId) { - // try { - // DatabaseReference ref = - // FirebaseDatabase.instance.ref('device-status/$deviceId'); - // Stream stream = ref.onValue; - - // 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 = TwoGangGlassStatusModel.fromJson( - // usersMap['productUuid'], statusList); - // if (!isClosed) { - // add(StatusUpdated(deviceStatus)); - // } - // }); - // } catch (_) {} - // } - - void _onStatusUpdated( - StatusUpdated event, Emitter emit) { - // Update the local deviceStatus with the new status from the event - deviceStatus = event.deviceStatus; - // Emit the new state with the updated status - emit(TwoGangGlassSwitchStatusLoaded(deviceStatus)); - } } diff --git a/lib/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_event.dart b/lib/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_event.dart index 02b61bd0..46444cce 100644 --- a/lib/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_event.dart +++ b/lib/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_event.dart @@ -1,12 +1,17 @@ part of 'two_gang_glass_switch_bloc.dart'; @immutable -abstract class TwoGangGlassSwitchEvent {} +abstract class TwoGangGlassSwitchEvent extends Equatable { + const TwoGangGlassSwitchEvent(); +} class TwoGangGlassSwitchFetchDeviceEvent extends TwoGangGlassSwitchEvent { final String deviceId; - TwoGangGlassSwitchFetchDeviceEvent(this.deviceId); + const TwoGangGlassSwitchFetchDeviceEvent(this.deviceId); + + @override + List get props => [deviceId]; } class TwoGangGlassSwitchControl extends TwoGangGlassSwitchEvent { @@ -14,11 +19,14 @@ class TwoGangGlassSwitchControl extends TwoGangGlassSwitchEvent { final String code; final bool value; - TwoGangGlassSwitchControl({ + const TwoGangGlassSwitchControl({ required this.deviceId, required this.code, required this.value, }); + + @override + List get props => [deviceId, code, value]; } class TwoGangGlassSwitchBatchControl extends TwoGangGlassSwitchEvent { @@ -26,33 +34,43 @@ class TwoGangGlassSwitchBatchControl extends TwoGangGlassSwitchEvent { final String code; final bool value; - TwoGangGlassSwitchBatchControl({ + const TwoGangGlassSwitchBatchControl({ required this.deviceIds, required this.code, required this.value, }); + + @override + List get props => [deviceIds, code, value]; } class TwoGangGlassSwitchFetchBatchStatusEvent extends TwoGangGlassSwitchEvent { final List deviceIds; - TwoGangGlassSwitchFetchBatchStatusEvent(this.deviceIds); + const TwoGangGlassSwitchFetchBatchStatusEvent(this.deviceIds); + + @override + List get props => [deviceIds]; } class TwoGangGlassFactoryReset extends TwoGangGlassSwitchEvent { final String deviceId; final FactoryResetModel factoryReset; - TwoGangGlassFactoryReset({ + const TwoGangGlassFactoryReset({ required this.deviceId, required this.factoryReset, }); + + @override + List get props => [deviceId, factoryReset]; } class StatusUpdated extends TwoGangGlassSwitchEvent { final TwoGangGlassStatusModel deviceStatus; - StatusUpdated(this.deviceStatus); + + const StatusUpdated(this.deviceStatus); + @override List get props => [deviceStatus]; } - diff --git a/lib/pages/device_managment/two_g_glass_switch/factories/two_gang_glass_switch_bloc_factory.dart b/lib/pages/device_managment/two_g_glass_switch/factories/two_gang_glass_switch_bloc_factory.dart new file mode 100644 index 00000000..bd832d8f --- /dev/null +++ b/lib/pages/device_managment/two_g_glass_switch/factories/two_gang_glass_switch_bloc_factory.dart @@ -0,0 +1,18 @@ +import 'package:syncrow_web/pages/device_managment/factories/device_bloc_dependencies_factory.dart'; +import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_bloc.dart'; + +abstract final class TwoGangGlassSwitchBlocFactory { + const TwoGangGlassSwitchBlocFactory._(); + + static TwoGangGlassSwitchBloc create({ + required String deviceId, + }) { + return TwoGangGlassSwitchBloc( + deviceId: deviceId, + controlDeviceService: + DeviceBlocDependenciesFactory.createControlDeviceService(), + batchControlDevicesService: + DeviceBlocDependenciesFactory.createBatchControlDevicesService(), + ); + } +} diff --git a/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_batch_control_view.dart b/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_batch_control_view.dart index c84c1d07..9d120ad6 100644 --- a/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_batch_control_view.dart +++ b/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_batch_control_view.dart @@ -5,6 +5,7 @@ import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_ // import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.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/helpers/responsice_layout_helper/responsive_layout_helper.dart'; @@ -16,7 +17,7 @@ class TwoGangGlassSwitchBatchControlView extends StatelessWidget with HelperResp @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => TwoGangGlassSwitchBloc(deviceId: deviceIds.first) + create: (context) => TwoGangGlassSwitchBlocFactory.create(deviceId: deviceIds.first) ..add(TwoGangGlassSwitchFetchBatchStatusEvent(deviceIds)), child: BlocBuilder( builder: (context, state) { 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 cca794e9..575deeac 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 @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.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/constants/assets.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; @@ -15,7 +16,7 @@ class TwoGangGlassSwitchControlView extends StatelessWidget @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => TwoGangGlassSwitchBloc(deviceId: deviceId) + create: (context) => TwoGangGlassSwitchBlocFactory.create(deviceId: deviceId) ..add(TwoGangGlassSwitchFetchDeviceEvent(deviceId)), child: BlocBuilder( builder: (context, state) { From 3c983653388b7715b525d389122f085bfeca15b4 Mon Sep 17 00:00:00 2001 From: mohammad Date: Tue, 3 Jun 2025 10:44:34 +0300 Subject: [PATCH 49/58] change the validation from static code to backend --- lib/pages/auth/bloc/auth_bloc.dart | 29 +-- .../bloc/routine_bloc/routine_bloc.dart | 212 ++++++++++++------ .../space_tree/view/space_tree_view.dart | 189 +++++++++------- lib/services/api/api_exception.dart | 10 + lib/services/auth_api.dart | 79 ++++--- lib/services/routines_api.dart | 40 ++-- 6 files changed, 346 insertions(+), 213 deletions(-) create mode 100644 lib/services/api/api_exception.dart diff --git a/lib/pages/auth/bloc/auth_bloc.dart b/lib/pages/auth/bloc/auth_bloc.dart index e5de46c9..58950089 100644 --- a/lib/pages/auth/bloc/auth_bloc.dart +++ b/lib/pages/auth/bloc/auth_bloc.dart @@ -13,6 +13,7 @@ import 'package:syncrow_web/pages/common/bloc/project_manager.dart'; import 'package:syncrow_web/pages/home/bloc/home_bloc.dart'; import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart'; import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; import 'package:syncrow_web/services/auth_api.dart'; import 'package:syncrow_web/utils/constants/strings_manager.dart'; import 'package:syncrow_web/utils/helpers/shared_preferences_helper.dart'; @@ -99,7 +100,8 @@ class AuthBloc extends Bloc { emit(const TimerState(isButtonEnabled: true, remainingTime: 0)); } - Future changePassword(ChangePasswordEvent event, Emitter emit) async { +Future changePassword( + ChangePasswordEvent event, Emitter emit) async { emit(LoadingForgetState()); try { var response = await AuthenticationAPI.verifyOtp( @@ -113,14 +115,14 @@ class AuthBloc extends Bloc { emit(const TimerState(isButtonEnabled: true, remainingTime: 0)); emit(SuccessForgetState()); } - } on DioException catch (e) { - final errorData = e.response!.data; - String errorMessage = errorData['error']['message'] ?? 'something went wrong'; + } on APIException catch (e) { + final errorMessage = e.message; validate = errorMessage; emit(AuthInitialState()); } } + String? validateCode(String? value) { if (value == null || value.isEmpty) { return 'Code is required'; @@ -149,6 +151,7 @@ class AuthBloc extends Bloc { static UserModel? user; bool showValidationMessage = false; + void _login(LoginButtonPressed event, Emitter emit) async { emit(AuthLoading()); if (isChecked) { @@ -165,21 +168,20 @@ class AuthBloc extends Bloc { password: event.password, ), ); - } on DioException catch (e) { - final errorData = e.response!.data; - String errorMessage = errorData['error']['message']; - if (errorMessage == "Access denied for web platform") { - validate = errorMessage; - } else { - validate = 'Invalid Credentials!'; - } + } on APIException catch (e) { + validate = e.message; + emit(LoginInitial()); + return; + } catch (e) { + validate = 'Something went wrong'; emit(LoginInitial()); return; } if (token.accessTokenIsNotEmpty) { FlutterSecureStorage storage = const FlutterSecureStorage(); - await storage.write(key: Token.loginAccessTokenKey, value: token.accessToken); + await storage.write( + key: Token.loginAccessTokenKey, value: token.accessToken); const FlutterSecureStorage().write( key: UserModel.userUuidKey, value: Token.decodeToken(token.accessToken)['uuid'].toString()); @@ -195,6 +197,7 @@ class AuthBloc extends Bloc { } } + checkBoxToggle( CheckBoxEvent event, Emitter emit, diff --git a/lib/pages/routines/bloc/routine_bloc/routine_bloc.dart b/lib/pages/routines/bloc/routine_bloc/routine_bloc.dart index 760702d4..ca8aac06 100644 --- a/lib/pages/routines/bloc/routine_bloc/routine_bloc.dart +++ b/lib/pages/routines/bloc/routine_bloc/routine_bloc.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; +import 'package:dio/dio.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/common/bloc/project_manager.dart'; @@ -15,6 +16,7 @@ import 'package:syncrow_web/pages/routines/models/device_functions.dart'; import 'package:syncrow_web/pages/routines/models/routine_details_model.dart'; import 'package:syncrow_web/pages/routines/models/routine_model.dart'; import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; import 'package:syncrow_web/services/devices_mang_api.dart'; import 'package:syncrow_web/services/routines_api.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; @@ -64,7 +66,8 @@ class RoutineBloc extends Bloc { TriggerSwitchTabsEvent event, Emitter emit, ) { - emit(state.copyWith(routineTab: event.isRoutineTab, createRoutineView: false)); + emit(state.copyWith( + routineTab: event.isRoutineTab, createRoutineView: false)); add(ResetRoutineState()); if (event.isRoutineTab) { add(const LoadScenes()); @@ -90,8 +93,8 @@ class RoutineBloc extends Bloc { final updatedIfItems = List>.from(state.ifItems); // Find the index of the item in teh current itemsList - int index = - updatedIfItems.indexWhere((map) => map['uniqueCustomId'] == event.item['uniqueCustomId']); + int index = updatedIfItems.indexWhere( + (map) => map['uniqueCustomId'] == event.item['uniqueCustomId']); // Replace the map if the index is valid if (index != -1) { updatedIfItems[index] = event.item; @@ -100,18 +103,21 @@ class RoutineBloc extends Bloc { } if (event.isTabToRun) { - emit(state.copyWith(ifItems: updatedIfItems, isTabToRun: true, isAutomation: false)); + emit(state.copyWith( + ifItems: updatedIfItems, isTabToRun: true, isAutomation: false)); } else { - emit(state.copyWith(ifItems: updatedIfItems, isTabToRun: false, isAutomation: true)); + emit(state.copyWith( + ifItems: updatedIfItems, isTabToRun: false, isAutomation: true)); } } - void _onAddToThenContainer(AddToThenContainer event, Emitter emit) { + void _onAddToThenContainer( + AddToThenContainer event, Emitter emit) { final currentItems = List>.from(state.thenItems); // Find the index of the item in teh current itemsList - int index = - currentItems.indexWhere((map) => map['uniqueCustomId'] == event.item['uniqueCustomId']); + int index = currentItems.indexWhere( + (map) => map['uniqueCustomId'] == event.item['uniqueCustomId']); // Replace the map if the index is valid if (index != -1) { currentItems[index] = event.item; @@ -122,7 +128,8 @@ class RoutineBloc extends Bloc { emit(state.copyWith(thenItems: currentItems)); } - void _onAddFunctionsToRoutine(AddFunctionToRoutine event, Emitter emit) { + void _onAddFunctionsToRoutine( + AddFunctionToRoutine event, Emitter emit) { try { if (event.functions.isEmpty) return; @@ -157,7 +164,8 @@ class RoutineBloc extends Bloc { // currentSelectedFunctions[event.uniqueCustomId] = List.from(event.functions); // } - currentSelectedFunctions[event.uniqueCustomId] = List.from(event.functions); + currentSelectedFunctions[event.uniqueCustomId] = + List.from(event.functions); emit(state.copyWith(selectedFunctions: currentSelectedFunctions)); } catch (e) { @@ -165,24 +173,30 @@ class RoutineBloc extends Bloc { } } - Future _onLoadScenes(LoadScenes event, Emitter emit) async { + 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 == '') { + if (createRoutineBloc.selectedSpaceId == '' && + createRoutineBloc.selectedCommunityId == '') { var spaceBloc = context.read(); for (var communityId in spaceBloc.state.selectedCommunities) { - List spacesList = spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? []; + List spacesList = + spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? []; for (var spaceId in spacesList) { - scenes.addAll(await SceneApi.getScenes(spaceId, communityId, projectUuid)); + scenes.addAll( + await SceneApi.getScenes(spaceId, communityId, projectUuid)); } } } else { scenes.addAll(await SceneApi.getScenes( - createRoutineBloc.selectedSpaceId, createRoutineBloc.selectedCommunityId, projectUuid)); + createRoutineBloc.selectedSpaceId, + createRoutineBloc.selectedCommunityId, + projectUuid)); } emit(state.copyWith( @@ -199,7 +213,8 @@ class RoutineBloc extends Bloc { } } - Future _onLoadAutomation(LoadAutomation event, Emitter emit) async { + Future _onLoadAutomation( + LoadAutomation event, Emitter emit) async { emit(state.copyWith(isLoading: true, errorMessage: null)); List automations = []; final projectId = await ProjectManager.getProjectUUID() ?? ''; @@ -207,17 +222,22 @@ class RoutineBloc extends Bloc { BuildContext context = NavigationService.navigatorKey.currentContext!; var createRoutineBloc = context.read(); try { - if (createRoutineBloc.selectedSpaceId == '' && createRoutineBloc.selectedCommunityId == '') { + if (createRoutineBloc.selectedSpaceId == '' && + createRoutineBloc.selectedCommunityId == '') { var spaceBloc = context.read(); for (var communityId in spaceBloc.state.selectedCommunities) { - List spacesList = spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? []; + List spacesList = + spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? []; for (var spaceId in spacesList) { - automations.addAll(await SceneApi.getAutomation(spaceId, communityId, projectId)); + automations.addAll( + await SceneApi.getAutomation(spaceId, communityId, projectId)); } } } else { automations.addAll(await SceneApi.getAutomation( - createRoutineBloc.selectedSpaceId, createRoutineBloc.selectedCommunityId, projectId)); + createRoutineBloc.selectedSpaceId, + createRoutineBloc.selectedCommunityId, + projectId)); } emit(state.copyWith( automations: automations, @@ -233,14 +253,16 @@ class RoutineBloc extends Bloc { } } - FutureOr _onSearchRoutines(SearchRoutines event, Emitter emit) async { + FutureOr _onSearchRoutines( + SearchRoutines event, Emitter emit) async { emit(state.copyWith(isLoading: true, errorMessage: null)); await Future.delayed(const Duration(seconds: 1)); emit(state.copyWith(isLoading: false, errorMessage: null)); emit(state.copyWith(searchText: event.query)); } - FutureOr _onAddSelectedIcon(AddSelectedIcon event, Emitter emit) { + FutureOr _onAddSelectedIcon( + AddSelectedIcon event, Emitter emit) { emit(state.copyWith(selectedIcon: event.icon)); } @@ -254,7 +276,8 @@ class RoutineBloc extends Bloc { return actions.last['deviceId'] == 'delay'; } - Future _onCreateScene(CreateSceneEvent event, Emitter emit) async { + Future _onCreateScene( + CreateSceneEvent event, Emitter emit) async { try { // Check if first action is delay // if (_isFirstActionDelay(state.thenItems)) { @@ -267,7 +290,8 @@ class RoutineBloc extends Bloc { if (_isLastActionDelay(state.thenItems)) { emit(state.copyWith( - errorMessage: 'A delay condition cannot be the only or the last action', + errorMessage: + 'A delay condition cannot be the only or the last action', isLoading: false, )); return; @@ -335,15 +359,18 @@ class RoutineBloc extends Bloc { errorMessage: 'Something went wrong', )); } - } catch (e) { + } on APIException catch (e) { + final errorData = e.message; + String errorMessage = errorData; emit(state.copyWith( isLoading: false, - errorMessage: 'Something went wrong', + errorMessage: errorMessage, )); } } - Future _onCreateAutomation(CreateAutomationEvent event, Emitter emit) async { + Future _onCreateAutomation( + CreateAutomationEvent event, Emitter emit) async { try { final projectUuid = await ProjectManager.getProjectUUID() ?? ''; if (state.routineName == null || state.routineName!.isEmpty) { @@ -365,7 +392,8 @@ class RoutineBloc extends Bloc { if (_isLastActionDelay(state.thenItems)) { emit(state.copyWith( - errorMessage: 'A delay condition cannot be the only or the last action', + errorMessage: + 'A delay condition cannot be the only or the last action', isLoading: false, )); CustomSnackBar.redSnackBar('Cannot have delay as the last action'); @@ -456,7 +484,8 @@ class RoutineBloc extends Bloc { actions: actions, ); - final result = await SceneApi.createAutomation(createAutomationModel, projectUuid); + final result = + await SceneApi.createAutomation(createAutomationModel, projectUuid); if (result['success']) { add(ResetRoutineState()); add(const LoadAutomation()); @@ -468,26 +497,32 @@ class RoutineBloc extends Bloc { )); CustomSnackBar.redSnackBar('Something went wrong'); } - } catch (e) { + } on APIException catch (e) { + final errorData = e.message; + String errorMessage = errorData; emit(state.copyWith( isLoading: false, - errorMessage: 'Something went wrong', + errorMessage: errorMessage, )); - CustomSnackBar.redSnackBar('Something went wrong'); + CustomSnackBar.redSnackBar(errorMessage); } } - FutureOr _onRemoveDragCard(RemoveDragCard event, Emitter emit) { + FutureOr _onRemoveDragCard( + RemoveDragCard event, Emitter emit) { if (event.isFromThen) { final thenItems = List>.from(state.thenItems); - final selectedFunctions = Map>.from(state.selectedFunctions); + final selectedFunctions = + Map>.from(state.selectedFunctions); thenItems.removeAt(event.index); selectedFunctions.remove(event.key); - emit(state.copyWith(thenItems: thenItems, selectedFunctions: selectedFunctions)); + emit(state.copyWith( + thenItems: thenItems, selectedFunctions: selectedFunctions)); } else { final ifItems = List>.from(state.ifItems); - final selectedFunctions = Map>.from(state.selectedFunctions); + final selectedFunctions = + Map>.from(state.selectedFunctions); ifItems.removeAt(event.index); selectedFunctions.remove(event.key); @@ -498,7 +533,8 @@ class RoutineBloc extends Bloc { isAutomation: false, isTabToRun: false)); } else { - emit(state.copyWith(ifItems: ifItems, selectedFunctions: selectedFunctions)); + emit(state.copyWith( + ifItems: ifItems, selectedFunctions: selectedFunctions)); } } } @@ -510,11 +546,13 @@ class RoutineBloc extends Bloc { )); } - FutureOr _onEffectiveTimeEvent(EffectiveTimePeriodEvent event, Emitter emit) { + FutureOr _onEffectiveTimeEvent( + EffectiveTimePeriodEvent event, Emitter emit) { emit(state.copyWith(effectiveTime: event.effectiveTime)); } - FutureOr _onSetRoutineName(SetRoutineName event, Emitter emit) { + FutureOr _onSetRoutineName( + SetRoutineName event, Emitter emit) { emit(state.copyWith( routineName: event.name, )); @@ -641,7 +679,8 @@ class RoutineBloc extends Bloc { // return (thenItems, ifItems, currentFunctions); // } - Future _onGetSceneDetails(GetSceneDetails event, Emitter emit) async { + Future _onGetSceneDetails( + GetSceneDetails event, Emitter emit) async { try { emit(state.copyWith( isLoading: true, @@ -689,10 +728,12 @@ class RoutineBloc extends Bloc { // if (!deviceCards.containsKey(deviceId)) { deviceCards[deviceId] = { 'entityId': action.entityId, - 'deviceId': action.actionExecutor == 'delay' ? 'delay' : action.entityId, - 'uniqueCustomId': action.type == 'automation' || action.actionExecutor == 'delay' - ? action.entityId - : const Uuid().v4(), + 'deviceId': + action.actionExecutor == 'delay' ? 'delay' : action.entityId, + 'uniqueCustomId': + action.type == 'automation' || action.actionExecutor == 'delay' + ? action.entityId + : const Uuid().v4(), 'title': action.actionExecutor == 'delay' ? 'Delay' : action.type == 'automation' @@ -732,7 +773,8 @@ class RoutineBloc extends Bloc { ), ); // emit(state.copyWith(automationActionExecutor: action.actionExecutor)); - } else if (action.executorProperty != null && action.actionExecutor != 'delay') { + } else if (action.executorProperty != null && + action.actionExecutor != 'delay') { final functions = matchingDevice?.functions ?? []; final functionCode = action.executorProperty?.functionCode; for (DeviceFunction function in functions) { @@ -798,7 +840,8 @@ class RoutineBloc extends Bloc { } } - FutureOr _onResetRoutineState(ResetRoutineState event, Emitter emit) { + FutureOr _onResetRoutineState( + ResetRoutineState event, Emitter emit) { emit(state.copyWith( ifItems: [], thenItems: [], @@ -822,7 +865,8 @@ class RoutineBloc extends Bloc { createRoutineView: false)); } - FutureOr _deleteScene(DeleteScene event, Emitter emit) async { + FutureOr _deleteScene( + DeleteScene event, Emitter emit) async { try { final projectId = await ProjectManager.getProjectUUID() ?? ''; @@ -831,7 +875,8 @@ class RoutineBloc extends Bloc { var spaceBloc = context.read(); if (state.isTabToRun) { await SceneApi.deleteScene( - unitUuid: spaceBloc.state.selectedSpaces[0], sceneId: state.sceneId ?? ''); + unitUuid: spaceBloc.state.selectedSpaces[0], + sceneId: state.sceneId ?? ''); } else { await SceneApi.deleteAutomation( unitUuid: spaceBloc.state.selectedSpaces[0], @@ -854,11 +899,14 @@ class RoutineBloc extends Bloc { add(const LoadAutomation()); add(ResetRoutineState()); emit(state.copyWith(isLoading: false, createRoutineView: false)); - } catch (e) { + } on APIException catch (e) { + final errorData = e.message; + String errorMessage = errorData; emit(state.copyWith( isLoading: false, - errorMessage: 'Failed to delete scene', + errorMessage: errorMessage, )); + CustomSnackBar.redSnackBar(errorMessage); } } @@ -876,7 +924,8 @@ class RoutineBloc extends Bloc { // } // } - FutureOr _fetchDevices(FetchDevicesInRoutine event, Emitter emit) async { + FutureOr _fetchDevices( + FetchDevicesInRoutine event, Emitter emit) async { emit(state.copyWith(isLoading: true)); try { final projectUuid = await ProjectManager.getProjectUUID() ?? ''; @@ -885,17 +934,21 @@ class RoutineBloc extends Bloc { var createRoutineBloc = context.read(); var spaceBloc = context.read(); - if (createRoutineBloc.selectedSpaceId == '' && createRoutineBloc.selectedCommunityId == '') { + if (createRoutineBloc.selectedSpaceId == '' && + createRoutineBloc.selectedCommunityId == '') { for (var communityId in spaceBloc.state.selectedCommunities) { - List spacesList = spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? []; + List spacesList = + spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? []; for (var spaceId in spacesList) { - devices.addAll( - await DevicesManagementApi().fetchDevices(communityId, spaceId, projectUuid)); + devices.addAll(await DevicesManagementApi() + .fetchDevices(communityId, spaceId, projectUuid)); } } } else { devices.addAll(await DevicesManagementApi().fetchDevices( - createRoutineBloc.selectedCommunityId, createRoutineBloc.selectedSpaceId, projectUuid)); + createRoutineBloc.selectedCommunityId, + createRoutineBloc.selectedSpaceId, + projectUuid)); } emit(state.copyWith(isLoading: false, devices: devices)); @@ -904,7 +957,8 @@ class RoutineBloc extends Bloc { } } - FutureOr _onUpdateScene(UpdateScene event, Emitter emit) async { + FutureOr _onUpdateScene( + UpdateScene event, Emitter emit) async { try { // Check if first action is delay // if (_isFirstActionDelay(state.thenItems)) { @@ -918,7 +972,8 @@ class RoutineBloc extends Bloc { if (_isLastActionDelay(state.thenItems)) { emit(state.copyWith( - errorMessage: 'A delay condition cannot be the only or the last action', + errorMessage: + 'A delay condition cannot be the only or the last action', isLoading: false, )); return; @@ -971,7 +1026,8 @@ class RoutineBloc extends Bloc { actions: actions, ); - final result = await SceneApi.updateScene(createSceneModel, state.sceneId ?? ''); + final result = + await SceneApi.updateScene(createSceneModel, state.sceneId ?? ''); if (result['success']) { add(ResetRoutineState()); add(const LoadScenes()); @@ -990,7 +1046,8 @@ class RoutineBloc extends Bloc { } } - FutureOr _onUpdateAutomation(UpdateAutomation event, Emitter emit) async { + FutureOr _onUpdateAutomation( + UpdateAutomation event, Emitter emit) async { try { if (state.routineName == null || state.routineName!.isEmpty) { emit(state.copyWith( @@ -1114,10 +1171,11 @@ class RoutineBloc extends Bloc { errorMessage: result['message'], )); } - } catch (e) { + } on APIException catch (e) { + final errorData = e.message; emit(state.copyWith( isLoading: false, - errorMessage: 'Something went wrong', + errorMessage: errorData, )); } } @@ -1214,7 +1272,8 @@ class RoutineBloc extends Bloc { // if (!deviceThenCards.containsKey(deviceId)) { deviceThenCards[deviceId] = { 'entityId': action.entityId, - 'deviceId': action.actionExecutor == 'delay' ? 'delay' : action.entityId, + 'deviceId': + action.actionExecutor == 'delay' ? 'delay' : action.entityId, 'uniqueCustomId': const Uuid().v4(), 'title': action.actionExecutor == 'delay' ? 'Delay' @@ -1249,7 +1308,8 @@ class RoutineBloc extends Bloc { updatedFunctions[uniqueCustomId] = []; } - if (action.executorProperty != null && action.actionExecutor != 'delay') { + if (action.executorProperty != null && + action.actionExecutor != 'delay') { final functions = matchingDevice.functions; final functionCode = action.executorProperty!.functionCode; for (var function in functions) { @@ -1291,10 +1351,14 @@ class RoutineBloc extends Bloc { } } - final ifItems = deviceIfCards.values.where((card) => card['type'] == 'condition').toList(); + final ifItems = deviceIfCards.values + .where((card) => card['type'] == 'condition') + .toList(); final thenItems = deviceThenCards.values .where((card) => - card['type'] == 'action' || card['type'] == 'automation' || card['type'] == 'scene') + card['type'] == 'action' || + card['type'] == 'automation' || + card['type'] == 'scene') .toList(); emit(state.copyWith( @@ -1316,7 +1380,8 @@ class RoutineBloc extends Bloc { } } - Future _onSceneTrigger(SceneTrigger event, Emitter emit) async { + Future _onSceneTrigger( + SceneTrigger event, Emitter emit) async { emit(state.copyWith(loadingSceneId: event.sceneId)); try { @@ -1358,24 +1423,29 @@ class RoutineBloc extends Bloc { if (success) { final updatedAutomations = await SceneApi.getAutomationByUnitId( - event.automationStatusUpdate.spaceUuid, event.communityId, projectId); + event.automationStatusUpdate.spaceUuid, + event.communityId, + projectId); // Remove from loading set safely - final updatedLoadingIds = {...state.loadingAutomationIds!}..remove(event.automationId); + final updatedLoadingIds = {...state.loadingAutomationIds!} + ..remove(event.automationId); emit(state.copyWith( automations: updatedAutomations, loadingAutomationIds: updatedLoadingIds, )); } else { - final updatedLoadingIds = {...state.loadingAutomationIds!}..remove(event.automationId); + final updatedLoadingIds = {...state.loadingAutomationIds!} + ..remove(event.automationId); emit(state.copyWith( loadingAutomationIds: updatedLoadingIds, errorMessage: 'Update failed', )); } } catch (e) { - final updatedLoadingIds = {...state.loadingAutomationIds!}..remove(event.automationId); + final updatedLoadingIds = {...state.loadingAutomationIds!} + ..remove(event.automationId); emit(state.copyWith( loadingAutomationIds: updatedLoadingIds, errorMessage: 'Update error: ${e.toString()}', diff --git a/lib/pages/space_tree/view/space_tree_view.dart b/lib/pages/space_tree/view/space_tree_view.dart index fadcdc0c..c60474f8 100644 --- a/lib/pages/space_tree/view/space_tree_view.dart +++ b/lib/pages/space_tree/view/space_tree_view.dart @@ -48,7 +48,8 @@ class _SpaceTreeViewState extends State { @override Widget build(BuildContext context) { - return BlocBuilder(builder: (context, state) { + return BlocBuilder( + builder: (context, state) { final communities = state.searchQuery.isNotEmpty ? state.filteredCommunity : state.communityList; @@ -132,104 +133,118 @@ class _SpaceTreeViewState extends State { ) else CustomSearchBar( - onSearchChanged: (query) => context.read().add( - SearchQueryEvent(query), - ), + onSearchChanged: (query) => + context.read().add( + SearchQueryEvent(query), + ), ), const SizedBox(height: 16), Expanded( child: state.isSearching ? const Center(child: CircularProgressIndicator()) - : SidebarCommunitiesList( - onScrollToEnd: () { - if (!state.paginationIsLoading) { - context.read().add( - PaginationEvent( - state.paginationModel, - state.communityList, - ), - ); - } - }, - scrollController: _scrollController, - communities: communities, - itemBuilder: (context, index) { - return CustomExpansionTileSpaceTree( - title: communities[index].name, - isSelected: state.selectedCommunities - .contains(communities[index].uuid), - isSoldCheck: state.selectedCommunities - .contains(communities[index].uuid), - onExpansionChanged: () => - context.read().add( - OnCommunityExpanded( - communities[index].uuid, - ), - ), - isExpanded: state.expandedCommunities.contains( - communities[index].uuid, + : communities.isEmpty + ? Center( + child: Text( + 'No communities found', + style: context.textTheme.bodyMedium?.copyWith( + color: ColorsManager.textGray, + ), ), - onItemSelected: () { - widget.onSelect(); - context.read().add( - OnCommunitySelected( - communities[index].uuid, - communities[index].spaces, - ), - ); - }, - children: communities[index].spaces.map( - (space) { - return CustomExpansionTileSpaceTree( - title: space.name, - isExpanded: - state.expandedSpaces.contains(space.uuid), - onItemSelected: () { - final isParentSelected = _isParentSelected( - state, - communities[index], - space, + ) + : SidebarCommunitiesList( + onScrollToEnd: () { + if (!state.paginationIsLoading) { + context.read().add( + PaginationEvent( + state.paginationModel, + state.communityList, + ), ); - if (widget - .shouldDisableDeselectingChildrenOfSelectedParent && - isParentSelected) { - return; - } - widget.onSelect(); + } + }, + scrollController: _scrollController, + communities: communities, + itemBuilder: (context, index) { + return CustomExpansionTileSpaceTree( + title: communities[index].name, + isSelected: state.selectedCommunities + .contains(communities[index].uuid), + isSoldCheck: state.selectedCommunities + .contains(communities[index].uuid), + onExpansionChanged: () => context.read().add( - OnSpaceSelected( - communities[index], - space.uuid ?? '', - space.children, + OnCommunityExpanded( + communities[index].uuid, ), + ), + isExpanded: + state.expandedCommunities.contains( + communities[index].uuid, + ), + onItemSelected: () { + widget.onSelect(); + context.read().add( + OnCommunitySelected( + communities[index].uuid, + communities[index].spaces, + ), + ); + }, + children: communities[index].spaces.map( + (space) { + return CustomExpansionTileSpaceTree( + title: space.name, + isExpanded: state.expandedSpaces + .contains(space.uuid), + onItemSelected: () { + final isParentSelected = + _isParentSelected( + state, + communities[index], + space, ); + if (widget + .shouldDisableDeselectingChildrenOfSelectedParent && + isParentSelected) { + return; + } + widget.onSelect(); + context.read().add( + OnSpaceSelected( + communities[index], + space.uuid ?? '', + space.children, + ), + ); + }, + onExpansionChanged: () => + context.read().add( + OnSpaceExpanded( + communities[index].uuid, + space.uuid ?? '', + ), + ), + isSelected: state.selectedSpaces + .contains(space.uuid) || + state.soldCheck + .contains(space.uuid), + isSoldCheck: state.soldCheck + .contains(space.uuid), + children: _buildNestedSpaces( + context, + state, + space, + communities[index], + ), + ); }, - onExpansionChanged: () => - context.read().add( - OnSpaceExpanded( - communities[index].uuid, - space.uuid ?? '', - ), - ), - isSelected: state.selectedSpaces - .contains(space.uuid) || - state.soldCheck.contains(space.uuid), - isSoldCheck: - state.soldCheck.contains(space.uuid), - children: _buildNestedSpaces( - context, - state, - space, - communities[index], - ), - ); - }, - ).toList(), - ); - }, - ), + ).toList(), + ); + }, + ), ), - if (state.paginationIsLoading) const CircularProgressIndicator(), + if (state.paginationIsLoading) + const CircularProgressIndicator(), ], ), ); diff --git a/lib/services/api/api_exception.dart b/lib/services/api/api_exception.dart new file mode 100644 index 00000000..89d969d3 --- /dev/null +++ b/lib/services/api/api_exception.dart @@ -0,0 +1,10 @@ +class APIException implements Exception { + final String message; + + APIException(this.message); + + @override + String toString() { + return message; + } +} diff --git a/lib/services/auth_api.dart b/lib/services/auth_api.dart index 190eb624..18d951c1 100644 --- a/lib/services/auth_api.dart +++ b/lib/services/auth_api.dart @@ -1,18 +1,26 @@ +import 'package:dio/dio.dart'; import 'package:syncrow_web/pages/auth/model/region_model.dart'; import 'package:syncrow_web/pages/auth/model/token.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 AuthenticationAPI { static Future loginWithEmail({required var model}) async { - final response = await HTTPService().post( - path: ApiEndpoints.login, - body: model.toJson(), - showServerMessage: true, - expectedResponseModel: (json) { - return Token.fromJson(json['data']); - }); - return response; + try { + final response = await HTTPService().post( + path: ApiEndpoints.login, + body: model.toJson(), + showServerMessage: true, + expectedResponseModel: (json) { + return Token.fromJson(json['data']); + }); + return response; + } on DioException catch (e) { + final message = e.response?.data['error']['message'] ?? + 'An error occurred while logging in'; + throw APIException(message); + } } static Future forgetPassword({ @@ -20,12 +28,18 @@ class AuthenticationAPI { required var password, required var otpCode, }) async { - final response = await HTTPService().post( - path: ApiEndpoints.forgetPassword, - body: {"email": email, "password": password, "otpCode": otpCode}, - showServerMessage: true, - expectedResponseModel: (json) {}); - return response; + try { + final response = await HTTPService().post( + path: ApiEndpoints.forgetPassword, + body: {"email": email, "password": password, "otpCode": otpCode}, + showServerMessage: true, + expectedResponseModel: (json) {}); + return response; + } on DioException catch (e) { + final message = e.response?.data['error']['message'] ?? + 'An error occurred while resetting the password'; + throw APIException(message); + } } static Future sendOtp({required String email}) async { @@ -39,19 +53,26 @@ class AuthenticationAPI { return response; } - static Future verifyOtp({required String email, required String otpCode}) async { - final response = await HTTPService().post( - path: ApiEndpoints.verifyOtp, - body: {"email": email, "type": "PASSWORD", "otpCode": otpCode}, - showServerMessage: true, - expectedResponseModel: (json) { - if (json['message'] == 'Otp Verified Successfully') { - return true; - } else { - return false; - } - }); - return response; + static Future verifyOtp( + {required String email, required String otpCode}) async { + try { + final response = await HTTPService().post( + path: ApiEndpoints.verifyOtp, + body: {"email": email, "type": "PASSWORD", "otpCode": otpCode}, + showServerMessage: true, + expectedResponseModel: (json) { + if (json['message'] == 'Otp Verified Successfully') { + return true; + } else { + return false; + } + }); + return response; + } on APIException catch (e) { + throw APIException(e.message); + } catch (e) { + throw APIException('An error occurred while verifying the OTP'); + } } static Future> fetchRegion() async { @@ -59,7 +80,9 @@ class AuthenticationAPI { path: ApiEndpoints.getRegion, showServerMessage: true, expectedResponseModel: (json) { - return (json as List).map((zone) => RegionModel.fromJson(zone)).toList(); + return (json as List) + .map((zone) => RegionModel.fromJson(zone)) + .toList(); }); return response; } diff --git a/lib/services/routines_api.dart b/lib/services/routines_api.dart index eaa09e27..bdc46ac1 100644 --- a/lib/services/routines_api.dart +++ b/lib/services/routines_api.dart @@ -1,3 +1,4 @@ +import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/routines/bloc/automation_scene_trigger_bloc/automation_status_update.dart'; import 'package:syncrow_web/pages/routines/models/create_scene_and_autoamtion/create_automation_model.dart'; @@ -5,6 +6,7 @@ import 'package:syncrow_web/pages/routines/models/create_scene_and_autoamtion/cr import 'package:syncrow_web/pages/routines/models/icon_model.dart'; import 'package:syncrow_web/pages/routines/models/routine_details_model.dart'; import 'package:syncrow_web/pages/routines/models/routine_model.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'; @@ -26,9 +28,10 @@ class SceneApi { ); debugPrint('create scene response: $response'); return response; - } catch (e) { - debugPrint(e.toString()); - rethrow; + } on DioException catch (e) { + String errorMessage = + e.response?.data['error']['message'][0] ?? 'something went wrong'; + throw APIException(errorMessage); } } @@ -48,9 +51,10 @@ class SceneApi { ); debugPrint('create automation response: $response'); return response; - } catch (e) { - debugPrint(e.toString()); - rethrow; + } on DioException catch (e) { + String errorMessage = + e.response?.data['error']['message'][0] ?? 'something went wrong'; + throw APIException(errorMessage); } } @@ -165,8 +169,10 @@ class SceneApi { }, ); return response; - } catch (e) { - rethrow; + } on DioException catch (e) { + String errorMessage = + e.response?.data['error']['message'][0] ?? 'something went wrong'; + throw APIException(errorMessage); } } @@ -185,8 +191,10 @@ class SceneApi { }, ); return response; - } catch (e) { - rethrow; + } on DioException catch (e) { + String errorMessage = + e.response?.data['error']['message'][0] ?? 'something went wrong'; + throw APIException(errorMessage); } } @@ -217,8 +225,10 @@ class SceneApi { expectedResponseModel: (json) => json['statusCode'] == 200, ); return response; - } catch (e) { - rethrow; + } on DioException catch (e) { + String errorMessage = + e.response?.data['error']['message'][0] ?? 'something went wrong'; + throw APIException(errorMessage); } } @@ -236,8 +246,10 @@ class SceneApi { expectedResponseModel: (json) => json['statusCode'] == 200, ); return response; - } catch (e) { - rethrow; + } on DioException catch (e) { + String errorMessage = + e.response?.data['error']['message'][0] ?? 'something went wrong'; + throw APIException(errorMessage); } } From 6a3640553063cced54fa4a87602d71f1a9067952 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 3 Jun 2025 10:48:01 +0300 Subject: [PATCH 50/58] Refactor `TwoGangSwitchBloc` to integrate new service dependencies and utilize a factory for instantiation. Enhanced event handling methods for improved error management and real-time status updates from Firebase, including parsing logic for device status values. --- .../bloc/two_gang_switch_bloc.dart | 250 ++++++++---------- .../two_gang_switch_bloc_factory.dart | 18 ++ .../models/two_gang_status_model.dart | 8 +- .../view/wall_light_batch_control.dart | 4 +- .../view/wall_light_device_control.dart | 3 +- 5 files changed, 133 insertions(+), 150 deletions(-) create mode 100644 lib/pages/device_managment/two_gang_switch/factories/two_gang_switch_bloc_factory.dart diff --git a/lib/pages/device_managment/two_gang_switch/bloc/two_gang_switch_bloc.dart b/lib/pages/device_managment/two_gang_switch/bloc/two_gang_switch_bloc.dart index ea72e05b..5efe0848 100644 --- a/lib/pages/device_managment/two_gang_switch/bloc/two_gang_switch_bloc.dart +++ b/lib/pages/device_managment/two_gang_switch/bloc/two_gang_switch_bloc.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:developer'; import 'package:firebase_database/firebase_database.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -6,10 +7,22 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/device_sta 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/models/two_gang_status_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'; class TwoGangSwitchBloc extends Bloc { - TwoGangSwitchBloc({required this.deviceId}) : super(TwoGangSwitchInitial()) { + final String deviceId; + final ControlDeviceService controlDeviceService; + final BatchControlDevicesService batchControlDevicesService; + + late TwoGangStatusModel deviceStatus; + + TwoGangSwitchBloc({ + required this.deviceId, + required this.controlDeviceService, + required this.batchControlDevicesService, + }) : super(TwoGangSwitchInitial()) { on(_onFetchDeviceStatus); on(_onControl); on(_onFetchBatchStatus); @@ -18,16 +31,13 @@ class TwoGangSwitchBloc extends Bloc { on(_onStatusUpdated); } - late TwoGangStatusModel deviceStatus; - final String deviceId; - Timer? _timer; - - FutureOr _onFetchDeviceStatus(TwoGangSwitchFetchDeviceEvent event, - Emitter emit) async { + Future _onFetchDeviceStatus( + TwoGangSwitchFetchDeviceEvent event, + Emitter emit, + ) async { emit(TwoGangSwitchLoading()); try { - final status = - await DevicesManagementApi().getDeviceStatus(event.deviceId); + final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); deviceStatus = TwoGangStatusModel.fromJson(event.deviceId, status.status); _listenToChanges(event.deviceId); emit(TwoGangSwitchStatusLoaded(deviceStatus)); @@ -36,131 +46,96 @@ class TwoGangSwitchBloc extends Bloc { } } - FutureOr _onControl( - TwoGangSwitchControl event, Emitter emit) async { - final oldValue = _getValueByCode(event.code); + void _listenToChanges(String deviceId) { + try { + final ref = FirebaseDatabase.instance.ref( + 'device-status/$deviceId', + ); + ref.onValue.listen((event) { + final eventsMap = event.snapshot.value as Map; + + List statusList = []; + eventsMap['status'].forEach((element) { + statusList.add( + Status( + code: element['code'], + value: element['value'].toString(), + ), + ); + }); + + deviceStatus = TwoGangStatusModel.fromJson(deviceId, statusList); + add(StatusUpdated(deviceStatus)); + }); + } catch (_) { + log( + 'Error listening to changes', + name: 'TwoGangSwitchBloc._listenToChanges', + ); + } + } + + Future _onControl( + TwoGangSwitchControl event, + Emitter emit, + ) async { + emit(TwoGangSwitchLoading()); _updateLocalValue(event.code, event.value); - emit(TwoGangSwitchStatusLoaded(deviceStatus)); - await _runDebounce( - deviceId: event.deviceId, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - isBatch: false, - ); + try { + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status(code: event.code, value: event.value), + ); + } catch (e) { + _updateLocalValue(event.code, !event.value); + emit(TwoGangSwitchError(e.toString())); + } } - Future _runDebounce({ - required dynamic deviceId, - required String code, - required bool value, - required bool oldValue, - required Emitter emit, - required bool isBatch, - }) async { - late String id; - - if (deviceId is List) { - id = deviceId.first; - } else { - id = deviceId; - } - - if (_timer != null) { - _timer!.cancel(); - } - - _timer = Timer(const Duration(milliseconds: 500), () async { - try { - late bool response; - if (isBatch) { - response = await DevicesManagementApi() - .deviceBatchControl(deviceId, code, value); - } else { - response = await DevicesManagementApi() - .deviceControl(deviceId, Status(code: code, value: value)); - } - - if (!response) { - _revertValueAndEmit(id, code, oldValue, emit); - } - } catch (e) { - _revertValueAndEmit(id, code, oldValue, emit); - } - }); - } - - void _revertValueAndEmit(String deviceId, String code, bool oldValue, - Emitter emit) { - _updateLocalValue(code, oldValue); + Future _onBatchControl( + TwoGangSwitchBatchControl event, + Emitter emit, + ) async { + emit(TwoGangSwitchLoading()); + _updateLocalValue(event.code, event.value); emit(TwoGangSwitchStatusLoaded(deviceStatus)); - } - void _updateLocalValue(String code, bool value) { - if (code == 'switch_1') { - deviceStatus = deviceStatus.copyWith(switch1: value); - } - - if (code == 'switch_2') { - deviceStatus = deviceStatus.copyWith(switch2: value); + try { + await batchControlDevicesService.batchControlDevices( + uuids: event.deviceId, + code: event.code, + value: event.value, + ); + } catch (e) { + _updateLocalValue(event.code, !event.value); + emit(TwoGangSwitchError(e.toString())); } } - bool _getValueByCode(String code) { - switch (code) { - case 'switch_1': - return deviceStatus.switch1; - case 'switch_2': - return deviceStatus.switch2; - default: - return false; - } - } - - Future _onFetchBatchStatus(TwoGangSwitchFetchBatchEvent event, - Emitter emit) async { + Future _onFetchBatchStatus( + TwoGangSwitchFetchBatchEvent event, + Emitter emit, + ) async { emit(TwoGangSwitchLoading()); try { - final status = - await DevicesManagementApi().getBatchStatus(event.devicesIds); - deviceStatus = - TwoGangStatusModel.fromJson(event.devicesIds.first, status.status); + final status = await DevicesManagementApi().getBatchStatus(event.devicesIds); + deviceStatus = TwoGangStatusModel.fromJson( + event.devicesIds.first, + status.status, + ); emit(TwoGangSwitchStatusLoaded(deviceStatus)); } catch (e) { emit(TwoGangSwitchError(e.toString())); } } - @override - Future close() { - _timer?.cancel(); - return super.close(); - } - - FutureOr _onBatchControl( - TwoGangSwitchBatchControl event, Emitter emit) async { - final oldValue = _getValueByCode(event.code); - - _updateLocalValue(event.code, event.value); - - emit(TwoGangSwitchStatusLoaded(deviceStatus)); - - await _runDebounce( - deviceId: event.deviceId, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - isBatch: true, - ); - } - - FutureOr _onFactoryReset( - TwoGangFactoryReset event, Emitter emit) async { + Future _onFactoryReset( + TwoGangFactoryReset event, + Emitter emit, + ) async { emit(TwoGangSwitchLoading()); try { final response = await DevicesManagementApi().factoryReset( @@ -168,42 +143,31 @@ class TwoGangSwitchBloc extends Bloc { event.deviceId, ); if (!response) { - emit(TwoGangSwitchError('Failed')); + emit(TwoGangSwitchError('Failed to reset device')); } else { - emit(TwoGangSwitchStatusLoaded(deviceStatus)); + add(TwoGangSwitchFetchDeviceEvent(event.deviceId)); } } catch (e) { emit(TwoGangSwitchError(e.toString())); } } - _listenToChanges(deviceId) { - try { - DatabaseReference ref = - FirebaseDatabase.instance.ref('device-status/$deviceId'); - Stream stream = ref.onValue; - - 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 = - TwoGangStatusModel.fromJson(usersMap['productUuid'], statusList); - if (!isClosed) { - add(StatusUpdated(deviceStatus)); - } - }); - } catch (_) {} - } - - void _onStatusUpdated(StatusUpdated event, Emitter emit) { + void _onStatusUpdated( + StatusUpdated event, + Emitter emit, + ) { deviceStatus = event.deviceStatus; emit(TwoGangSwitchStatusLoaded(deviceStatus)); } + + void _updateLocalValue(String code, bool value) { + switch (code) { + case 'switch_1': + deviceStatus = deviceStatus.copyWith(switch1: value); + break; + case 'switch_2': + deviceStatus = deviceStatus.copyWith(switch2: value); + break; + } + } } diff --git a/lib/pages/device_managment/two_gang_switch/factories/two_gang_switch_bloc_factory.dart b/lib/pages/device_managment/two_gang_switch/factories/two_gang_switch_bloc_factory.dart new file mode 100644 index 00000000..37893caf --- /dev/null +++ b/lib/pages/device_managment/two_gang_switch/factories/two_gang_switch_bloc_factory.dart @@ -0,0 +1,18 @@ +import 'package:syncrow_web/pages/device_managment/factories/device_bloc_dependencies_factory.dart'; +import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_bloc.dart'; + +abstract final class TwoGangSwitchBlocFactory { + const TwoGangSwitchBlocFactory._(); + + static TwoGangSwitchBloc create({ + required String deviceId, + }) { + return TwoGangSwitchBloc( + deviceId: deviceId, + controlDeviceService: + DeviceBlocDependenciesFactory.createControlDeviceService(), + batchControlDevicesService: + DeviceBlocDependenciesFactory.createBatchControlDevicesService(), + ); + } +} diff --git a/lib/pages/device_managment/two_gang_switch/models/two_gang_status_model.dart b/lib/pages/device_managment/two_gang_switch/models/two_gang_status_model.dart index 6cec4256..58094a71 100644 --- a/lib/pages/device_managment/two_gang_switch/models/two_gang_status_model.dart +++ b/lib/pages/device_managment/two_gang_switch/models/two_gang_status_model.dart @@ -24,16 +24,16 @@ class TwoGangStatusModel { for (var status in jsonList) { switch (status.code) { case 'switch_1': - switch1 = status.value ?? false; + switch1 = bool.tryParse(status.value.toString()) ?? false; break; case 'countdown_1': - countDown = status.value ?? 0; + countDown = int.tryParse(status.value.toString()) ?? 0; break; case 'switch_2': - switch2 = status.value ?? false; + switch2 = bool.tryParse(status.value.toString()) ?? false; break; case 'countdown_2': - countDown2 = status.value ?? 0; + countDown2 = int.tryParse(status.value.toString()) ?? 0; break; } } 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 b3a39287..e8346cb2 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 @@ -2,11 +2,11 @@ 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/shared/batch_control/factory_reset.dart'; -// import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.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/helpers/responsice_layout_helper/responsive_layout_helper.dart'; @@ -18,7 +18,7 @@ class TwoGangBatchControlView extends StatelessWidget with HelperResponsiveLayou @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => TwoGangSwitchBloc(deviceId: deviceIds.first) + create: (context) => TwoGangSwitchBlocFactory.create(deviceId: deviceIds.first) ..add(TwoGangSwitchFetchBatchEvent(deviceIds)), child: BlocBuilder( builder: (context, state) { 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 840d356e..882aac3e 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 @@ -4,6 +4,7 @@ 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/helpers/responsice_layout_helper/responsive_layout_helper.dart'; @@ -15,7 +16,7 @@ class TwoGangDeviceControlView extends StatelessWidget @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => TwoGangSwitchBloc(deviceId: deviceId) + create: (context) => TwoGangSwitchBlocFactory.create(deviceId: deviceId) ..add(TwoGangSwitchFetchDeviceEvent(deviceId)), child: BlocBuilder( builder: (context, state) { From d1df33b31ee4b41ee56b0148a9f7f15c26a1a092 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 3 Jun 2025 11:15:06 +0300 Subject: [PATCH 51/58] Refactor `WallSensorBloc` to integrate new service dependencies and utilize a factory for instantiation. Enhanced event handling methods for improved error management and real-time status updates from Firebase, including optimized parsing logic for device status values. --- .../bloc/two_gang_switch_bloc.dart | 9 +- .../wall_sensor/bloc/wall_bloc.dart | 231 +++++++++--------- .../factories/wall_sensor_bloc_factory.dart | 18 ++ .../view/wall_sensor_batch_control.dart | 3 +- .../view/wall_sensor_conrtols.dart | 3 +- 5 files changed, 139 insertions(+), 125 deletions(-) create mode 100644 lib/pages/device_managment/wall_sensor/factories/wall_sensor_bloc_factory.dart diff --git a/lib/pages/device_managment/two_gang_switch/bloc/two_gang_switch_bloc.dart b/lib/pages/device_managment/two_gang_switch/bloc/two_gang_switch_bloc.dart index 5efe0848..2e3a8633 100644 --- a/lib/pages/device_managment/two_gang_switch/bloc/two_gang_switch_bloc.dart +++ b/lib/pages/device_managment/two_gang_switch/bloc/two_gang_switch_bloc.dart @@ -48,9 +48,7 @@ class TwoGangSwitchBloc extends Bloc { void _listenToChanges(String deviceId) { try { - final ref = FirebaseDatabase.instance.ref( - 'device-status/$deviceId', - ); + final ref = FirebaseDatabase.instance.ref('device-status/$deviceId'); ref.onValue.listen((event) { final eventsMap = event.snapshot.value as Map; @@ -58,10 +56,7 @@ class TwoGangSwitchBloc extends Bloc { List statusList = []; eventsMap['status'].forEach((element) { statusList.add( - Status( - code: element['code'], - value: element['value'].toString(), - ), + Status(code: element['code'], value: element['value']), ); }); diff --git a/lib/pages/device_managment/wall_sensor/bloc/wall_bloc.dart b/lib/pages/device_managment/wall_sensor/bloc/wall_bloc.dart index 3c144142..630a132b 100644 --- a/lib/pages/device_managment/wall_sensor/bloc/wall_bloc.dart +++ b/lib/pages/device_managment/wall_sensor/bloc/wall_bloc.dart @@ -1,18 +1,28 @@ import 'dart:async'; +import 'dart:developer'; + import 'package:firebase_database/firebase_database.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/wall_sensor/bloc/wall_event.dart'; import 'package:syncrow_web/pages/device_managment/wall_sensor/bloc/wall_state.dart'; import 'package:syncrow_web/pages/device_managment/wall_sensor/model/wall_sensor_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'; class WallSensorBloc extends Bloc { final String deviceId; - late WallSensorModel deviceStatus; - Timer? _timer; + final ControlDeviceService controlDeviceService; + final BatchControlDevicesService batchControlDevicesService; - WallSensorBloc({required this.deviceId}) : super(WallSensorInitialState()) { + late WallSensorModel deviceStatus; + + WallSensorBloc({ + required this.deviceId, + required this.controlDeviceService, + required this.batchControlDevicesService, + }) : super(WallSensorInitialState()) { on(_fetchWallSensorStatus); on(_fetchWallSensorBatchControl); on(_changeValue); @@ -24,28 +34,28 @@ class WallSensorBloc extends Bloc { on(_onRealtimeUpdate); } - void _fetchWallSensorStatus( - WallSensorFetchStatusEvent event, Emitter emit) async { + Future _fetchWallSensorStatus( + WallSensorFetchStatusEvent event, + Emitter emit, + ) async { emit(WallSensorLoadingInitialState()); try { - var response = await DevicesManagementApi().getDeviceStatus(deviceId); + final response = await DevicesManagementApi().getDeviceStatus(deviceId); deviceStatus = WallSensorModel.fromJson(response.status); - emit(WallSensorUpdateState(wallSensorModel: deviceStatus)); _listenToChanges(deviceId); + emit(WallSensorUpdateState(wallSensorModel: deviceStatus)); } catch (e) { emit(WallSensorFailedState(error: e.toString())); - return; } } - // Fetch batch status - FutureOr _fetchWallSensorBatchControl( - WallSensorFetchBatchStatusEvent event, - Emitter emit) async { + Future _fetchWallSensorBatchControl( + WallSensorFetchBatchStatusEvent event, + Emitter emit, + ) async { emit(WallSensorLoadingInitialState()); try { - var response = - await DevicesManagementApi().getBatchStatus(event.devicesIds); + final response = await DevicesManagementApi().getBatchStatus(event.devicesIds); deviceStatus = WallSensorModel.fromJson(response.status); emit(WallSensorUpdateState(wallSensorModel: deviceStatus)); } catch (e) { @@ -54,132 +64,105 @@ class WallSensorBloc extends Bloc { } void _listenToChanges(String deviceId) { - DatabaseReference ref = - FirebaseDatabase.instance.ref('device-status/$deviceId'); - ref.onValue.listen((DatabaseEvent event) { - final data = event.snapshot.value as Map?; - if (data == null) return; + try { + final ref = FirebaseDatabase.instance.ref('device-status/$deviceId'); - final statusList = (data['status'] as List?) - ?.map((e) => Status(code: e['code'], value: e['value'])) - .toList(); + ref.onValue.listen((event) { + final eventsMap = event.snapshot.value as Map; - if (statusList != null) { - final updatedDeviceStatus = WallSensorModel.fromJson(statusList); + List statusList = []; + eventsMap['status'].forEach((element) { + statusList.add( + Status(code: element['code'], value: element['value']), + ); + }); + + deviceStatus = WallSensorModel.fromJson(statusList); if (!isClosed) { - add(WallSensorRealtimeUpdateEvent(updatedDeviceStatus)); + add(WallSensorRealtimeUpdateEvent(deviceStatus)); } - } - }); + }); + } catch (_) { + log( + 'Error listening to changes', + name: 'WallSensorBloc._listenToChanges', + ); + } } - - void _changeValue( - WallSensorChangeValueEvent event, Emitter emit) async { + Future _changeValue( + WallSensorChangeValueEvent event, + Emitter emit, + ) async { emit(WallSensorLoadingNewSate(wallSensorModel: deviceStatus)); - if (event.code == 'far_detection') { - deviceStatus.farDetection = event.value; - } else if (event.code == 'motionless_sensitivity') { - deviceStatus.motionlessSensitivity = event.value; - } else if (event.code == 'motion_sensitivity_value') { - deviceStatus.motionSensitivity = event.value; - } else if (event.code == 'no_one_time') { - deviceStatus.noBodyTime = event.value; - } + _updateLocalValue(event.code, event.value); emit(WallSensorUpdateState(wallSensorModel: deviceStatus)); - await _runDeBouncer( - deviceId: deviceId, - code: event.code, - value: event.value, - isBatch: false, - emit: emit, - ); + + try { + await controlDeviceService.controlDevice( + deviceUuid: deviceId, + status: Status(code: event.code, value: event.value), + ); + } catch (e) { + _updateLocalValue(event.code, event.value == 0 ? 1 : 0); + emit(WallSensorFailedState(error: e.toString())); + } } Future _onBatchControl( - WallSensorBatchControlEvent event, Emitter emit) async { + WallSensorBatchControlEvent event, + Emitter emit, + ) async { emit(WallSensorLoadingNewSate(wallSensorModel: deviceStatus)); - if (event.code == 'far_detection') { - deviceStatus.farDetection = event.value; - } else if (event.code == 'motionless_sensitivity') { - deviceStatus.motionlessSensitivity = event.value; - } else if (event.code == 'motion_sensitivity_value') { - deviceStatus.motionSensitivity = event.value; - } else if (event.code == 'no_one_time') { - deviceStatus.noBodyTime = event.value; - } + _updateLocalValue(event.code, event.value); emit(WallSensorUpdateState(wallSensorModel: deviceStatus)); - await _runDeBouncer( - deviceId: event.deviceIds, - code: event.code, - value: event.value, - emit: emit, - isBatch: true, - ); - } - - _runDeBouncer({ - required dynamic deviceId, - required String code, - required dynamic value, - required Emitter emit, - required bool isBatch, - }) { - if (_timer != null) { - _timer!.cancel(); - } - _timer = Timer(const Duration(seconds: 1), () async { - try { - late bool response; - if (isBatch) { - response = await DevicesManagementApi() - .deviceBatchControl(deviceId, code, value); - } else { - response = await DevicesManagementApi() - .deviceControl(deviceId, Status(code: code, value: value)); - } - - if (!response) { - add(WallSensorFetchStatusEvent()); - } - } catch (_) { - await Future.delayed(const Duration(milliseconds: 500)); - add(WallSensorFetchStatusEvent()); - } - }); - } - - FutureOr _getDeviceReports( - GetDeviceReportsEvent event, Emitter emit) async { - emit(DeviceReportsLoadingState()); - // final from = DateTime.now().subtract(const Duration(days: 30)).millisecondsSinceEpoch; - // final to = DateTime.now().millisecondsSinceEpoch; try { - // await DevicesManagementApi.getDeviceReportsByDate( - // deviceId, event.code, from.toString(), to.toString()) - await DevicesManagementApi.getDeviceReports(deviceId, event.code) - .then((value) { - emit(DeviceReportsState(deviceReport: value, code: event.code)); - }); + await batchControlDevicesService.batchControlDevices( + uuids: event.deviceIds, + code: event.code, + value: event.value, + ); + } catch (e) { + _updateLocalValue(event.code, !event.value); + emit(WallSensorFailedState(error: e.toString())); + } + } + + Future _getDeviceReports( + GetDeviceReportsEvent event, + Emitter emit, + ) async { + emit(DeviceReportsLoadingState()); + try { + final reports = await DevicesManagementApi.getDeviceReports( + deviceId, + event.code, + ); + emit(DeviceReportsState(deviceReport: reports, code: event.code)); } catch (e) { emit(DeviceReportsFailedState(error: e.toString())); - return; } } void _showDescription( - ShowDescriptionEvent event, Emitter emit) { + ShowDescriptionEvent event, + Emitter emit, + ) { emit(WallSensorShowDescriptionState(description: event.description)); } void _backToGridView( - BackToGridViewEvent event, Emitter emit) { + BackToGridViewEvent event, + Emitter emit, + ) { emit(WallSensorUpdateState(wallSensorModel: deviceStatus)); } - FutureOr _onFactoryReset( - WallSensorFactoryResetEvent event, Emitter emit) async { + Future _onFactoryReset( + WallSensorFactoryResetEvent event, + Emitter emit, + ) async { emit(WallSensorLoadingNewSate(wallSensorModel: deviceStatus)); try { final response = await DevicesManagementApi().factoryReset( @@ -187,9 +170,9 @@ class WallSensorBloc extends Bloc { event.deviceId, ); if (!response) { - emit(const WallSensorFailedState(error: 'Failed')); + emit(const WallSensorFailedState(error: 'Failed to reset device')); } else { - emit(WallSensorUpdateState(wallSensorModel: deviceStatus)); + add(WallSensorFetchStatusEvent()); } } catch (e) { emit(WallSensorFailedState(error: e.toString())); @@ -200,7 +183,23 @@ class WallSensorBloc extends Bloc { WallSensorRealtimeUpdateEvent event, Emitter emit, ) { - deviceStatus = event.deviceStatus; - emit(WallSensorUpdateState(wallSensorModel: deviceStatus)); + emit(WallSensorUpdateState(wallSensorModel: event.deviceStatus)); + } + + void _updateLocalValue(String code, dynamic value) { + switch (code) { + case 'far_detection': + deviceStatus.farDetection = value; + break; + case 'motionless_sensitivity': + deviceStatus.motionlessSensitivity = value; + break; + case 'motion_sensitivity_value': + deviceStatus.motionSensitivity = value; + break; + case 'no_one_time': + deviceStatus.noBodyTime = value; + break; + } } } diff --git a/lib/pages/device_managment/wall_sensor/factories/wall_sensor_bloc_factory.dart b/lib/pages/device_managment/wall_sensor/factories/wall_sensor_bloc_factory.dart new file mode 100644 index 00000000..d7811717 --- /dev/null +++ b/lib/pages/device_managment/wall_sensor/factories/wall_sensor_bloc_factory.dart @@ -0,0 +1,18 @@ +import 'package:syncrow_web/pages/device_managment/factories/device_bloc_dependencies_factory.dart'; +import 'package:syncrow_web/pages/device_managment/wall_sensor/bloc/wall_bloc.dart'; + +abstract final class WallSensorBlocFactory { + const WallSensorBlocFactory._(); + + static WallSensorBloc create({ + required String deviceId, + }) { + return WallSensorBloc( + deviceId: deviceId, + controlDeviceService: + DeviceBlocDependenciesFactory.createControlDeviceService(), + batchControlDevicesService: + DeviceBlocDependenciesFactory.createBatchControlDevicesService(), + ); + } +} diff --git a/lib/pages/device_managment/wall_sensor/view/wall_sensor_batch_control.dart b/lib/pages/device_managment/wall_sensor/view/wall_sensor_batch_control.dart index 27169f0e..61108387 100644 --- a/lib/pages/device_managment/wall_sensor/view/wall_sensor_batch_control.dart +++ b/lib/pages/device_managment/wall_sensor/view/wall_sensor_batch_control.dart @@ -7,6 +7,7 @@ import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presen import 'package:syncrow_web/pages/device_managment/wall_sensor/bloc/wall_bloc.dart'; import 'package:syncrow_web/pages/device_managment/wall_sensor/bloc/wall_event.dart'; import 'package:syncrow_web/pages/device_managment/wall_sensor/bloc/wall_state.dart'; +import 'package:syncrow_web/pages/device_managment/wall_sensor/factories/wall_sensor_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/wall_sensor/model/wall_sensor_model.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; @@ -21,7 +22,7 @@ class WallSensorBatchControlView extends StatelessWidget with HelperResponsiveLa final isLarge = isLargeScreenSize(context); final isMedium = isMediumScreenSize(context); return BlocProvider( - create: (context) => WallSensorBloc(deviceId: devicesIds.first) + create: (context) => WallSensorBlocFactory.create(deviceId: devicesIds.first) ..add(WallSensorFetchBatchStatusEvent(devicesIds)), child: BlocBuilder( builder: (context, state) { diff --git a/lib/pages/device_managment/wall_sensor/view/wall_sensor_conrtols.dart b/lib/pages/device_managment/wall_sensor/view/wall_sensor_conrtols.dart index 370edaa5..def8ed93 100644 --- a/lib/pages/device_managment/wall_sensor/view/wall_sensor_conrtols.dart +++ b/lib/pages/device_managment/wall_sensor/view/wall_sensor_conrtols.dart @@ -10,6 +10,7 @@ import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presen import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presence_static_widget.dart'; import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presence_status.dart'; import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presence_update_data.dart'; +import 'package:syncrow_web/pages/device_managment/wall_sensor/factories/wall_sensor_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/wall_sensor/model/wall_sensor_model.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; @@ -26,7 +27,7 @@ class WallSensorControlsView extends StatelessWidget with HelperResponsiveLayout final isMedium = isMediumScreenSize(context); return BlocProvider( create: (context) => - WallSensorBloc(deviceId: device.uuid!)..add(WallSensorFetchStatusEvent()), + WallSensorBlocFactory.create(deviceId: device.uuid!)..add(WallSensorFetchStatusEvent()), child: BlocBuilder( builder: (context, state) { if (state is WallSensorLoadingInitialState || state is DeviceReportsLoadingState) { From 7cc46d464fdc47e38557a966d46a7621f5b104e9 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 3 Jun 2025 12:24:38 +0300 Subject: [PATCH 52/58] SP-1510-show date instead of index in occupancy chart. --- .../analytics/modules/occupancy/widgets/occupancy_chart.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart b/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart index 4ff85841..70087c46 100644 --- a/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart +++ b/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart @@ -16,7 +16,7 @@ class OccupancyChart extends StatelessWidget { Widget build(BuildContext context) { return BarChart( BarChartData( - maxY: 100.0, + maxY: 100.001, gridData: EnergyManagementChartsHelper.gridData().copyWith( checkToShowHorizontalLine: (value) => true, horizontalInterval: 20, @@ -134,7 +134,7 @@ class OccupancyChart extends StatelessWidget { alignment: AlignmentDirectional.bottomCenter, fit: BoxFit.scaleDown, child: Text( - (value + 1).toString(), + chartData[value.toInt()].date.day.toString(), style: context.textTheme.bodySmall?.copyWith( color: ColorsManager.greyColor, fontSize: 8, From 46feb0ea28d88fa8acb1183a57732c9ac3afddf6 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 3 Jun 2025 15:20:30 +0300 Subject: [PATCH 53/58] SP-1509 attatch space uuid to analytics device dropdown on energy management tab. --- .../analytics/models/analytics_device.dart | 27 +++++++++++-------- ...y_consumption_per_device_devices_list.dart | 2 +- .../power_clamp_energy_data_widget.dart | 2 +- .../widgets/analytics_sidebar_header.dart | 5 ++-- 4 files changed, 21 insertions(+), 15 deletions(-) diff --git a/lib/pages/analytics/models/analytics_device.dart b/lib/pages/analytics/models/analytics_device.dart index 88f18ec5..eaac8b2b 100644 --- a/lib/pages/analytics/models/analytics_device.dart +++ b/lib/pages/analytics/models/analytics_device.dart @@ -23,17 +23,18 @@ class AnalyticsDevice { return AnalyticsDevice( uuid: json['uuid'] as String, name: json['name'] as String, - createdAt: json['createdAt'] != null ? DateTime.parse(json['createdAt'] as String) : null, - updatedAt: json['updatedAt'] != null ? DateTime.parse(json['updatedAt'] as String) : null, + createdAt: json['createdAt'] != null + ? DateTime.parse(json['createdAt'] as String) + : null, + updatedAt: json['updatedAt'] != null + ? DateTime.parse(json['updatedAt'] as String) + : null, deviceTuyaUuid: json['deviceTuyaUuid'] as String?, isActive: json['isActive'] as bool?, - productDevice: json['productDevice'] != null - ? ProductDevice.fromJson(json['productDevice'] as Map) - : null, - spaceUuid: (json['spaces'] as List?) - ?.map((e) => e['uuid']) - .firstOrNull - ?.toString(), + productDevice: json['productDevice'] != null + ? ProductDevice.fromJson(json['productDevice'] as Map) + : null, + spaceUuid: json['spaceUuid'] as String?, ); } } @@ -60,8 +61,12 @@ class ProductDevice { factory ProductDevice.fromJson(Map json) { return ProductDevice( uuid: json['uuid'] as String?, - createdAt: json['createdAt'] != null ? DateTime.parse(json['createdAt'] as String) : null, - updatedAt: json['updatedAt'] != null ? DateTime.parse(json['updatedAt'] as String) : null, + createdAt: json['createdAt'] != null + ? DateTime.parse(json['createdAt'] as String) + : null, + updatedAt: json['updatedAt'] != null + ? DateTime.parse(json['updatedAt'] as String) + : null, catName: json['catName'] as String?, prodId: json['prodId'] as String?, name: json['name'] as String?, diff --git a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_devices_list.dart b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_devices_list.dart index b7205424..f0cb5d64 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_devices_list.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_devices_list.dart @@ -41,7 +41,7 @@ class EnergyConsumptionPerDeviceDevicesList extends StatelessWidget { .color; return Tooltip( - message: '${device.name}\n${device.productDevice?.uuid ?? ''}', + message: '${device.name}\n${device.spaceUuid ?? ''}', child: ChartInformativeCell(title: Text(device.name), color: deviceColor), ); } diff --git a/lib/pages/analytics/modules/energy_management/widgets/power_clamp_energy_data_widget.dart b/lib/pages/analytics/modules/energy_management/widgets/power_clamp_energy_data_widget.dart index 4d04a36b..f95ff7d1 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/power_clamp_energy_data_widget.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/power_clamp_energy_data_widget.dart @@ -41,7 +41,7 @@ class PowerClampEnergyDataWidget extends StatelessWidget { AnalyticsErrorWidget(state.errorMessage), AnalyticsSidebarHeader( title: 'Smart Power Clamp', - showSpaceUuid: true, + showSpaceUuidInDevicesDropdown: true, onChanged: (device) { FetchEnergyManagementDataHelper.loadEnergyConsumptionByPhases( context, diff --git a/lib/pages/analytics/widgets/analytics_sidebar_header.dart b/lib/pages/analytics/widgets/analytics_sidebar_header.dart index 5e454ea4..5ff1d042 100644 --- a/lib/pages/analytics/widgets/analytics_sidebar_header.dart +++ b/lib/pages/analytics/widgets/analytics_sidebar_header.dart @@ -10,13 +10,13 @@ import 'package:syncrow_web/utils/extension/build_context_x.dart'; class AnalyticsSidebarHeader extends StatelessWidget { const AnalyticsSidebarHeader({ required this.title, - this.showSpaceUuid = false, + this.showSpaceUuidInDevicesDropdown = false, this.onChanged, super.key, }); final String title; - final bool showSpaceUuid; + final bool showSpaceUuidInDevicesDropdown; final void Function(AnalyticsDevice device)? onChanged; @override @@ -49,6 +49,7 @@ class AnalyticsSidebarHeader extends StatelessWidget { alignment: AlignmentDirectional.centerEnd, fit: BoxFit.scaleDown, child: AnalyticsDeviceDropdown( + showSpaceUuid: showSpaceUuidInDevicesDropdown, onChanged: (value) { context.read().add( SelectAnalyticsDeviceEvent(value), From 0135b6711eec7fe17cfdb3d400411a64052288ee Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 3 Jun 2025 16:01:45 +0300 Subject: [PATCH 54/58] removed getting energy management data using communityUuid. --- .../helpers/fetch_energy_management_data_helper.dart | 7 ------- .../params/get_energy_consumption_per_device_param.dart | 3 --- .../params/get_total_energy_consumption_param.dart | 3 --- 3 files changed, 13 deletions(-) diff --git a/lib/pages/analytics/modules/energy_management/helpers/fetch_energy_management_data_helper.dart b/lib/pages/analytics/modules/energy_management/helpers/fetch_energy_management_data_helper.dart index a6fe4703..8de92098 100644 --- a/lib/pages/analytics/modules/energy_management/helpers/fetch_energy_management_data_helper.dart +++ b/lib/pages/analytics/modules/energy_management/helpers/fetch_energy_management_data_helper.dart @@ -16,7 +16,6 @@ import 'package:syncrow_web/pages/analytics/params/get_total_energy_consumption_ abstract final class FetchEnergyManagementDataHelper { const FetchEnergyManagementDataHelper._(); - // static const String _powerClampId = 'cb71d6ad-6e29-4eaa-ae3e-1a0d1c5f60fa'; static AnalyticsDevice? getSelectedDevice(BuildContext context) { return context.read().state.selectedDevice; } @@ -48,7 +47,6 @@ abstract final class FetchEnergyManagementDataHelper { loadTotalEnergyConsumption( context, selectedDate: selectedDate0, - communityId: communityId, spaceId: spaceId, ); final selectedDevice = getSelectedDevice(context); @@ -61,7 +59,6 @@ abstract final class FetchEnergyManagementDataHelper { } loadEnergyConsumptionPerDevice( context, - communityId: communityId, spaceId: spaceId, selectedDate: selectedDate0, ); @@ -84,12 +81,10 @@ abstract final class FetchEnergyManagementDataHelper { static void loadTotalEnergyConsumption( BuildContext context, { DateTime? selectedDate, - required String communityId, required String spaceId, }) { final param = GetTotalEnergyConsumptionParam( spaceId: spaceId, - communityId: communityId, monthDate: selectedDate, ); context.read().add( @@ -100,12 +95,10 @@ abstract final class FetchEnergyManagementDataHelper { static void loadEnergyConsumptionPerDevice( BuildContext context, { DateTime? selectedDate, - required String communityId, required String spaceId, }) { final param = GetEnergyConsumptionPerDeviceParam( spaceId: spaceId, - communityId: communityId, monthDate: selectedDate, ); context.read().add( diff --git a/lib/pages/analytics/params/get_energy_consumption_per_device_param.dart b/lib/pages/analytics/params/get_energy_consumption_per_device_param.dart index ba659ae7..79d0f2f4 100644 --- a/lib/pages/analytics/params/get_energy_consumption_per_device_param.dart +++ b/lib/pages/analytics/params/get_energy_consumption_per_device_param.dart @@ -2,18 +2,15 @@ class GetEnergyConsumptionPerDeviceParam { const GetEnergyConsumptionPerDeviceParam({ this.monthDate, this.spaceId, - this.communityId, }); final DateTime? monthDate; final String? spaceId; - final String? communityId; Map toJson() => { 'monthDate': '${monthDate?.year}-${monthDate?.month.toString().padLeft(2, '0')}', if (spaceId == null || spaceId == null) 'spaceUuid': spaceId, - 'communityUuid': communityId, 'groupByDevice': true, }; } diff --git a/lib/pages/analytics/params/get_total_energy_consumption_param.dart b/lib/pages/analytics/params/get_total_energy_consumption_param.dart index c47e5bfe..6428fd30 100644 --- a/lib/pages/analytics/params/get_total_energy_consumption_param.dart +++ b/lib/pages/analytics/params/get_total_energy_consumption_param.dart @@ -1,12 +1,10 @@ class GetTotalEnergyConsumptionParam { final DateTime? monthDate; final String? spaceId; - final String? communityId; const GetTotalEnergyConsumptionParam({ this.monthDate, this.spaceId, - this.communityId, }); Map toJson() { @@ -14,7 +12,6 @@ class GetTotalEnergyConsumptionParam { 'monthDate': '${monthDate?.year}-${monthDate?.month.toString().padLeft(2, '0')}', if (spaceId == null || spaceId == null) 'spaceUuid': spaceId, - 'communityUuid': communityId, 'groupByDevice': false, }; } From c2c58e6a7a39d2e1c3d9c15847d10ee534f55748 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 3 Jun 2025 16:17:14 +0300 Subject: [PATCH 55/58] SP-1658-the-analytics-chart-padding-is-not-aligned-with-the-design. --- ...ergy_consumption_per_device_chart_box.dart | 3 ++- .../total_energy_consumption_chart_box.dart | 3 ++- .../widgets/occupancy_chart_box.dart | 5 +++-- .../widgets/occupancy_heat_map_box.dart | 5 +++-- .../widgets/analytics_error_widget.dart | 19 +++++++++++-------- 5 files changed, 21 insertions(+), 14 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 f22517d5..be5faf57 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 @@ -23,7 +23,6 @@ class EnergyConsumptionPerDeviceChartBox extends StatelessWidget { ), padding: const EdgeInsets.all(30), child: Column( - spacing: 20, crossAxisAlignment: CrossAxisAlignment.start, children: [ AnalyticsErrorWidget(state.errorMessage), @@ -52,7 +51,9 @@ class EnergyConsumptionPerDeviceChartBox extends StatelessWidget { ), ], ), + const SizedBox(height: 20), const Divider(height: 0), + const SizedBox(height: 20), Expanded( child: EnergyConsumptionPerDeviceChart(chartData: state.chartData), ), 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 9e70e45e..e197c297 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 @@ -19,7 +19,6 @@ class TotalEnergyConsumptionChartBox extends StatelessWidget { ), padding: const EdgeInsets.all(30), child: Column( - spacing: 20, crossAxisAlignment: CrossAxisAlignment.start, children: [ AnalyticsErrorWidget(state.errorMessage), @@ -39,7 +38,9 @@ class TotalEnergyConsumptionChartBox extends StatelessWidget { const Spacer(flex: 4), ], ), + const SizedBox(height: 20), const Divider(), + const SizedBox(height: 20), TotalEnergyConsumptionChart(chartData: state.chartData), ], ), diff --git a/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart_box.dart b/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart_box.dart index ab1d1699..08f7223f 100644 --- a/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart_box.dart +++ b/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart_box.dart @@ -22,7 +22,6 @@ class OccupancyChartBox extends StatelessWidget { padding: const EdgeInsets.all(30), decoration: containerWhiteDecoration, child: Column( - spacing: 20, mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -65,7 +64,9 @@ class OccupancyChartBox extends StatelessWidget { ), ], ), - const Divider(height: 0), + const SizedBox(height: 20), + const Divider(), + const SizedBox(height: 20), Expanded(child: OccupancyChart(chartData: state.chartData)), ], ), 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 cab9eab4..c3b537e0 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 @@ -22,7 +22,6 @@ class OccupancyHeatMapBox extends StatelessWidget { padding: const EdgeInsets.all(30), decoration: containerWhiteDecoration, child: Column( - spacing: 20, mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -66,7 +65,9 @@ class OccupancyHeatMapBox extends StatelessWidget { ), ], ), - const Divider(height: 0), + const SizedBox(height: 20), + const Divider(), + const SizedBox(height: 20), Expanded( child: OccupancyHeatMap( heatMapData: state.heatMapData.asMap().map( diff --git a/lib/pages/analytics/widgets/analytics_error_widget.dart b/lib/pages/analytics/widgets/analytics_error_widget.dart index 60167992..7c560da4 100644 --- a/lib/pages/analytics/widgets/analytics_error_widget.dart +++ b/lib/pages/analytics/widgets/analytics_error_widget.dart @@ -11,14 +11,17 @@ class AnalyticsErrorWidget extends StatelessWidget { Widget build(BuildContext context) { return Visibility( visible: errorMessage != null || (errorMessage?.isNotEmpty ?? false), - child: Text( - errorMessage ?? 'Something went wrong', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: context.textTheme.bodySmall?.copyWith( - color: ColorsManager.red, - fontWeight: FontWeight.w400, - fontSize: 8, + child: Padding( + padding: const EdgeInsetsDirectional.only(bottom: 10), + child: Text( + errorMessage ?? 'Something went wrong', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.red, + fontWeight: FontWeight.w400, + fontSize: 8, + ), ), ), ); From e86c25c74ab3413eb784d48201eda52e8e660885 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 3 Jun 2025 16:18:57 +0300 Subject: [PATCH 56/58] includes min in all left titles charts. --- .../helpers/energy_management_charts_helper.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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..2ed68e76 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 @@ -38,7 +38,7 @@ abstract final class EnergyManagementChartsHelper { sideTitles: SideTitles( showTitles: true, maxIncluded: false, - minIncluded: false, + minIncluded: true, interval: leftTitlesInterval, reservedSize: 110, getTitlesWidget: (value, meta) => Padding( From 906c2d0430393cd8e26f99ef9b500cbd7c2dd1a7 Mon Sep 17 00:00:00 2001 From: mohammad Date: Tue, 3 Jun 2025 16:34:00 +0300 Subject: [PATCH 57/58] Refactor device management and space management APIs, update event and state classes, and add RemoveDeviceWidget for device removal functionality. --- .../bloc/setting_bloc_bloc.dart | 132 +++--- .../bloc/setting_bloc_event.dart | 29 +- .../bloc/setting_bloc_state.dart | 42 +- .../device_management_content.dart | 127 ++++++ .../device_setting/device_settings_panel.dart | 419 ++++++------------ .../device_setting/remove_device_widget.dart | 82 ++++ .../device_setting/sub_space_dialog.dart | 71 +-- .../subspace_dialog_buttons.dart | 114 +++++ lib/services/devices_mang_api.dart | 6 +- lib/services/space_mana_api.dart | 6 +- 10 files changed, 549 insertions(+), 479 deletions(-) create mode 100644 lib/pages/device_managment/device_setting/device_management_content.dart create mode 100644 lib/pages/device_managment/device_setting/remove_device_widget.dart create mode 100644 lib/pages/device_managment/device_setting/subspace_dialog_buttons.dart diff --git a/lib/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart b/lib/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart index e4d6a835..92d94a8f 100644 --- a/lib/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart +++ b/lib/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart @@ -10,26 +10,24 @@ import 'package:syncrow_web/services/space_mana_api.dart'; import 'package:syncrow_web/utils/snack_bar.dart'; part 'setting_bloc_event.dart'; -class SettingBlocBloc extends Bloc { +class SettingDeviceBloc extends Bloc { final String deviceId; - SettingBlocBloc({ + SettingDeviceBloc({ required this.deviceId, - }) : super(const SettingBlocInitial()) { - on(fetchDeviceInfo); - on(saveName); + }) : super(const DeviceSettingsInitial()) { + on(_fetchDeviceInfo); + on(_saveName); on(_changeName); - on(deleteDevice); - on(_fetchRooms); - on(_assignDevice); + on(_deleteDevice); + on(_fetchRooms); + on(_onAssignDevice); } - static String deviceName = ''; - final TextEditingController nameController = - TextEditingController(text: deviceName); + final TextEditingController nameController = TextEditingController(); List roomsList = []; bool isEditingName = false; bool _validateInputs() { - final nameError = fullNameValidator(nameController.text); + final nameError = _fullNameValidator(nameController.text); if (nameError != null) { CustomSnackBar.displaySnackBar(nameError); return true; @@ -37,7 +35,7 @@ class SettingBlocBloc extends Bloc { return false; } - String? fullNameValidator(String? value) { + String? _fullNameValidator(String? value) { if (value == null) return 'name is required'; final withoutExtraSpaces = value.replaceAll(RegExp(r"\s+"), ' ').trim(); if (withoutExtraSpaces.length < 2 || withoutExtraSpaces.length > 30) { @@ -49,69 +47,35 @@ class SettingBlocBloc extends Bloc { return null; } - Future saveName( - SaveNameEvent event, Emitter emit) async { + Future _saveName( + SettingBlocSaveName event, Emitter emit) async { if (_validateInputs()) return; try { - emit(SettingLoadingState()); + emit(DeviceSettingsLoading()); await DevicesManagementApi.putDeviceName( deviceId: deviceId, deviceName: nameController.text); add(DeviceSettingInitialInfo()); CustomSnackBar.displaySnackBar('Save Successfully'); - emit(UpdateSettingState(deviceName: nameController.text)); + emit(DeviceSettingsUpdate(deviceName: nameController.text)); } catch (e) { - emit(ErrorState(message: e.toString())); - } + emit(DeviceSettingsError(message: e.toString())); + } } - DeviceInfoModel deviceInfo = DeviceInfoModel( - activeTime: 0, - category: "", - categoryName: "", - createTime: 0, - gatewayId: "", - icon: "", - ip: "", - lat: "", - localKey: "", - lon: "", - model: "", - name: "", - nodeId: "", - online: false, - ownerId: "", - productName: "", - sub: false, - timeZone: "", - updateTime: 0, - uuid: "", - productUuid: "", - productType: "", - permissionType: "", - macAddress: "", - subspace: Subspace( - uuid: "", - createdAt: "", - updatedAt: "", - subspaceName: "", - ), - ); - - Future fetchDeviceInfo( + Future _fetchDeviceInfo( DeviceSettingInitialInfo event, Emitter emit) async { try { - emit(SettingLoadingState()); + emit(DeviceSettingsLoading()); var response = await DevicesManagementApi.getDeviceInfo(deviceId); - deviceInfo = DeviceInfoModel.fromJson(response); + DeviceInfoModel deviceInfo = DeviceInfoModel.fromJson(response); nameController.text = deviceInfo.name; - - emit(UpdateSettingState( + emit(DeviceSettingsUpdate( deviceName: nameController.text, deviceInfo: deviceInfo, roomsList: roomsList, )); } catch (e) { - emit(ErrorState(message: e.toString())); + emit(DeviceSettingsError(message: e.toString())); } } @@ -119,46 +83,50 @@ class SettingBlocBloc extends Bloc { final FocusNode focusNode = FocusNode(); void _changeName(ChangeNameEvent event, Emitter emit) { - emit(SettingLoadingState()); + emit(DeviceSettingsInitial( + deviceName: nameController.text, + deviceId: deviceId, + isEditingName: event.value ?? false, + editingNameValue: event.value?.toString() ?? '', + deviceInfo: state.deviceInfo, + )); editName = event.value!; if (editName) { Future.delayed(const Duration(milliseconds: 500), () { focusNode.requestFocus(); }); } else { - add(const SaveNameEvent()); + add(const SettingBlocSaveName()); focusNode.unfocus(); } - emit(UpdateSettingState( - deviceName: deviceName, - deviceInfo: deviceInfo, + emit(DeviceSettingsUpdate( + deviceName: nameController.text, + deviceInfo: state.deviceInfo, roomsList: roomsList, )); } - void deleteDevice( - DeleteDeviceEvent event, Emitter emit) async { + void _deleteDevice( + SettingBlocDeleteDevice event, Emitter emit) async { try { - emit(SettingLoadingState()); - await DevicesManagementApi.resetDevise(devicesUuid: deviceId); + emit(DeviceSettingsLoading()); + await DevicesManagementApi.resetDevice(devicesUuid: deviceId); CustomSnackBar.displaySnackBar('Reset Successfully'); - emit(UpdateSettingState( + emit(DeviceSettingsUpdate( deviceName: nameController.text, - deviceInfo: deviceInfo, + deviceInfo: state.deviceInfo, roomsList: roomsList, )); } catch (e) { - emit(ErrorState(message: e.toString())); + emit(DeviceSettingsError(message: e.toString())); return; } } - //=========================== assign device to room ========================== - - void _assignDevice( - AssignRoomEvent event, Emitter emit) async { + void _onAssignDevice( + SettingBlocAssignRoom event, Emitter emit) async { try { - emit(SettingLoadingState()); + emit(DeviceSettingsLoading()); final projectUuid = await ProjectManager.getProjectUUID() ?? ''; await CommunitySpaceManagementApi.assignDeviceToRoom( communityId: event.communityUuid, @@ -168,29 +136,29 @@ class SettingBlocBloc extends Bloc { projectId: projectUuid); add(DeviceSettingInitialInfo()); CustomSnackBar.displaySnackBar('Save Successfully'); - emit(SaveSelectionSuccessState()); + emit(DeviceSettingsSaveSelectionSuccess()); } catch (e) { - emit(ErrorState(message: e.toString())); + emit(DeviceSettingsError(message: e.toString())); return; } } void _fetchRooms( - FetchRoomsEvent event, Emitter emit) async { + SettingBlocFetchRooms event, Emitter emit) async { try { - emit(SettingLoadingState()); + emit(DeviceSettingsLoading()); final projectUuid = await ProjectManager.getProjectUUID() ?? ''; roomsList = await CommunitySpaceManagementApi.getSubSpaceBySpaceId( communityId: event.communityUuid, spaceId: event.spaceUuid, projectId: projectUuid); - emit(UpdateSettingState( + emit(DeviceSettingsUpdate( deviceName: nameController.text, - deviceInfo: deviceInfo, + deviceInfo: state.deviceInfo, roomsList: roomsList, )); } catch (e) { - emit(ErrorState(message: e.toString())); + emit(DeviceSettingsError(message: e.toString())); return; } } diff --git a/lib/pages/device_managment/device_setting/bloc/setting_bloc_event.dart b/lib/pages/device_managment/device_setting/bloc/setting_bloc_event.dart index 66d9e09f..ab62d8a0 100644 --- a/lib/pages/device_managment/device_setting/bloc/setting_bloc_event.dart +++ b/lib/pages/device_managment/device_setting/bloc/setting_bloc_event.dart @@ -6,40 +6,42 @@ abstract class SettingBlocEvent extends Equatable { List get props => []; } -class SaveDeviceName extends SettingBlocEvent { +class SettingBlocSaveDeviceName extends SettingBlocEvent { final String deviceName; final String deviceId; - const SaveDeviceName({required this.deviceName, required this.deviceId}); + const SettingBlocSaveDeviceName( + {required this.deviceName, required this.deviceId}); @override List get props => [deviceName, deviceId]; } -class StartEditingName extends SettingBlocEvent {} +class SettingBlocStartEditingName extends SettingBlocEvent {} -class CancelEditingName extends SettingBlocEvent {} +class SettingBlocCancelEditingName extends SettingBlocEvent {} -class ChangeEditingNameValue extends SettingBlocEvent { +class SettingBlocChangeEditingNameValue extends SettingBlocEvent { final String value; - const ChangeEditingNameValue(this.value); + const SettingBlocChangeEditingNameValue(this.value); @override List get props => [value]; } -class FetchRoomsEvent extends SettingBlocEvent { +class SettingBlocFetchRooms extends SettingBlocEvent { final String communityUuid; final String spaceUuid; - const FetchRoomsEvent({required this.communityUuid, required this.spaceUuid}); + const SettingBlocFetchRooms( + {required this.communityUuid, required this.spaceUuid}); @override List get props => [communityUuid, spaceUuid]; } -class SaveNameEvent extends SettingBlocEvent { - const SaveNameEvent(); +class SettingBlocSaveName extends SettingBlocEvent { + const SettingBlocSaveName(); } class DeviceSettingInitialInfo extends SettingBlocEvent {} @@ -49,14 +51,14 @@ class ChangeNameEvent extends SettingBlocEvent { const ChangeNameEvent({this.value}); } -class DeleteDeviceEvent extends SettingBlocEvent {} +class SettingBlocDeleteDevice extends SettingBlocEvent {} -class AssignRoomEvent extends SettingBlocEvent { +class SettingBlocAssignRoom extends SettingBlocEvent { final String communityUuid; final String spaceUuid; final String subSpaceUuid; - const AssignRoomEvent({ + const SettingBlocAssignRoom({ required this.communityUuid, required this.spaceUuid, required this.subSpaceUuid, @@ -65,3 +67,4 @@ class AssignRoomEvent extends SettingBlocEvent { @override List get props => [spaceUuid, communityUuid, subSpaceUuid]; } + diff --git a/lib/pages/device_managment/device_setting/bloc/setting_bloc_state.dart b/lib/pages/device_managment/device_setting/bloc/setting_bloc_state.dart index eb30b70a..55054c9a 100644 --- a/lib/pages/device_managment/device_setting/bloc/setting_bloc_state.dart +++ b/lib/pages/device_managment/device_setting/bloc/setting_bloc_state.dart @@ -3,32 +3,35 @@ import 'package:syncrow_web/pages/device_managment/device_setting/settings_model import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/sub_space_model.dart'; abstract class DeviceSettingsState extends Equatable { - const DeviceSettingsState(); + const DeviceSettingsState({this.deviceInfo}); + + final DeviceInfoModel? deviceInfo; @override - List get props => []; + List get props => [deviceInfo]; } -class SettingBlocInitial extends DeviceSettingsState { +class DeviceSettingsInitial extends DeviceSettingsState { final String deviceName; final String deviceId; final bool isEditingName; final String editingNameValue; - const SettingBlocInitial({ + const DeviceSettingsInitial({ this.deviceName = '', this.deviceId = '', this.isEditingName = false, this.editingNameValue = '', + super.deviceInfo, }); - SettingBlocInitial copyWith({ + DeviceSettingsInitial copyWith({ String? deviceName, String? deviceId, bool? isEditingName, String? editingNameValue, }) => - SettingBlocInitial( + DeviceSettingsInitial( deviceName: deviceName ?? this.deviceName, deviceId: deviceId ?? this.deviceId, isEditingName: isEditingName ?? this.isEditingName, @@ -40,36 +43,39 @@ class SettingBlocInitial extends DeviceSettingsState { [deviceName, deviceId, isEditingName, editingNameValue]; } -class SettingLoadingState extends DeviceSettingsState {} +class DeviceSettingsLoading extends DeviceSettingsState {} -class UpdateSettingState extends DeviceSettingsState { +class DeviceSettingsUpdate extends DeviceSettingsState { final String? deviceName; - final DeviceInfoModel? deviceInfo; - final List roomsList; + final List roomsList; - const UpdateSettingState({ + const DeviceSettingsUpdate({ this.deviceName, - this.deviceInfo, - this.roomsList = const [], + super.deviceInfo, + this.roomsList = const [], }); + + @override List get props => [deviceName, deviceInfo, roomsList]; } -class ErrorState extends DeviceSettingsState { +class DeviceSettingsError extends DeviceSettingsState { final String message; - const ErrorState({required this.message}); + const DeviceSettingsError({required this.message}); @override List get props => [message]; } -class FetchRoomsState extends DeviceSettingsState { +class DeviceSettingsFetchRooms extends DeviceSettingsState { final List roomsList; - const FetchRoomsState({required this.roomsList}); + const DeviceSettingsFetchRooms({required this.roomsList}); @override List get props => [roomsList]; } -class SaveSelectionSuccessState extends DeviceSettingsState {} +class DeviceSettingsSaveSelectionSuccess extends DeviceSettingsState {} + +class ChangeNameState extends DeviceSettingsState {} diff --git a/lib/pages/device_managment/device_setting/device_management_content.dart b/lib/pages/device_managment/device_setting/device_management_content.dart new file mode 100644 index 00000000..9c758341 --- /dev/null +++ b/lib/pages/device_managment/device_setting/device_management_content.dart @@ -0,0 +1,127 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/device_info_model.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/sub_space_model.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/sub_space_dialog.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:syncrow_web/web_layout/default_container.dart'; + +class DeviceManagementContent extends StatelessWidget { + const DeviceManagementContent({ + super.key, + required this.device, + required this.subSpaces, + required this.deviceInfo, + }); + + final AllDevicesModel device; + final List subSpaces; + final DeviceInfoModel deviceInfo; + + @override + Widget build(BuildContext context) { + Widget infoRow( + {required String label, + required String value, + Widget? trailing, + required Color? valueColor}) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: context.theme.textTheme.bodyMedium!.copyWith( + fontSize: 14, + color: ColorsManager.grayColor, + ), + ), + Expanded( + child: Text( + value, + textAlign: TextAlign.end, + style: context.theme.textTheme.bodyMedium! + .copyWith(fontSize: 14, color: valueColor), + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 8), + trailing ?? const SizedBox.shrink(), + ], + ), + ); + } + + return DefaultContainer( + padding: EdgeInsets.zero, + child: Column( + children: [ + const SizedBox(height: 5), + Padding( + padding: const EdgeInsets.all(10.0), + child: InkWell( + onTap: () { + showSubSpaceDialog( + context, + communityUuid: device.community!.uuid!, + spaceUuid: device.spaces!.first.uuid!, + subSpaces: subSpaces, + selected: device.subspace!.uuid, + ); + }, + child: infoRow( + label: 'Sub-Space:', + value: deviceInfo.subspace.subspaceName, + valueColor: ColorsManager.textGray, + trailing: const Icon( + Icons.arrow_forward_ios, + size: 16, + color: ColorsManager.greyColor, + ), + ), + ), + ), + const Divider(color: ColorsManager.dividerColor), + Padding( + padding: const EdgeInsets.all(10.0), + child: infoRow( + label: 'Virtual Address:', + value: deviceInfo.productUuid, + valueColor: ColorsManager.blackColor, + trailing: InkWell( + onTap: () { + Clipboard.setData( + ClipboardData(text: device.productUuid ?? ''), + ); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Virtual Address copied to clipboard'), + ), + ); + }, + child: const Icon( + Icons.copy, + size: 16, + color: ColorsManager.greyColor, + ), + ), + ), + ), + const Divider(color: ColorsManager.dividerColor), + Padding( + padding: const EdgeInsets.all(10.0), + child: infoRow( + label: 'MAC Address:', + valueColor: ColorsManager.blackColor, + value: deviceInfo.macAddress, + ), + ), + const SizedBox(height: 5), + ], + ), + ); + } +} diff --git a/lib/pages/device_managment/device_setting/device_settings_panel.dart b/lib/pages/device_managment/device_setting/device_settings_panel.dart index 6d960a20..cebd80b3 100644 --- a/lib/pages/device_managment/device_setting/device_settings_panel.dart +++ b/lib/pages/device_managment/device_setting/device_settings_panel.dart @@ -1,13 +1,13 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.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/device_setting/device_icon_type_helper.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/device_management_content.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/remove_device_widget.dart'; import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/device_info_model.dart'; import 'package:syncrow_web/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart'; import 'package:syncrow_web/pages/device_managment/device_setting/bloc/setting_bloc_state.dart'; -import 'package:syncrow_web/pages/device_managment/device_setting/sub_space_dialog.dart'; import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/sub_space_model.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; @@ -18,325 +18,164 @@ class DeviceSettingsPanel extends StatelessWidget { final VoidCallback? onClose; final AllDevicesModel device; const DeviceSettingsPanel({super.key, this.onClose, required this.device}); + @override Widget build(BuildContext context) { final sectionTitle = context.theme.textTheme.titleMedium!.copyWith( fontWeight: FontWeight.bold, color: ColorsManager.grayColor, ); - Widget infoRow( - {required String label, - required String value, - Widget? trailing, - required Color? valueColor}) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 6.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - label, - style: context.theme.textTheme.bodyMedium!.copyWith( - fontSize: 14, - color: ColorsManager.grayColor, - ), - ), - Expanded( - child: Text( - value, - textAlign: TextAlign.end, - style: context.theme.textTheme.bodyMedium! - .copyWith(fontSize: 14, color: valueColor), - overflow: TextOverflow.ellipsis, - ), - ), - const SizedBox(width: 8), - trailing ?? const SizedBox.shrink(), - ], - ), - ); - } - return BlocProvider( - create: (context) => SettingBlocBloc( + create: (context) => SettingDeviceBloc( deviceId: device.uuid ?? '', ) ..add(DeviceSettingInitialInfo()) - ..add(FetchRoomsEvent( + ..add(SettingBlocFetchRooms( communityUuid: device.community!.uuid!, spaceUuid: device.spaces!.first.uuid!, )), - child: BlocBuilder( - builder: (context, state) { - final iconPath = - DeviceIconTypeHelper.getDeviceIconByTypeCode(device.productType); - final _bloc = BlocProvider.of(context); - DeviceInfoModel deviceInfo = DeviceInfoModel.empty(); - List subSpaces = []; - if (state is UpdateSettingState) { - deviceInfo = state.deviceInfo!; - subSpaces = state.roomsList; - } - return Stack( - children: [ - Container( - width: MediaQuery.of(context).size.width * 0.3, - color: ColorsManager.grey25, - padding: - const EdgeInsets.symmetric(horizontal: 20, vertical: 24), - child: ListView( - children: [ - // Header - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + child: Builder( + builder: (context) { + return BlocBuilder( + builder: (context, state) { + final _bloc = context.read(); + final iconPath = DeviceIconTypeHelper.getDeviceIconByTypeCode( + device.productType); + final deviceInfo = state is DeviceSettingsUpdate + ? state.deviceInfo ?? DeviceInfoModel.empty() + : DeviceInfoModel.empty(); + final subSpaces = + state is DeviceSettingsUpdate ? state.roomsList ?? [] : []; + return Stack( + children: [ + Container( + width: MediaQuery.of(context).size.width * 0.3, + color: ColorsManager.grey25, + padding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 24), + child: ListView( children: [ - IconButton( - icon: SvgPicture.asset(Assets.closeSettingsIcon), - onPressed: - onClose ?? () => Navigator.of(context).pop(), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + icon: SvgPicture.asset(Assets.closeSettingsIcon), + onPressed: + onClose ?? () => Navigator.of(context).pop(), + ), + ], ), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'Device Settings', - style: context.theme.textTheme.titleLarge!.copyWith( - fontWeight: FontWeight.bold, - color: ColorsManager.primaryColor, - ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Device Settings', + style: + context.theme.textTheme.titleLarge!.copyWith( + fontWeight: FontWeight.bold, + color: ColorsManager.primaryColor, + ), + ), + ], ), - ], - ), - const SizedBox(height: 24), - // Device Name + Icon - DefaultContainer( - child: Row( - children: [ - CircleAvatar( - radius: 40, - backgroundColor: - const Color.fromARGB(177, 213, 213, 213), - child: CircleAvatar( - backgroundColor: ColorsManager.whiteColors, - radius: 36, - child: SvgPicture.asset( - iconPath, - fit: BoxFit.cover, - ), - ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Device Name:', - style: context.textTheme.bodyMedium!.copyWith( - color: ColorsManager.grayColor, + const SizedBox(height: 24), + DefaultContainer( + child: Row( + children: [ + CircleAvatar( + radius: 40, + backgroundColor: + const Color.fromARGB(177, 213, 213, 213), + child: CircleAvatar( + backgroundColor: ColorsManager.whiteColors, + radius: 36, + child: SvgPicture.asset( + iconPath, + fit: BoxFit.cover, ), ), - TextFormField( - maxLength: 30, - style: const TextStyle( - color: ColorsManager.blackColor, - ), - textAlign: TextAlign.start, - focusNode: _bloc.focusNode, - controller: _bloc.nameController, - enabled: _bloc.editName, - onFieldSubmitted: (value) { - _bloc.add( - const ChangeNameEvent(value: false)); - }, - decoration: const InputDecoration( - border: InputBorder.none, - fillColor: Colors.white10, - counterText: '', - ), - ), - ], - ), - ), - const SizedBox(width: 8), - Visibility( - visible: _bloc.editName != true, - replacement: const SizedBox(), - child: GestureDetector( - onTap: () { - _bloc.add(const ChangeNameEvent(value: true)); - }, - child: SvgPicture.asset( - Assets.editNameIconSettings, - color: ColorsManager.grayColor, - height: 20, - width: 20, - ), - ), - ) - ], - ), - ), - const SizedBox(height: 32), - // Device Management - Text('Device Management', style: sectionTitle), - DefaultContainer( - padding: EdgeInsets.zero, - child: Column( - children: [ - const SizedBox(height: 5), - Padding( - padding: const EdgeInsets.all(10.0), - child: InkWell( - onTap: () { - showSubSpaceDialog( - context, - communityUuid: device.community!.uuid!, - spaceUuid: device.spaces!.first.uuid!, - subSpaces: subSpaces, - selected: device.subspace!.uuid, - ); - }, - child: infoRow( - label: 'Sub-Space:', - value: deviceInfo.subspace.subspaceName, - valueColor: ColorsManager.textGray, - trailing: const Icon( - Icons.arrow_forward_ios, - size: 16, - color: ColorsManager.greyColor, - ), ), - ), - ), - const Divider(color: ColorsManager.dividerColor), - Padding( - padding: const EdgeInsets.all(10.0), - child: infoRow( - label: 'Virtual Address:', - value: deviceInfo.productUuid, - valueColor: ColorsManager.blackColor, - trailing: InkWell( - onTap: () { - Clipboard.setData( - ClipboardData( - text: device.productUuid ?? ''), - ); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - 'Virtual Address copied to clipboard'), - ), - ); - }, - child: const Icon( - Icons.copy, - size: 16, - color: ColorsManager.greyColor, - ), - ), - ), - ), - const Divider(color: ColorsManager.dividerColor), - Padding( - padding: const EdgeInsets.all(10.0), - child: infoRow( - label: 'MAC Address:', - valueColor: ColorsManager.blackColor, - value: deviceInfo.macAddress, - ), - ), - const SizedBox(height: 5), - ], - ), - ), - const SizedBox(height: 32), - - // Remove Device Button - SizedBox( - width: double.infinity, - child: InkWell( - onTap: () { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: Text( - 'Remove Device', - style: context.textTheme.bodyMedium!.copyWith( - fontWeight: FontWeight.w700, - color: ColorsManager.red, - ), - ), - content: Text( - 'Are you sure you want to remove this device?', - style: context.textTheme.bodyMedium!.copyWith( - color: ColorsManager.grayColor, - ), - ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text( - 'Cancel', + const SizedBox(width: 12), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Device Name:', style: context.textTheme.bodyMedium! .copyWith( color: ColorsManager.grayColor, ), ), - ), - TextButton( - onPressed: () { - _bloc.add(DeleteDeviceEvent()); - Navigator.of(context).pop(); - }, - child: Text( - 'Remove', - style: context.textTheme.bodyMedium! - .copyWith( - color: ColorsManager.red, + TextFormField( + maxLength: 30, + style: const TextStyle( + color: ColorsManager.blackColor, + ), + textAlign: TextAlign.start, + focusNode: _bloc.focusNode, + controller: _bloc.nameController, + enabled: _bloc.editName, + onFieldSubmitted: (value) { + _bloc.add(const ChangeNameEvent( + value: false)); + }, + decoration: const InputDecoration( + border: InputBorder.none, + fillColor: Colors.white10, + counterText: '', ), ), + ], + ), + ), + const SizedBox(width: 8), + Visibility( + visible: _bloc.editName != true, + replacement: const SizedBox(), + child: GestureDetector( + onTap: () { + _bloc.add( + const ChangeNameEvent(value: true)); + }, + child: SvgPicture.asset( + Assets.editNameIconSettings, + color: ColorsManager.grayColor, + height: 20, + width: 20, ), - ], - ); - }, - ); - }, - child: DefaultContainer( - padding: const EdgeInsets.all(25), - child: Center( - child: Text( - 'Remove Device', - style: context.textTheme.bodyMedium!.copyWith( - fontSize: 14, - color: ColorsManager.red, - fontWeight: FontWeight.w700), - ), + ), + ) + ], + ), + ), + const SizedBox(height: 32), + Text('Device Management', style: sectionTitle), + DeviceManagementContent( + device: device, + subSpaces: subSpaces.cast(), + deviceInfo: deviceInfo, + ), + const SizedBox(height: 32), + RemoveDeviceWidget(bloc: _bloc), + ], + ), + ), + if (state is DeviceSettingsLoading) + Positioned.fill( + child: Container( + color: Colors.black.withOpacity(0.1), + child: const Center( + child: CircularProgressIndicator( + color: ColorsManager.primaryColor, ), ), ), ), - ], - ), - ), - if (state is SettingLoadingState) - Positioned.fill( - child: Container( - color: Colors.black.withOpacity(0.1), - child: const Center( - child: CircularProgressIndicator( - color: ColorsManager.primaryColor, - ), - ), - ), - ), - ], + ], + ); + }, ); }, ), diff --git a/lib/pages/device_managment/device_setting/remove_device_widget.dart b/lib/pages/device_managment/device_setting/remove_device_widget.dart new file mode 100644 index 00000000..e65ee125 --- /dev/null +++ b/lib/pages/device_managment/device_setting/remove_device_widget.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:syncrow_web/web_layout/default_container.dart'; + +class RemoveDeviceWidget extends StatelessWidget { + const RemoveDeviceWidget({ + super.key, + required SettingDeviceBloc bloc, + }) : _bloc = bloc; + + final SettingDeviceBloc _bloc; + + @override + Widget build(BuildContext context) { + return SizedBox( + width: double.infinity, + child: InkWell( + onTap: () { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text( + 'Remove Device', + style: context.textTheme.bodyMedium!.copyWith( + fontWeight: FontWeight.w700, + color: ColorsManager.red, + ), + ), + content: Text( + 'Are you sure you want to remove this device?', + style: context.textTheme.bodyMedium!.copyWith( + color: ColorsManager.grayColor, + ), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text( + 'Cancel', + style: context.textTheme.bodyMedium!.copyWith( + color: ColorsManager.grayColor, + ), + ), + ), + TextButton( + onPressed: () { + _bloc.add(SettingBlocDeleteDevice()); + Navigator.of(context).pop(); + }, + child: Text( + 'Remove', + style: context.textTheme.bodyMedium!.copyWith( + color: ColorsManager.red, + ), + ), + ), + ], + ); + }, + ); + }, + child: DefaultContainer( + padding: const EdgeInsets.all(25), + child: Center( + child: Text( + 'Remove Device', + style: context.textTheme.bodyMedium!.copyWith( + fontSize: 14, + color: ColorsManager.red, + fontWeight: FontWeight.w700), + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/device_managment/device_setting/sub_space_dialog.dart b/lib/pages/device_managment/device_setting/sub_space_dialog.dart index f2fdfa3e..28350d4d 100644 --- a/lib/pages/device_managment/device_setting/sub_space_dialog.dart +++ b/lib/pages/device_managment/device_setting/sub_space_dialog.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart'; import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/sub_space_model.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/subspace_dialog_buttons.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; @@ -77,71 +78,7 @@ class _SubSpaceDialogState extends State { }).toList(), const SizedBox(height: 12), const Divider(height: 1, thickness: 1), - SizedBox( - height: 50, - child: Row( - children: [ - Expanded( - child: Container( - decoration: const BoxDecoration( - border: Border( - right: BorderSide( - color: ColorsManager.dividerColor, - width: 0.5, - ), - ), - ), - child: TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text( - 'Cancel', - style: context.textTheme.bodyMedium?.copyWith( - color: ColorsManager.textGray, - fontSize: 18, - fontWeight: FontWeight.w500, - ), - ), - ), - ), - ), - Expanded( - child: Container( - decoration: const BoxDecoration( - border: Border( - left: BorderSide( - color: ColorsManager.dividerColor, - width: 0.5, - ), - ), - ), - child: TextButton( - onPressed: _selectedId == null - ? null - : () { - final selectedModel = widget.subSpaces - .firstWhere( - (space) => space.id == _selectedId, - orElse: () => SubSpaceModel( - id: null, name: '', devices: [])); - widget.onConfirmed(selectedModel); - Navigator.of(context).pop(); - }, - child: Text( - 'Confirm', - style: context.textTheme.bodyMedium?.copyWith( - color: ColorsManager.secondaryColor, - fontSize: 14, - fontWeight: FontWeight.w400, - ), - ), - ), - ), - ), - ], - ), - ), + SubSpaceDialogButtons(selectedId: _selectedId, widget: widget), ], ), ), @@ -164,8 +101,8 @@ void showSubSpaceDialog( selected: selected, onConfirmed: (selectedModel) { if (selectedModel != null) { - context.read().add( - AssignRoomEvent( + context.read().add( + SettingBlocAssignRoom( communityUuid: communityUuid, spaceUuid: spaceUuid, subSpaceUuid: selectedModel.id ?? '', diff --git a/lib/pages/device_managment/device_setting/subspace_dialog_buttons.dart b/lib/pages/device_managment/device_setting/subspace_dialog_buttons.dart new file mode 100644 index 00000000..80ece0cb --- /dev/null +++ b/lib/pages/device_managment/device_setting/subspace_dialog_buttons.dart @@ -0,0 +1,114 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/sub_space_model.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/sub_space_dialog.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class SubSpaceDialogButtons extends StatelessWidget { + const SubSpaceDialogButtons({ + super.key, + required String? selectedId, + required this.widget, + }) : _selectedId = selectedId; + + final String? _selectedId; + final SubSpaceDialog widget; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 50, + child: Row( + children: [ + Expanded( + child: Container( + decoration: const BoxDecoration( + border: Border( + right: BorderSide( + color: ColorsManager.dividerColor, + width: 0.5, + ), + ), + ), + child: TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text( + 'Cancel', + style: context.textTheme.bodyMedium?.copyWith( + color: ColorsManager.textGray, + fontSize: 18, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ), + Expanded( + child: Container( + decoration: const BoxDecoration( + border: Border( + left: BorderSide( + color: ColorsManager.dividerColor, + width: 0.5, + ), + ), + ), + child: TextButton( + onPressed: _selectedId == null + ? null + : () { + final selectedModel = widget.subSpaces.firstWhere( + (space) => space.id == _selectedId, + orElse: () => + SubSpaceModel(id: null, name: '', devices: [])); + widget.onConfirmed(selectedModel); + Navigator.of(context).pop(); + }, + child: Text( + 'Confirm', + style: context.textTheme.bodyMedium?.copyWith( + color: ColorsManager.secondaryColor, + fontSize: 14, + fontWeight: FontWeight.w400, + ), + ), + ), + ), + ), + ], + ), + ); + } +} + +void showSubSpaceDialog( + BuildContext context, { + required List subSpaces, + String? selected, + required String communityUuid, + required String spaceUuid, +}) { + showDialog( + context: context, + barrierDismissible: true, + builder: (ctx) => SubSpaceDialog( + subSpaces: subSpaces, + selected: selected, + onConfirmed: (selectedModel) { + if (selectedModel != null) { + context.read().add( + SettingBlocAssignRoom( + communityUuid: communityUuid, + spaceUuid: spaceUuid, + subSpaceUuid: selectedModel.id ?? '', + ), + ); + } + }, + ), + ); +} diff --git a/lib/services/devices_mang_api.dart b/lib/services/devices_mang_api.dart index 4d5200d4..6f60e34f 100644 --- a/lib/services/devices_mang_api.dart +++ b/lib/services/devices_mang_api.dart @@ -370,7 +370,8 @@ class DevicesManagementApi { }); return response; } - static Future resetDevise({ + + static Future resetDevice({ String? devicesUuid, }) async { final response = await HTTPService().post( @@ -385,7 +386,4 @@ class DevicesManagementApi { ); return response; } - - - } diff --git a/lib/services/space_mana_api.dart b/lib/services/space_mana_api.dart index 31f3cebd..8f8d1d07 100644 --- a/lib/services/space_mana_api.dart +++ b/lib/services/space_mana_api.dart @@ -373,7 +373,6 @@ class CommunitySpaceManagementApi { required String spaceId, required String projectId}) async { try { - // Construct the API path final path = ApiEndpoints.listSubspace .replaceFirst('{communityUuid}', communityId) .replaceFirst('{spaceUuid}', spaceId) @@ -389,9 +388,6 @@ class CommunitySpaceManagementApi { for (var subspace in json['data']) { rooms.add(SubSpaceModel.fromJson(subspace)); } - } else { - debugPrint( - "Warning: 'data' key is missing or null in response JSON."); } return rooms; }, @@ -399,7 +395,7 @@ class CommunitySpaceManagementApi { return response; } catch (error, stackTrace) { - return []; // Return an empty list if there's an error + return []; } } From 2797dce63739d07a96cd2c1269e65e9ace72e729 Mon Sep 17 00:00:00 2001 From: mohammad Date: Tue, 3 Jun 2025 16:55:24 +0300 Subject: [PATCH 58/58] Rename SettingBlocEvent to SettingEvent for consistency and clarity in event handling. --- .../bloc/setting_bloc_bloc.dart | 2 +- .../bloc/setting_bloc_event.dart | 25 +++++++++---------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/lib/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart b/lib/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart index 92d94a8f..c996cf72 100644 --- a/lib/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart +++ b/lib/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart @@ -10,7 +10,7 @@ import 'package:syncrow_web/services/space_mana_api.dart'; import 'package:syncrow_web/utils/snack_bar.dart'; part 'setting_bloc_event.dart'; -class SettingDeviceBloc extends Bloc { +class SettingDeviceBloc extends Bloc { final String deviceId; SettingDeviceBloc({ required this.deviceId, diff --git a/lib/pages/device_managment/device_setting/bloc/setting_bloc_event.dart b/lib/pages/device_managment/device_setting/bloc/setting_bloc_event.dart index ab62d8a0..7fb62ed9 100644 --- a/lib/pages/device_managment/device_setting/bloc/setting_bloc_event.dart +++ b/lib/pages/device_managment/device_setting/bloc/setting_bloc_event.dart @@ -1,12 +1,12 @@ part of 'setting_bloc_bloc.dart'; -abstract class SettingBlocEvent extends Equatable { - const SettingBlocEvent(); +abstract class DeviceSettingEvent extends Equatable { + const DeviceSettingEvent(); @override List get props => []; } -class SettingBlocSaveDeviceName extends SettingBlocEvent { +class SettingBlocSaveDeviceName extends DeviceSettingEvent { final String deviceName; final String deviceId; @@ -17,11 +17,11 @@ class SettingBlocSaveDeviceName extends SettingBlocEvent { List get props => [deviceName, deviceId]; } -class SettingBlocStartEditingName extends SettingBlocEvent {} +class SettingBlocStartEditingName extends DeviceSettingEvent {} -class SettingBlocCancelEditingName extends SettingBlocEvent {} +class SettingBlocCancelEditingName extends DeviceSettingEvent {} -class SettingBlocChangeEditingNameValue extends SettingBlocEvent { +class SettingBlocChangeEditingNameValue extends DeviceSettingEvent { final String value; const SettingBlocChangeEditingNameValue(this.value); @@ -29,7 +29,7 @@ class SettingBlocChangeEditingNameValue extends SettingBlocEvent { List get props => [value]; } -class SettingBlocFetchRooms extends SettingBlocEvent { +class SettingBlocFetchRooms extends DeviceSettingEvent { final String communityUuid; final String spaceUuid; @@ -40,20 +40,20 @@ class SettingBlocFetchRooms extends SettingBlocEvent { List get props => [communityUuid, spaceUuid]; } -class SettingBlocSaveName extends SettingBlocEvent { +class SettingBlocSaveName extends DeviceSettingEvent { const SettingBlocSaveName(); } -class DeviceSettingInitialInfo extends SettingBlocEvent {} +class DeviceSettingInitialInfo extends DeviceSettingEvent {} -class ChangeNameEvent extends SettingBlocEvent { +class ChangeNameEvent extends DeviceSettingEvent { final bool? value; const ChangeNameEvent({this.value}); } -class SettingBlocDeleteDevice extends SettingBlocEvent {} +class SettingBlocDeleteDevice extends DeviceSettingEvent {} -class SettingBlocAssignRoom extends SettingBlocEvent { +class SettingBlocAssignRoom extends DeviceSettingEvent { final String communityUuid; final String spaceUuid; final String subSpaceUuid; @@ -67,4 +67,3 @@ class SettingBlocAssignRoom extends SettingBlocEvent { @override List get props => [spaceUuid, communityUuid, subSpaceUuid]; } -