From 568b6be354c9ec8f7eedd33f04027dfd1c5a0abc Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 21 May 2025 16:46:38 +0300 Subject: [PATCH 001/181] Created `AirQualityView` widget for the new Air Quality analytics module. --- .../modules/air_quality/views/air_quality_view.dart | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 lib/pages/analytics/modules/air_quality/views/air_quality_view.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 new file mode 100644 index 00000000..8844eb9f --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/views/air_quality_view.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class AirQualityView extends StatelessWidget { + const AirQualityView({super.key}); + + @override + Widget build(BuildContext context) { + return const Placeholder(); + } +} From 5a61647fe4d4e68d4412b8fb93ec9948a3273244 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 21 May 2025 16:49:30 +0300 Subject: [PATCH 002/181] Prepared and created the necessary component for the air quality loading strategy for the side bar selection, and for loading data in different parts of the UI. --- .../fetch_air_quality_data_helper.dart | 19 ++++++++++ .../analytics/enums/analytics_page_tab.dart | 5 +++ .../air_quality_data_loading_strategy.dart | 38 +++++++++++++++++++ ...alytics_data_loading_strategy_factory.dart | 2 + 4 files changed, 64 insertions(+) create mode 100644 lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart create mode 100644 lib/pages/analytics/modules/analytics/strategies/air_quality_data_loading_strategy.dart 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 new file mode 100644 index 00000000..722b9210 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart'; + +abstract final class FetchAirQualityDataHelper { + const FetchAirQualityDataHelper._(); + + static void loadAirQualityData( + BuildContext context, + CommunityModel community, + SpaceModel space, + ) { + // TODO: implement loadAirQualityData + } + + static void clearAllData(BuildContext context) { + // TODO: implement clearAllData + } +} diff --git a/lib/pages/analytics/modules/analytics/enums/analytics_page_tab.dart b/lib/pages/analytics/modules/analytics/enums/analytics_page_tab.dart index b26cfc95..6552f6cf 100644 --- a/lib/pages/analytics/modules/analytics/enums/analytics_page_tab.dart +++ b/lib/pages/analytics/modules/analytics/enums/analytics_page_tab.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/views/air_quality_view.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/views/analytics_energy_management_view.dart'; import 'package:syncrow_web/pages/analytics/modules/occupancy/views/analytics_occupancy_view.dart'; @@ -10,6 +11,10 @@ enum AnalyticsPageTab { occupancy( title: 'Occupancy', child: AnalyticsOccupancyView(), + ), + airQuality( + title: 'Air Quality', + child: AirQualityView(), ); const AnalyticsPageTab({ 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 new file mode 100644 index 00000000..636bc53d --- /dev/null +++ b/lib/pages/analytics/modules/analytics/strategies/air_quality_data_loading_strategy.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/analytics/modules/analytics/strategies/analytics_data_loading_strategy.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart'; + +final class AirQualityDataLoadingStrategy implements AnalyticsDataLoadingStrategy { + @override + void onCommunitySelected( + BuildContext context, + CommunityModel community, + List spaces, + ) { + // TODO: implement onCommunitySelected + } + + @override + void onSpaceSelected( + BuildContext context, + CommunityModel community, + SpaceModel space, + ) { + // TODO: implement onSpaceSelected + } + + @override + void onChildSpaceSelected( + BuildContext context, + CommunityModel community, + SpaceModel child, + ) { + // TODO: implement onChildSpaceSelected + } + + @override + void clearData(BuildContext context) { + // TODO: implement clearData + } +} diff --git a/lib/pages/analytics/modules/analytics/strategies/analytics_data_loading_strategy_factory.dart b/lib/pages/analytics/modules/analytics/strategies/analytics_data_loading_strategy_factory.dart index 8b8bb60f..19b0aff2 100644 --- a/lib/pages/analytics/modules/analytics/strategies/analytics_data_loading_strategy_factory.dart +++ b/lib/pages/analytics/modules/analytics/strategies/analytics_data_loading_strategy_factory.dart @@ -1,4 +1,5 @@ import 'package:syncrow_web/pages/analytics/modules/analytics/enums/analytics_page_tab.dart'; +import 'package:syncrow_web/pages/analytics/modules/analytics/strategies/air_quality_data_loading_strategy.dart'; import 'package:syncrow_web/pages/analytics/modules/analytics/strategies/analytics_data_loading_strategy.dart'; import 'package:syncrow_web/pages/analytics/modules/analytics/strategies/energy_management_data_loading_strategy.dart'; import 'package:syncrow_web/pages/analytics/modules/analytics/strategies/occupancy_data_loading_strategy.dart'; @@ -9,6 +10,7 @@ abstract final class AnalyticsDataLoadingStrategyFactory { return switch (tab) { AnalyticsPageTab.energyManagement => EnergyManagementDataLoadingStrategy(), AnalyticsPageTab.occupancy => OccupancyDataLoadingStrategy(), + AnalyticsPageTab.airQuality => AirQualityDataLoadingStrategy(), }; } } From d2eea337141650e9235b17709640eb28008e13e6 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 22 May 2025 12:24:13 +0300 Subject: [PATCH 003/181] Prepared `AirQualityView` layout and structure with PlaceHolder widgets. --- .../air_quality/views/air_quality_view.dart | 51 ++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) 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 8844eb9f..ef2b8f51 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 @@ -3,8 +3,57 @@ import 'package:flutter/material.dart'; class AirQualityView extends StatelessWidget { const AirQualityView({super.key}); + static const _padding = EdgeInsetsDirectional.all(32); + @override Widget build(BuildContext context) { - return const Placeholder(); + return LayoutBuilder( + builder: (context, constraints) { + final isMediumOrLess = constraints.maxWidth <= 900; + final height = MediaQuery.sizeOf(context).height; + if (isMediumOrLess) { + return SingleChildScrollView( + padding: _padding, + child: Column( + spacing: 32, + children: [ + SizedBox(height: height * 1.2, child: const Placeholder()), + SizedBox(height: height * 0.5, child: const Placeholder()), + SizedBox(height: height * 0.5, child: const Placeholder()), + ], + ), + ); + } + + return SingleChildScrollView( + child: Container( + padding: _padding, + height: height * 1, + child: const Column( + children: [ + Expanded( + child: Row( + spacing: 32, + children: [ + Expanded( + flex: 2, + child: Column( + spacing: 20, + children: [ + Expanded(child: Placeholder()), + Expanded(child: Placeholder()), + ], + ), + ), + Expanded(child: Placeholder()), + ], + ), + ), + ], + ), + ), + ); + }, + ); } } From e792dbd72f73c0d65dc10276a16ced48d951b3e0 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 22 May 2025 14:58:42 +0300 Subject: [PATCH 004/181] SP-1591/ Implement business logic in `AirQualityDataLoadingStrategy` for community structure loading strategy. --- .../fetch_air_quality_data_helper.dart | 10 ++-- .../air_quality_data_loading_strategy.dart | 55 +++++++++++++++++-- 2 files changed, 55 insertions(+), 10 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart b/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart index 722b9210..aa2da2da 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,15 +1,13 @@ import 'package:flutter/material.dart'; -import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart'; -import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart'; abstract final class FetchAirQualityDataHelper { const FetchAirQualityDataHelper._(); static void loadAirQualityData( - BuildContext context, - CommunityModel community, - SpaceModel space, - ) { + BuildContext context, { + required String communityUuid, + required String spaceUuid, + }) { // TODO: implement loadAirQualityData } 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 636bc53d..af355e6d 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,5 +1,9 @@ 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/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'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart'; @@ -10,7 +14,24 @@ final class AirQualityDataLoadingStrategy implements AnalyticsDataLoadingStrateg CommunityModel community, List spaces, ) { - // TODO: implement onCommunitySelected + context.read().add( + OnCommunitySelected( + community.uuid, + spaces.isNotEmpty ? [spaces.first] : [], + ), + ); + + final spaceTreeState = context.read().state; + if (spaceTreeState.selectedCommunities.contains(community.uuid)) { + clearData(context); + return; + } + + FetchAirQualityDataHelper.loadAirQualityData( + context, + communityUuid: community.uuid, + spaceUuid: spaces.isNotEmpty ? (spaces.first.uuid ?? '') : '', + ); } @override @@ -19,7 +40,32 @@ final class AirQualityDataLoadingStrategy implements AnalyticsDataLoadingStrateg CommunityModel community, SpaceModel space, ) { - // TODO: implement onSpaceSelected + final spaceTreeBloc = context.read(); + final selectedSpacesIds = spaceTreeBloc.state.selectedSpaces; + final isSpaceSelected = selectedSpacesIds.contains(space.uuid); + + if (selectedSpacesIds.isEmpty) { + spaceTreeBloc.add(OnCommunitySelected(community.uuid, [space])); + } else if (isSpaceSelected) { + spaceTreeBloc.add(const SpaceTreeClearSelectionEvent()); + } else { + spaceTreeBloc + ..add(const SpaceTreeClearSelectionEvent()) + ..add(OnSpaceSelected(community, space.uuid ?? '', [])); + } + + final spaceTreeState = context.read().state; + if (spaceTreeState.selectedCommunities.contains(community.uuid) || + spaceTreeState.selectedSpaces.contains(space.uuid)) { + clearData(context); + return; + } + + FetchAirQualityDataHelper.loadAirQualityData( + context, + communityUuid: community.uuid, + spaceUuid: space.uuid ?? '', + ); } @override @@ -28,11 +74,12 @@ final class AirQualityDataLoadingStrategy implements AnalyticsDataLoadingStrateg CommunityModel community, SpaceModel child, ) { - // TODO: implement onChildSpaceSelected + // Do nothing } @override void clearData(BuildContext context) { - // TODO: implement clearData + context.read().add(const SpaceTreeClearSelectionEvent()); + FetchAirQualityDataHelper.clearAllData(context); } } From 9adbbb9a2d00996df1e7e1b719b79652e3de2b2d Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 22 May 2025 15:19:50 +0300 Subject: [PATCH 005/181] Integrated and implemented devices dropdown into the newly created widget `AirQualityEndSideWidget`. --- .../fetch_air_quality_data_helper.dart | 39 +++++++- .../air_quality/views/air_quality_view.dart | 8 +- .../widgets/air_quality_end_side_widget.dart | 88 +++++++++++++++++++ 3 files changed, 131 insertions(+), 4 deletions(-) create mode 100644 lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart 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 aa2da2da..dd646063 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,4 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_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_analytics_devices_param.dart'; abstract final class FetchAirQualityDataHelper { const FetchAirQualityDataHelper._(); @@ -8,10 +12,41 @@ abstract final class FetchAirQualityDataHelper { required String communityUuid, required String spaceUuid, }) { - // TODO: implement loadAirQualityData + loadAnalyticsDevices( + context, + communityUuid: communityUuid, + spaceUuid: spaceUuid, + ); } static void clearAllData(BuildContext context) { - // TODO: implement clearAllData + context.read().add( + const ClearAnalyticsDeviceEvent(), + ); + context.read().add( + const RealtimeDeviceChangesClosed(), + ); + } + + static void loadAnalyticsDevices( + BuildContext context, { + required String communityUuid, + required String spaceUuid, + }) { + context.read().add( + LoadAnalyticsDevicesEvent( + param: GetAnalyticsDevicesParam( + communityUuid: communityUuid, + spaceUuid: spaceUuid, + deviceTypes: ['AQI'], + requestType: AnalyticsDeviceRequestType.energyManagement, + ), + onSuccess: (device) { + context.read() + ..add(const RealtimeDeviceChangesClosed()) + ..add(RealtimeDeviceChangesStarted(device.uuid)); + }, + ), + ); } } 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 ef2b8f51..3b950e55 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,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart'; class AirQualityView extends StatelessWidget { const AirQualityView({super.key}); @@ -17,7 +18,10 @@ class AirQualityView extends StatelessWidget { child: Column( spacing: 32, children: [ - SizedBox(height: height * 1.2, child: const Placeholder()), + SizedBox( + height: height * 1.2, + child: const AirQualityEndSideWidget(), + ), SizedBox(height: height * 0.5, child: const Placeholder()), SizedBox(height: height * 0.5, child: const Placeholder()), ], @@ -45,7 +49,7 @@ class AirQualityView extends StatelessWidget { ], ), ), - Expanded(child: Placeholder()), + Expanded(child: AirQualityEndSideWidget()), ], ), ), diff --git a/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart new file mode 100644 index 00000000..2d6ace36 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_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/helpers/fetch_energy_management_data_helper.dart'; +import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/analytics_device_dropdown.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:syncrow_web/utils/style.dart'; + +class AirQualityEndSideWidget extends StatelessWidget { + const AirQualityEndSideWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + decoration: subSectionContainerDecoration.copyWith( + borderRadius: BorderRadius.circular(30), + ), + padding: const EdgeInsetsDirectional.all(32), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildHeader(context), + Text( + 'Device ID:', + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w400, + fontSize: 12, + ), + ), + const SizedBox(height: 6), + SelectableText( + context.watch().state.selectedDevice?.uuid ?? + 'N/A', + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.blackColor, + fontWeight: FontWeight.w400, + fontSize: 12, + ), + ), + ], + ), + ); + } + + Widget _buildHeader(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + flex: 3, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: AlignmentDirectional.centerStart, + child: SelectableText( + 'AQI Sensor', + style: context.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w700, + color: ColorsManager.vividBlue.withValues(alpha: 0.6), + fontSize: 18, + ), + ), + ), + ), + const Spacer(), + Expanded( + flex: 2, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: AlignmentDirectional.centerEnd, + child: AnalyticsDeviceDropdown( + onChanged: (value) { + context.read().add( + SelectAnalyticsDeviceEvent(value), + ); + FetchEnergyManagementDataHelper.loadRealtimeDeviceChanges( + context, + deviceUuid: value.uuid, + ); + }, + ), + ), + ), + ], + ); + } +} From 717d6983783f5517759fbf64dcf3c15351f9f6a3 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 22 May 2025 15:23:42 +0300 Subject: [PATCH 006/181] can select child spaces with children in `AirQualityDataLoadingStrategy`. --- .../analytics/strategies/air_quality_data_loading_strategy.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 af355e6d..c207d2ae 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 @@ -74,7 +74,7 @@ final class AirQualityDataLoadingStrategy implements AnalyticsDataLoadingStrateg CommunityModel community, SpaceModel child, ) { - // Do nothing + if (child.children.isNotEmpty) return onSpaceSelected(context, community, child); } @override From 5eeac01666de755b42a2d7ebf8f7e00aec017baf Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 22 May 2025 15:35:04 +0300 Subject: [PATCH 007/181] cannot select a community in `AirQualityDataLoadingStrategy`. --- .../air_quality_data_loading_strategy.dart | 19 +------------------ 1 file changed, 1 insertion(+), 18 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 c207d2ae..0a29a933 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 @@ -14,24 +14,7 @@ final class AirQualityDataLoadingStrategy implements AnalyticsDataLoadingStrateg CommunityModel community, List spaces, ) { - context.read().add( - OnCommunitySelected( - community.uuid, - spaces.isNotEmpty ? [spaces.first] : [], - ), - ); - - final spaceTreeState = context.read().state; - if (spaceTreeState.selectedCommunities.contains(community.uuid)) { - clearData(context); - return; - } - - FetchAirQualityDataHelper.loadAirQualityData( - context, - communityUuid: community.uuid, - spaceUuid: spaces.isNotEmpty ? (spaces.first.uuid ?? '') : '', - ); + // Do nothing } @override From 4c5b3908876a51ea72cf7ddb7ed6307627bc6bea Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 22 May 2025 15:42:49 +0300 Subject: [PATCH 008/181] Fixed typos. --- lib/pages/visitor_password/view/visitor_password_dialog.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pages/visitor_password/view/visitor_password_dialog.dart b/lib/pages/visitor_password/view/visitor_password_dialog.dart index 4db5017c..6385f3a6 100644 --- a/lib/pages/visitor_password/view/visitor_password_dialog.dart +++ b/lib/pages/visitor_password/view/visitor_password_dialog.dart @@ -38,7 +38,7 @@ class VisitorPasswordDialog extends StatelessWidget { if (visitorBloc.passwordStatus!.failedOperations.isNotEmpty) Column( children: [ - const Text('Failed Devises'), + const Text('Failed Devices'), SizedBox( width: 200, height: 50, @@ -63,7 +63,7 @@ class VisitorPasswordDialog extends StatelessWidget { if (visitorBloc.passwordStatus!.successOperations.isNotEmpty) Column( children: [ - const Text('Success Devises'), + const Text('Success Devices'), SizedBox( width: 200, height: 50, From d43c1847ff76848818e0c013115c814910e06646 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 22 May 2025 15:44:19 +0300 Subject: [PATCH 009/181] SP-1591 --- .../air_quality_data_loading_strategy.dart | 2 +- .../occupancy_data_loading_strategy.dart | 22 +++---------------- 2 files changed, 4 insertions(+), 20 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 0a29a933..90a4da61 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 @@ -28,7 +28,7 @@ final class AirQualityDataLoadingStrategy implements AnalyticsDataLoadingStrateg final isSpaceSelected = selectedSpacesIds.contains(space.uuid); if (selectedSpacesIds.isEmpty) { - spaceTreeBloc.add(OnCommunitySelected(community.uuid, [space])); + spaceTreeBloc.add(OnSpaceSelected(community, space.uuid ?? '', [])); } else if (isSpaceSelected) { spaceTreeBloc.add(const SpaceTreeClearSelectionEvent()); } else { 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 fb93ec30..534d275f 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 @@ -14,23 +14,7 @@ class OccupancyDataLoadingStrategy implements AnalyticsDataLoadingStrategy { CommunityModel community, List spaces, ) { - context.read().add( - OnCommunitySelected( - community.uuid, - spaces.isNotEmpty ? [spaces.first] : [], - ), - ); - - final spaceTreeState = context.read().state; - if (spaceTreeState.selectedCommunities.contains(community.uuid)) { - clearData(context); - return; - } - FetchOccupancyDataHelper.loadOccupancyData( - context, - communityId: community.uuid, - spaceId: spaces.isNotEmpty ? spaces.first.uuid ?? '' : '', - ); + // Do nothing } @override @@ -44,7 +28,7 @@ class OccupancyDataLoadingStrategy implements AnalyticsDataLoadingStrategy { final isSpaceSelected = selectedSpacesIds.contains(space.uuid); if (selectedSpacesIds.isEmpty) { - spaceTreeBloc.add(OnCommunitySelected(community.uuid, [space])); + spaceTreeBloc.add(OnSpaceSelected(community, space.uuid ?? '', [])); } else if (isSpaceSelected) { spaceTreeBloc.add(const SpaceTreeClearSelectionEvent()); } else { @@ -73,7 +57,7 @@ class OccupancyDataLoadingStrategy implements AnalyticsDataLoadingStrategy { CommunityModel community, SpaceModel child, ) { - // Do nothing + if (child.children.isNotEmpty) return onSpaceSelected(context, community, child); } @override From 8c53d5322a0e88517b1a595ed679178feefb4d8c Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 22 May 2025 15:53:18 +0300 Subject: [PATCH 010/181] SP-1591 --- .../air_quality_data_loading_strategy.dart | 21 ++++++------------- 1 file changed, 6 insertions(+), 15 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 90a4da61..19f9775d 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 @@ -24,26 +24,17 @@ final class AirQualityDataLoadingStrategy implements AnalyticsDataLoadingStrateg SpaceModel space, ) { final spaceTreeBloc = context.read(); - final selectedSpacesIds = spaceTreeBloc.state.selectedSpaces; - final isSpaceSelected = selectedSpacesIds.contains(space.uuid); + final isSpaceSelected = spaceTreeBloc.state.selectedSpaces.contains(space.uuid); - if (selectedSpacesIds.isEmpty) { - spaceTreeBloc.add(OnSpaceSelected(community, space.uuid ?? '', [])); - } else if (isSpaceSelected) { - spaceTreeBloc.add(const SpaceTreeClearSelectionEvent()); - } else { - spaceTreeBloc - ..add(const SpaceTreeClearSelectionEvent()) - ..add(OnSpaceSelected(community, space.uuid ?? '', [])); - } - - final spaceTreeState = context.read().state; - if (spaceTreeState.selectedCommunities.contains(community.uuid) || - spaceTreeState.selectedSpaces.contains(space.uuid)) { + if (isSpaceSelected) { clearData(context); return; } + spaceTreeBloc + ..add(const SpaceTreeClearSelectionEvent()) + ..add(OnSpaceSelected(community, space.uuid ?? '', [])); + FetchAirQualityDataHelper.loadAirQualityData( context, communityUuid: community.uuid, From 5b13962d41c774eaf916b85de3998fe4d19f8fae Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 22 May 2025 15:57:03 +0300 Subject: [PATCH 011/181] removed unnecessary * 1 calculation of height. --- .../analytics/modules/air_quality/views/air_quality_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 3b950e55..38f62cd7 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 @@ -32,7 +32,7 @@ class AirQualityView extends StatelessWidget { return SingleChildScrollView( child: Container( padding: _padding, - height: height * 1, + height: height, child: const Column( children: [ Expanded( From e9abac79337d0491a65ec2cb317776181d7669f0 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 22 May 2025 16:44:22 +0300 Subject: [PATCH 012/181] added analytics icon. --- assets/icons/landing_analytics.svg | 19 +++++ lib/utils/constants/assets.dart | 112 ++++++++++------------------- 2 files changed, 58 insertions(+), 73 deletions(-) create mode 100644 assets/icons/landing_analytics.svg diff --git a/assets/icons/landing_analytics.svg b/assets/icons/landing_analytics.svg new file mode 100644 index 00000000..6f9fbbf0 --- /dev/null +++ b/assets/icons/landing_analytics.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/lib/utils/constants/assets.dart b/lib/utils/constants/assets.dart index c86b7458..51053c9f 100644 --- a/lib/utils/constants/assets.dart +++ b/lib/utils/constants/assets.dart @@ -14,13 +14,14 @@ class Assets { static const String rightLine = "assets/images/right_line.png"; static const String google = "assets/images/google.svg"; static const String facebook = "assets/images/facebook.svg"; - static const String invisiblePassword = - "assets/images/Password_invisible.svg"; + static const String invisiblePassword = "assets/images/Password_invisible.svg"; static const String visiblePassword = "assets/images/password_visible.svg"; static const String accessIcon = "assets/images/access_icon.svg"; static const String spaseManagementIcon = "assets/images/spase_management_icon.svg"; static const String devicesIcon = "assets/images/devices_icon.svg"; + static const String analyticsIcon = "assets/icons/landing_analytics.svg"; + static const String moveinIcon = "assets/images/movein_icon.svg"; static const String constructionIcon = "assets/images/construction_icon.svg"; static const String energyIcon = "assets/images/energy_icon.svg"; @@ -32,8 +33,7 @@ class Assets { static const String emptyTable = "assets/images/empty_table.svg"; // General assets - static const String motionlessDetection = - "assets/icons/motionless_detection.svg"; + static const String motionlessDetection = "assets/icons/motionless_detection.svg"; static const String acHeating = "assets/icons/ac_heating.svg"; static const String acPowerOff = "assets/icons/ac_power_off.svg"; static const String acFanMiddle = "assets/icons/ac_fan_middle.svg"; @@ -70,22 +70,19 @@ class Assets { "assets/icons/automation_functions/temp_password_unlock.svg"; static const String doorlockNormalOpen = "assets/icons/automation_functions/doorlock_normal_open.svg"; - static const String doorbell = - "assets/icons/automation_functions/doorbell.svg"; + static const String doorbell = "assets/icons/automation_functions/doorbell.svg"; static const String remoteUnlockViaApp = "assets/icons/automation_functions/remote_unlock_via_app.svg"; static const String doubleLock = "assets/icons/automation_functions/double_lock.svg"; static const String selfTestResult = "assets/icons/automation_functions/self_test_result.svg"; - static const String lockAlarm = - "assets/icons/automation_functions/lock_alarm.svg"; + static const String lockAlarm = "assets/icons/automation_functions/lock_alarm.svg"; static const String presenceState = "assets/icons/automation_functions/presence_state.svg"; static const String currentTemp = "assets/icons/automation_functions/current_temp.svg"; - static const String presence = - "assets/icons/automation_functions/presence.svg"; + static const String presence = "assets/icons/automation_functions/presence.svg"; static const String residualElectricity = "assets/icons/automation_functions/residual_electricity.svg"; static const String hijackAlarm = @@ -102,15 +99,12 @@ class Assets { // Presence Sensor Assets static const String sensorMotionIcon = "assets/icons/sensor_motion_ic.svg"; - static const String sensorPresenceIcon = - "assets/icons/sensor_presence_ic.svg"; + static const String sensorPresenceIcon = "assets/icons/sensor_presence_ic.svg"; static const String sensorVacantIcon = "assets/icons/sensor_vacant_ic.svg"; static const String illuminanceRecordIcon = "assets/icons/illuminance_record_ic.svg"; - static const String presenceRecordIcon = - "assets/icons/presence_record_ic.svg"; - static const String helpDescriptionIcon = - "assets/icons/help_description_ic.svg"; + static const String presenceRecordIcon = "assets/icons/presence_record_ic.svg"; + static const String helpDescriptionIcon = "assets/icons/help_description_ic.svg"; static const String lightPulp = "assets/icons/light_pulb.svg"; static const String acDevice = "assets/icons/ac_device.svg"; @@ -160,12 +154,10 @@ class Assets { static const String unit = 'assets/icons/unit_icon.svg'; static const String villa = 'assets/icons/villa_icon.svg'; static const String iconEdit = 'assets/icons/icon_edit_icon.svg'; - static const String textFieldSearch = - 'assets/icons/textfield_search_icon.svg'; + static const String textFieldSearch = 'assets/icons/textfield_search_icon.svg'; static const String roundedAddIcon = 'assets/icons/rounded_add_icon.svg'; static const String addIcon = 'assets/icons/add_icon.svg'; - static const String smartThermostatIcon = - 'assets/icons/smart_thermostat_icon.svg'; + static const String smartThermostatIcon = 'assets/icons/smart_thermostat_icon.svg'; static const String smartLightIcon = 'assets/icons/smart_light_icon.svg'; static const String presenceSensor = 'assets/icons/presence_sensor.svg'; static const String Gang3SwitchIcon = 'assets/icons/3_Gang_switch_icon.svg'; @@ -213,8 +205,7 @@ class Assets { //assets/icons/water_leak_normal.svg static const String waterLeakNormal = 'assets/icons/water_leak_normal.svg'; //assets/icons/water_leak_detected.svg - static const String waterLeakDetected = - 'assets/icons/water_leak_detected.svg'; + static const String waterLeakDetected = 'assets/icons/water_leak_detected.svg'; //assets/icons/automation_records.svg static const String automationRecords = 'assets/icons/automation_records.svg'; @@ -285,16 +276,13 @@ class Assets { "assets/icons/functions_icons/sensitivity.svg"; static const String assetsSensitivityOperationIcon = "assets/icons/functions_icons/sesitivity_operation_icon.svg"; - static const String assetsAcPower = - "assets/icons/functions_icons/ac_power.svg"; + static const String assetsAcPower = "assets/icons/functions_icons/ac_power.svg"; static const String assetsAcPowerOFF = "assets/icons/functions_icons/ac_power_off.svg"; static const String assetsChildLock = "assets/icons/functions_icons/child_lock.svg"; - static const String assetsFreezing = - "assets/icons/functions_icons/freezing.svg"; - static const String assetsFanSpeed = - "assets/icons/functions_icons/fan_speed.svg"; + static const String assetsFreezing = "assets/icons/functions_icons/freezing.svg"; + static const String assetsFanSpeed = "assets/icons/functions_icons/fan_speed.svg"; static const String assetsAcCooling = "assets/icons/functions_icons/ac_cooling.svg"; static const String assetsAcHeating = @@ -303,8 +291,7 @@ class Assets { "assets/icons/functions_icons/celsius_degrees.svg"; static const String assetsTempreture = "assets/icons/functions_icons/tempreture.svg"; - static const String assetsAcFanLow = - "assets/icons/functions_icons/ac_fan_low.svg"; + static const String assetsAcFanLow = "assets/icons/functions_icons/ac_fan_low.svg"; static const String assetsAcFanMiddle = "assets/icons/functions_icons/ac_fan_middle.svg"; static const String assetsAcFanHigh = @@ -323,8 +310,7 @@ class Assets { "assets/icons/functions_icons/far_detection.svg"; static const String assetsFarDetectionFunction = "assets/icons/functions_icons/far_detection_function.svg"; - static const String assetsIndicator = - "assets/icons/functions_icons/indicator.svg"; + static const String assetsIndicator = "assets/icons/functions_icons/indicator.svg"; static const String assetsMotionDetection = "assets/icons/functions_icons/motion_detection.svg"; static const String assetsMotionlessDetection = @@ -337,8 +323,7 @@ class Assets { "assets/icons/functions_icons/master_state.svg"; static const String assetsSwitchAlarmSound = "assets/icons/functions_icons/switch_alarm_sound.svg"; - static const String assetsResetOff = - "assets/icons/functions_icons/reset_off.svg"; + static const String assetsResetOff = "assets/icons/functions_icons/reset_off.svg"; // Assets for automation_functions static const String assetsCardUnlock = @@ -382,14 +367,12 @@ class Assets { static const String activeUser = 'assets/icons/active_user.svg'; static const String deActiveUser = 'assets/icons/deactive_user.svg'; static const String invitedIcon = 'assets/icons/invited_icon.svg'; - static const String rectangleCheckBox = - 'assets/icons/rectangle_check_box.png'; + static const String rectangleCheckBox = 'assets/icons/rectangle_check_box.png'; static const String CheckBoxChecked = 'assets/icons/box_checked.png'; static const String emptyBox = 'assets/icons/empty_box.png'; static const String completeProcessIcon = 'assets/icons/compleate_process_icon.svg'; - static const String currentProcessIcon = - 'assets/icons/current_process_icon.svg'; + static const String currentProcessIcon = 'assets/icons/current_process_icon.svg'; static const String uncomplete_ProcessIcon = 'assets/icons/uncompleate_process_icon.svg'; static const String wrongProcessIcon = 'assets/icons/wrong_process_icon.svg'; @@ -410,11 +393,9 @@ class Assets { static const String successIcon = 'assets/icons/success_icon.svg'; static const String spaceLocationIcon = 'assets/icons/spaseLocationIcon.svg'; static const String scenesPlayIcon = 'assets/icons/scenesPlayIcon.png'; - static const String scenesPlayIconCheck = - 'assets/icons/scenesPlayIconCheck.png'; + static const String scenesPlayIconCheck = 'assets/icons/scenesPlayIconCheck.png'; static const String presenceStateIcon = 'assets/icons/presence_state.svg'; - static const String currentDistanceIcon = - 'assets/icons/current_distance_icon.svg'; + static const String currentDistanceIcon = 'assets/icons/current_distance_icon.svg'; static const String farDetectionIcon = 'assets/icons/far_detection_icon.svg'; static const String motionDetectionSensitivityIcon = @@ -437,44 +418,29 @@ class Assets { static const String cpsMode4 = 'assets/icons/cps_mode4.svg'; static const String closeToMotion = 'assets/icons/close_to_motion.svg'; static const String farAwayMotion = 'assets/icons/far_away_motion.svg'; - static const String communicationFault = - 'assets/icons/communication_fault.svg'; + static const String communicationFault = 'assets/icons/communication_fault.svg'; static const String radarFault = 'assets/icons/radar_fault.svg'; - static const String selfTestingSuccess = - 'assets/icons/self_testing_success.svg'; - static const String selfTestingFailure = - 'assets/icons/self_testing_failure.svg'; - static const String selfTestingTimeout = - 'assets/icons/self_testing_timeout.svg'; + static const String selfTestingSuccess = 'assets/icons/self_testing_success.svg'; + static const String selfTestingFailure = 'assets/icons/self_testing_failure.svg'; + static const String selfTestingTimeout = 'assets/icons/self_testing_timeout.svg'; static const String movingSpeed = 'assets/icons/moving_speed.svg'; static const String boundary = 'assets/icons/boundary.svg'; static const String motionMeter = 'assets/icons/motion_meter.svg'; - static const String spatialStaticValue = - 'assets/icons/spatial_static_value.svg'; - static const String spatialMotionValue = - 'assets/icons/spatial_motion_value.svg'; + static const String spatialStaticValue = 'assets/icons/spatial_static_value.svg'; + static const String spatialMotionValue = 'assets/icons/spatial_motion_value.svg'; static const String presenceJudgementThrshold = 'assets/icons/presence_judgement_threshold.svg'; static const String spaceType = 'assets/icons/space_type.svg'; static const String sportsPara = 'assets/icons/sports_para.svg'; - static const String sensitivityFeature1 = - 'assets/icons/sensitivity_feature_1.svg'; - static const String sensitivityFeature2 = - 'assets/icons/sensitivity_feature_2.svg'; - static const String sensitivityFeature3 = - 'assets/icons/sensitivity_feature_3.svg'; - static const String sensitivityFeature4 = - 'assets/icons/sensitivity_feature_4.svg'; - static const String sensitivityFeature5 = - 'assets/icons/sensitivity_feature_5.svg'; - static const String sensitivityFeature6 = - 'assets/icons/sensitivity_feature_6.svg'; - static const String sensitivityFeature7 = - 'assets/icons/sensitivity_feature_7.svg'; - static const String sensitivityFeature8 = - 'assets/icons/sensitivity_feature_8.svg'; - static const String sensitivityFeature9 = - 'assets/icons/sensitivity_feature_9.svg'; + static const String sensitivityFeature1 = 'assets/icons/sensitivity_feature_1.svg'; + static const String sensitivityFeature2 = 'assets/icons/sensitivity_feature_2.svg'; + static const String sensitivityFeature3 = 'assets/icons/sensitivity_feature_3.svg'; + static const String sensitivityFeature4 = 'assets/icons/sensitivity_feature_4.svg'; + static const String sensitivityFeature5 = 'assets/icons/sensitivity_feature_5.svg'; + static const String sensitivityFeature6 = 'assets/icons/sensitivity_feature_6.svg'; + static const String sensitivityFeature7 = 'assets/icons/sensitivity_feature_7.svg'; + static const String sensitivityFeature8 = 'assets/icons/sensitivity_feature_8.svg'; + static const String sensitivityFeature9 = 'assets/icons/sensitivity_feature_9.svg'; static const String deviceTagIcon = 'assets/icons/device_tag_ic.svg'; static const String targetConfirmTimeIcon = 'assets/icons/target_confirm_time_icon.svg'; @@ -482,5 +448,5 @@ class Assets { static const String indentLevelIcon = 'assets/icons/indent_level_icon.svg'; static const String triggerLevelIcon = 'assets/icons/trigger_level_icon.svg'; static const String blankCalendar = 'assets/icons/blank_calendar.svg'; - static const String refreshStatusIcon = 'assets/icons/refresh_status_icon.svg'; + static const String refreshStatusIcon = 'assets/icons/refresh_status_icon.svg'; } From 7aa9e7e5dc90eb59c47f0362f874324cc12a04db Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 22 May 2025 16:44:32 +0300 Subject: [PATCH 013/181] fixed typos. --- lib/pages/visitor_password/view/visitor_password_dialog.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pages/visitor_password/view/visitor_password_dialog.dart b/lib/pages/visitor_password/view/visitor_password_dialog.dart index 4db5017c..fff845f5 100644 --- a/lib/pages/visitor_password/view/visitor_password_dialog.dart +++ b/lib/pages/visitor_password/view/visitor_password_dialog.dart @@ -32,7 +32,7 @@ class VisitorPasswordDialog extends StatelessWidget { .stateDialog( context: context, message: 'Password Created Successfully', - title: 'Send Success', + title: 'Sent Successfully', widgeta: Column( children: [ if (visitorBloc.passwordStatus!.failedOperations.isNotEmpty) @@ -95,7 +95,7 @@ class VisitorPasswordDialog extends StatelessWidget { visitorBloc.stateDialog( context: context, message: state.message, - title: 'Something Wrong', + title: 'Something went wrong', ); } }, From 92abcdc4f9173ab26bb04f70a69bb0f140e95293 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 25 May 2025 10:57:23 +0300 Subject: [PATCH 014/181] SP-1492-landing_page_analytics_button_design. --- lib/pages/home/bloc/home_bloc.dart | 78 ++--------------------- lib/pages/home/view/home_card.dart | 30 ++------- lib/pages/home/view/home_page_mobile.dart | 57 +---------------- lib/pages/home/view/home_page_web.dart | 3 +- 4 files changed, 16 insertions(+), 152 deletions(-) diff --git a/lib/pages/home/bloc/home_bloc.dart b/lib/pages/home/bloc/home_bloc.dart index 33d55628..f6aab9eb 100644 --- a/lib/pages/home/bloc/home_bloc.dart +++ b/lib/pages/home/bloc/home_bloc.dart @@ -11,16 +11,11 @@ import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_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/home_api.dart'; -import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/constants/routes_const.dart'; import 'package:syncrow_web/utils/navigation_service.dart'; class HomeBloc extends Bloc { - // final Graph graph = Graph()..isTree = true; - // final BuchheimWalkerConfiguration builder = BuchheimWalkerConfiguration(); - // List sourcesList = []; - // List destinationsList = []; UserModel? user; String terms = ''; String policy = ''; @@ -33,22 +28,6 @@ class HomeBloc extends Bloc { on(_confirmUserAgreement); } - // void _createNode(CreateNewNode event, Emitter emit) async { - // emit(HomeInitial()); - // sourcesList.add(event.sourceNode); - // destinationsList.add(event.destinationNode); - // for (int i = 0; i < sourcesList.length; i++) { - // graph.addEdge(sourcesList[i], destinationsList[i]); - // } - - // builder - // ..siblingSeparation = (100) - // ..levelSeparation = (150) - // ..subtreeSeparation = (150) - // ..orientation = (BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM); - // emit(HomeUpdateTree(graph: graph, builder: builder)); - // } - Future _fetchUserInfo(FetchUserInfo event, Emitter emit) async { try { var uuid = await const FlutterSecureStorage().read(key: UserModel.userUuidKey); @@ -99,16 +78,6 @@ class HomeBloc extends Bloc { } } -// static Future fetchUserInfo() async { -// try { -// var uuid = -// await const FlutterSecureStorage().read(key: UserModel.userUuidKey); -// user = await HomeApi().fetchUserInfo(uuid); -// } catch (e) { -// return; -// } -// } - List homeItems = [ HomeItemModel( title: 'Access Management', @@ -118,7 +87,7 @@ class HomeBloc extends Bloc { context.read().add(ClearCachedData()); context.go(RoutesConst.accessManagementPage); }, - color: null, + color: const Color(0xFF0036E6), ), HomeItemModel( title: 'Space Management', @@ -128,7 +97,7 @@ class HomeBloc extends Bloc { context.read().add(ClearCachedData()); context.go(RoutesConst.spacesManagementPage); }, - color: ColorsManager.primaryColor, + color: const Color(0xFF0026A2), ), HomeItemModel( title: 'Devices Management', @@ -140,12 +109,11 @@ class HomeBloc extends Bloc { .add(const TriggerSwitchTabsEvent(isRoutineTab: false)); context.go(RoutesConst.deviceManagementPage); }, - color: ColorsManager.primaryColor, + color: const Color(0xFF00165E), ), - HomeItemModel( title: 'Syncrow Analytics', - icon: Assets.devicesIcon, + icon: Assets.analyticsIcon, active: true, onPress: (context) { context.read().add(ClearCachedData()); @@ -153,43 +121,7 @@ class HomeBloc extends Bloc { .add(const TriggerSwitchTabsEvent(isRoutineTab: false)); context.go(RoutesConst.analytics); }, - color: ColorsManager.primaryColor, + color: const Color(0xFF023DFE), ), - - // HomeItemModel( - // title: 'Move in', - // icon: Assets.moveinIcon, - // active: false, - // onPress: (context) {}, - // color: ColorsManager.primaryColor, - // ), - // HomeItemModel( - // title: 'Construction', - // icon: Assets.constructionIcon, - // active: false, - // onPress: (context) {}, - // color: ColorsManager.primaryColor, - // ), - // HomeItemModel( - // title: 'Energy', - // icon: Assets.energyIcon, - // active: false, - // onPress: (context) {}, - // color: ColorsManager.slidingBlueColor.withOpacity(0.2), - // ), - // HomeItemModel( - // title: 'Integrations', - // icon: Assets.integrationsIcon, - // active: false, - // onPress: (context) {}, - // color: ColorsManager.slidingBlueColor.withOpacity(0.2), - // ), - // HomeItemModel( - // title: 'Asset', - // icon: Assets.assetIcon, - // active: false, - // onPress: (context) {}, - // color: ColorsManager.slidingBlueColor.withOpacity(0.2), - // ), ]; } diff --git a/lib/pages/home/view/home_card.dart b/lib/pages/home/view/home_card.dart index d2e71608..ef3bd8de 100644 --- a/lib/pages/home/view/home_card.dart +++ b/lib/pages/home/view/home_card.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:syncrow_web/utils/color_manager.dart'; class HomeCard extends StatelessWidget { final bool active; @@ -8,6 +7,7 @@ class HomeCard extends StatelessWidget { final int index; final String name; final Function()? onTap; + final Color? color; const HomeCard({ super.key, required this.name, @@ -15,28 +15,16 @@ class HomeCard extends StatelessWidget { this.active = false, required this.img, required this.onTap, + required this.color, }); @override Widget build(BuildContext context) { - // bool evenNumbers = index % 2 == 0; return InkWell( onTap: active ? onTap : null, child: Container( padding: const EdgeInsets.only(left: 10, right: 10, bottom: 10), decoration: BoxDecoration( - color: index == 0 && active - ? ColorsManager.blue1.withOpacity(0.9) - : index == 1 && active - ? ColorsManager.blue2.withOpacity(0.9) - : index == 2 && active - ? ColorsManager.blue3 - : index == 4 && active == false - ? ColorsManager.blue4.withOpacity(0.2) - : index == 7 && active == false - ? ColorsManager.blue4.withOpacity(0.2) - : ColorsManager.blueColor.withOpacity(0.2), - // (active ?ColorsManager.blueColor - // : ColorsManager.blueColor.withOpacity(0.2)), + color: color, borderRadius: BorderRadius.circular(30), ), child: Column( @@ -64,15 +52,9 @@ class HomeCard extends StatelessWidget { ), const SizedBox(height: 10), Expanded( - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - SizedBox( - child: SvgPicture.asset( - img, - ), - ), - ], + child: Align( + alignment: AlignmentDirectional.bottomEnd, + child: SvgPicture.asset(img), ), ), ], diff --git a/lib/pages/home/view/home_page_mobile.dart b/lib/pages/home/view/home_page_mobile.dart index d0719c3e..ad019ea8 100644 --- a/lib/pages/home/view/home_page_mobile.dart +++ b/lib/pages/home/view/home_page_mobile.dart @@ -50,7 +50,7 @@ class HomeMobilePage extends StatelessWidget { height: size.height * 0.6, width: size.width * 0.68, child: GridView.builder( - itemCount: homeItems.length, + itemCount: homeBloc.homeItems.length, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, @@ -61,7 +61,8 @@ class HomeMobilePage extends StatelessWidget { itemBuilder: (context, index) { return HomeCard( index: index, - active: homeBloc.homeItems[index].active!, + active: true, + color: homeBloc.homeItems[index].color, name: homeBloc.homeItems[index].title!, img: homeBloc.homeItems[index].icon!, onTap: () => @@ -78,56 +79,4 @@ class HomeMobilePage extends StatelessWidget { ), ); } - - final dynamic homeItems = [ - { - 'title': 'Access', - 'icon': Assets.accessIcon, - 'active': true, - }, - { - 'title': 'Space\nManagement', - 'icon': Assets.spaseManagementIcon, - 'color': ColorsManager.primaryColor, - 'active': true, - }, - { - 'title': 'Devices', - 'icon': Assets.devicesIcon, - 'active': true, - }, - { - 'title': 'Syncrow Analytics', - 'icon': Assets.iconEdit, - 'active': true, - }, - // { - // 'title': 'Move in', - // 'icon': Assets.moveinIcon, - // 'active': false, - // }, - // { - // 'title': 'Construction', - // 'icon': Assets.constructionIcon, - // 'active': false, - // }, - // { - // 'title': 'Energy', - // 'icon': Assets.energyIcon, - // 'color': ColorsManager.slidingBlueColor.withOpacity(0.2), - // 'active': false, - // }, - // { - // 'title': 'Integrations', - // 'icon': Assets.integrationsIcon, - // 'color': ColorsManager.slidingBlueColor.withOpacity(0.2), - // 'active': false, - // }, - // { - // 'title': 'Asset', - // 'icon': Assets.assetIcon, - // 'color': ColorsManager.slidingBlueColor.withOpacity(0.2), - // 'active': false, - // }, - ]; } diff --git a/lib/pages/home/view/home_page_web.dart b/lib/pages/home/view/home_page_web.dart index 9a59f51c..334cec4d 100644 --- a/lib/pages/home/view/home_page_web.dart +++ b/lib/pages/home/view/home_page_web.dart @@ -106,8 +106,9 @@ class _HomeWebPageState extends State { ), itemBuilder: (context, index) { return HomeCard( + color: homeBloc.homeItems[index].color, index: index, - active: homeBloc.homeItems[index].active!, + active: true, name: homeBloc.homeItems[index].title!, img: homeBloc.homeItems[index].icon!, onTap: () => homeBloc.homeItems[index].onPress(context), From 660649145833f476adb1854c9ca18e309778e773 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 25 May 2025 10:59:41 +0300 Subject: [PATCH 015/181] made `active` dynamic --- lib/pages/home/view/home_page_web.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/home/view/home_page_web.dart b/lib/pages/home/view/home_page_web.dart index 334cec4d..fb35fa04 100644 --- a/lib/pages/home/view/home_page_web.dart +++ b/lib/pages/home/view/home_page_web.dart @@ -108,7 +108,7 @@ class _HomeWebPageState extends State { return HomeCard( color: homeBloc.homeItems[index].color, index: index, - active: true, + active: homeBloc.homeItems[index].active!, name: homeBloc.homeItems[index].title!, img: homeBloc.homeItems[index].icon!, onTap: () => homeBloc.homeItems[index].onPress(context), From a878b9328a587e96ea2622ddb2c0fec28e23e5cb Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 25 May 2025 11:06:36 +0300 Subject: [PATCH 016/181] SP-1493 rework, can select a subspace in sidebar even when the space has no child-spaces. --- .../strategies/air_quality_data_loading_strategy.dart | 2 +- .../strategies/energy_management_data_loading_strategy.dart | 4 +--- .../analytics/strategies/occupancy_data_loading_strategy.dart | 2 +- 3 files changed, 3 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 19f9775d..dc3b1c5e 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 @@ -48,7 +48,7 @@ final class AirQualityDataLoadingStrategy implements AnalyticsDataLoadingStrateg CommunityModel community, SpaceModel child, ) { - if (child.children.isNotEmpty) return onSpaceSelected(context, community, child); + return onSpaceSelected(context, community, child); } @override 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 c0d03879..e73b5179 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 @@ -68,9 +68,7 @@ class EnergyManagementDataLoadingStrategy implements AnalyticsDataLoadingStrateg CommunityModel community, SpaceModel child, ) { - if (child.children.isNotEmpty) { - return onSpaceSelected(context, community, child); - } + return onSpaceSelected(context, community, child); } @override 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 158f5128..5241564c 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 @@ -48,7 +48,7 @@ class OccupancyDataLoadingStrategy implements AnalyticsDataLoadingStrategy { CommunityModel community, SpaceModel child, ) { - if (child.children.isNotEmpty) return onSpaceSelected(context, community, child); + return onSpaceSelected(context, community, child); } @override From 9d27ed2dc57a605ff6fa4fa267ecc8af209f6f2f Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 25 May 2025 11:13:24 +0300 Subject: [PATCH 017/181] SP-1506 rework, coloring and padding. --- .../helpers/energy_management_charts_helper.dart | 4 ++-- .../widgets/energy_consumption_by_phases_chart.dart | 2 +- .../widgets/energy_consumption_by_phases_chart_box.dart | 6 ++++-- 3 files changed, 7 insertions(+), 5 deletions(-) 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 567e03ed..11c088e8 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: true, + minIncluded: false, interval: leftTitlesInterval, reservedSize: 110, getTitlesWidget: (value, meta) => Padding( @@ -50,7 +50,7 @@ abstract final class EnergyManagementChartsHelper { value.formatNumberToKwh, style: context.textTheme.bodySmall?.copyWith( fontSize: 12, - color: ColorsManager.greyColor, + color: ColorsManager.lightGreyColor, ), ), ), diff --git a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_by_phases_chart.dart b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_by_phases_chart.dart index 1497d0fd..001f4d2c 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_by_phases_chart.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_by_phases_chart.dart @@ -170,7 +170,7 @@ class EnergyConsumptionByPhasesChart extends StatelessWidget { child: Text( month, style: context.textTheme.bodySmall?.copyWith( - color: ColorsManager.greyColor, + color: ColorsManager.lightGreyColor, fontSize: 11, ), ), diff --git a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_by_phases_chart_box.dart b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_by_phases_chart_box.dart index 1766266c..1bd1ed9e 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_by_phases_chart_box.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_by_phases_chart_box.dart @@ -19,10 +19,12 @@ class EnergyConsumptionByPhasesChartBox extends StatelessWidget { decoration: secondarySection, child: Column( mainAxisSize: MainAxisSize.min, - spacing: 20, children: [ AnalyticsErrorWidget(state.errorMessage), - EnergyConsumptionByPhasesTitle(isLoading: state.status == EnergyConsumptionByPhasesStatus.loading,), + EnergyConsumptionByPhasesTitle( + isLoading: state.status == EnergyConsumptionByPhasesStatus.loading, + ), + const SizedBox(height: 20), Expanded( child: EnergyConsumptionByPhasesChart( energyData: state.chartData, From 12deceb7d319d17bb73213885d7363cb105df954 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 25 May 2025 11:35:01 +0300 Subject: [PATCH 018/181] SP-1513-rework --- .../power_clamp_phases_data_widget.dart | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/lib/pages/analytics/modules/energy_management/widgets/power_clamp_phases_data_widget.dart b/lib/pages/analytics/modules/energy_management/widgets/power_clamp_phases_data_widget.dart index 1cb20aac..dc0aa050 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/power_clamp_phases_data_widget.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/power_clamp_phases_data_widget.dart @@ -125,7 +125,48 @@ class PowerClampPhasesDataWidget extends StatelessWidget { (e) => e.code == code, orElse: () => DataPoint(value: '--'), ); + final value = element?.value; + if (code.contains('Current')) { + return _formatCurrentValue(value?.toString()); + } + if (code.contains('PowerFactor')) { + return _formatPowerFactor(value?.toString()); + } + if (code.contains('Voltage')) { + return _formatVoltage(value?.toString()); + } + return value?.toString() ?? '--'; + } - return element?.value.toString() ?? '--'; + String _formatCurrentValue(String? value) { + if (value == null) return '--'; + String str = value; + if (str.isEmpty || str == '--') return '--'; + str = str.replaceAll(RegExp(r'[^0-9]'), ''); + if (str.isEmpty) return '--'; + if (str.length == 1) return '${str[0]}.0'; + return '${str[0]}.${str.substring(1)}'; + } + + String _formatPowerFactor(String? value) { + if (value == null) return '--'; + String str = value; + if (str.isEmpty || str == '--') return '--'; + str = str.replaceAll(RegExp(r'[^0-9]'), ''); + if (str.isEmpty) return '--'; + final intValue = int.tryParse(str); + if (intValue == null) return '--'; + final doubleValue = intValue / 100; + return doubleValue.toStringAsFixed(2); + } + + String _formatVoltage(String? value) { + if (value == null) return '--'; + String str = value; + if (str.isEmpty || str == '--') return '--'; + str = str.replaceAll(RegExp(r'[^0-9]'), ''); + if (str.isEmpty) return '--'; + if (str.length == 1) return '0.${str[0]}'; + return '${str.substring(0, str.length - 1)}.${str.substring(str.length - 1)}'; } } From c8fe4e3baa6ba86934ae42bfdf568cabffb88822 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 25 May 2025 12:01:26 +0300 Subject: [PATCH 019/181] Created an initial version of `RangeOfAqiChart`. --- .../air_quality/views/air_quality_view.dart | 5 +- .../widgets/range_of_aqi_chart.dart | 105 ++++++++++++++++++ .../widgets/range_of_aqi_chart_box.dart | 58 ++++++++++ 3 files changed, 166 insertions(+), 2 deletions(-) create mode 100644 lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart create mode 100644 lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.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 38f62cd7..be3b9b04 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/range_of_aqi_chart_box.dart'; class AirQualityView extends StatelessWidget { const AirQualityView({super.key}); @@ -22,7 +23,7 @@ class AirQualityView extends StatelessWidget { height: height * 1.2, child: const AirQualityEndSideWidget(), ), - SizedBox(height: height * 0.5, child: const Placeholder()), + SizedBox(height: height * 0.5, child: const RangeOfAqiChartBox()), SizedBox(height: height * 0.5, child: const Placeholder()), ], ), @@ -44,7 +45,7 @@ class AirQualityView extends StatelessWidget { child: Column( spacing: 20, children: [ - Expanded(child: Placeholder()), + Expanded(child: RangeOfAqiChartBox()), Expanded(child: Placeholder()), ], ), 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 new file mode 100644 index 00000000..6e8974f7 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart @@ -0,0 +1,105 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class RangeOfAqiChart extends StatelessWidget { + final List minValues; + final List avgValues; + final List maxValues; + + const RangeOfAqiChart({ + super.key, + required this.minValues, + required this.avgValues, + required this.maxValues, + }); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + // Fixed gradient background + Positioned.fill( + child: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [ + Color(0xFF0CEC16), + Color(0xFFFAC96C), + Color(0xFFEC7400), + Color(0xFFD40000), + Color(0xFFD40094), + Color(0xFFBA01FD), + ], + ), + ), + ), + ), + LineChart( + LineChartData( + minY: 0, + maxY: 320, + gridData: EnergyManagementChartsHelper.gridData(), + titlesData: EnergyManagementChartsHelper.titlesData(context), + borderData: FlBorderData(show: false), + lineBarsData: [ + // Max line (top, purple) + LineChartBarData( + spots: List.generate( + maxValues.length, + (i) => FlSpot(i.toDouble(), maxValues[i]), + ), + isCurved: true, + color: const Color(0xFF962DFF), + barWidth: 3, + isStrokeCapRound: true, + dotData: _buildDotData(const Color(0xFF5F00BD)), + belowBarData: BarAreaData(show: false), + ), + // Avg line (middle, white) + LineChartBarData( + spots: List.generate( + avgValues.length, + (i) => FlSpot(i.toDouble(), avgValues[i]), + ), + isCurved: true, + color: Colors.white, + barWidth: 3, + dotData: const FlDotData(show: false), + belowBarData: BarAreaData(show: false), + ), + // Min line (bottom, blue) + LineChartBarData( + spots: List.generate( + minValues.length, + (i) => FlSpot(i.toDouble(), minValues[i]), + ), + isCurved: true, + color: const Color(0xFF93AAFD), + barWidth: 3, + isStrokeCapRound: true, + dotData: _buildDotData(const Color(0xFF023DFE)), + belowBarData: BarAreaData(show: false), + ), + ], + ), + ), + ], + ); + } + + FlDotData _buildDotData(Color color) { + return FlDotData( + show: true, + getDotPainter: (spot, percent, bar, index) => FlDotCirclePainter( + radius: 2, + color: ColorsManager.whiteColors, + strokeWidth: 2, + strokeColor: color, + ), + ); + } +} 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 new file mode 100644 index 00000000..a8b173d7 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart'; +import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart'; +import 'package:syncrow_web/utils/style.dart'; + +class RangeOfAqiChartBox extends StatelessWidget { + const RangeOfAqiChartBox({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsetsDirectional.all(20), + decoration: secondarySection, + child: const Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 20, + children: [ + ChartTitle(title: Text('Range of AQI')), + Expanded( + child: RangeOfAqiChart( + avgValues: [ + 120, + 60, + 110, + 100, + 90, + 70, + 80, + 90, + 100, + 110, + 120, + 150, + 160 + ], + minValues: [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130], + maxValues: [ + 130, + 140, + 150, + 160, + 170, + 180, + 190, + 200, + 210, + 220, + 230, + 240, + ], + ), + ), + ], + ), + ); + } +} From 39351a710d1a756f44234a530c0260e63ad19586 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 25 May 2025 12:18:09 +0300 Subject: [PATCH 020/181] Added aqi info colors to `ColorsManager`. --- lib/utils/color_manager.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/utils/color_manager.dart b/lib/utils/color_manager.dart index 5a892aa6..c2e4e60d 100644 --- a/lib/utils/color_manager.dart +++ b/lib/utils/color_manager.dart @@ -73,4 +73,10 @@ abstract class ColorsManager { static const Color vividBlue = Color(0xFF023DFE); static const Color semiTransparentRed = Color(0x99FF0000); static const Color grey700 = Color(0xFF2D3748); + static const Color goodGreen = Color(0xFF0CEC16); + static const Color moderateYellow = Color(0xFFFAC96C); + static const Color poorOrange = Color(0xFFEC7400); + static const Color unhealthyRed = Color(0xFFD40000); + static const Color severePink = Color(0xFFD40094); + static const Color hazardousPurple = Color(0xFFBA01FD); } From cd2eb46f4958c34efecceb150348b7df2804de0c Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 09:50:53 +0300 Subject: [PATCH 021/181] Implemented the overall design of `RangeOfAqiChart`, whats left is 100% matching it with the figma design. --- .../widgets/range_of_aqi_chart.dart | 132 ++++++++++-------- 1 file changed, 73 insertions(+), 59 deletions(-) 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 6e8974f7..934e4c6e 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 @@ -15,75 +15,45 @@ class RangeOfAqiChart extends StatelessWidget { required this.maxValues, }); + static const _gradientColors = [ + ColorsManager.goodGreen, + ColorsManager.moderateYellow, + ColorsManager.poorOrange, + ColorsManager.unhealthyRed, + ColorsManager.severePink, + ColorsManager.hazardousPurple, + ]; + @override Widget build(BuildContext context) { return Stack( children: [ - // Fixed gradient background - Positioned.fill( - child: Container( - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - colors: [ - Color(0xFF0CEC16), - Color(0xFFFAC96C), - Color(0xFFEC7400), - Color(0xFFD40000), - Color(0xFFD40094), - Color(0xFFBA01FD), - ], - ), - ), - ), - ), LineChart( LineChartData( minY: 0, - maxY: 320, + maxY: 301, gridData: EnergyManagementChartsHelper.gridData(), titlesData: EnergyManagementChartsHelper.titlesData(context), - borderData: FlBorderData(show: false), + borderData: EnergyManagementChartsHelper.borderData(), + betweenBarsData: [ + BetweenBarsData( + fromIndex: 0, + toIndex: 2, + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: _gradientColors + .map( + (e) => e.withValues(alpha: 0.8), + ) + .toList(), + ), + ), + ], lineBarsData: [ - // Max line (top, purple) - LineChartBarData( - spots: List.generate( - maxValues.length, - (i) => FlSpot(i.toDouble(), maxValues[i]), - ), - isCurved: true, - color: const Color(0xFF962DFF), - barWidth: 3, - isStrokeCapRound: true, - dotData: _buildDotData(const Color(0xFF5F00BD)), - belowBarData: BarAreaData(show: false), - ), - // Avg line (middle, white) - LineChartBarData( - spots: List.generate( - avgValues.length, - (i) => FlSpot(i.toDouble(), avgValues[i]), - ), - isCurved: true, - color: Colors.white, - barWidth: 3, - dotData: const FlDotData(show: false), - belowBarData: BarAreaData(show: false), - ), - // Min line (bottom, blue) - LineChartBarData( - spots: List.generate( - minValues.length, - (i) => FlSpot(i.toDouble(), minValues[i]), - ), - isCurved: true, - color: const Color(0xFF93AAFD), - barWidth: 3, - isStrokeCapRound: true, - dotData: _buildDotData(const Color(0xFF023DFE)), - belowBarData: BarAreaData(show: false), - ), + _buildMaxLine(), + _buildAverageLine(), + _buildMinLine(), ], ), ), @@ -91,6 +61,50 @@ class RangeOfAqiChart extends StatelessWidget { ); } + LineChartBarData _buildMinLine() { + return LineChartBarData( + spots: List.generate( + minValues.length, + (i) => FlSpot(i.toDouble(), minValues[i]), + ), + isCurved: true, + color: const Color(0xFF93AAFD), + barWidth: 3, + isStrokeCapRound: true, + dotData: _buildDotData(const Color(0xFF023DFE)), + belowBarData: BarAreaData(show: false), + ); + } + + LineChartBarData _buildAverageLine() { + return LineChartBarData( + spots: List.generate( + avgValues.length, + (i) => FlSpot(i.toDouble(), avgValues[i]), + ), + isCurved: true, + color: Colors.white, + barWidth: 3, + dotData: const FlDotData(show: false), + belowBarData: BarAreaData(show: false), + ); + } + + LineChartBarData _buildMaxLine() { + return LineChartBarData( + spots: List.generate( + maxValues.length, + (i) => FlSpot(i.toDouble(), maxValues[i]), + ), + isCurved: true, + color: const Color(0xFF962DFF), + barWidth: 3, + isStrokeCapRound: true, + dotData: _buildDotData(const Color(0xFF5F00BD)), + belowBarData: BarAreaData(show: false), + ); + } + FlDotData _buildDotData(Color color) { return FlDotData( show: true, From 82006e9aaf13ec44bc5d3917d968913f8030d9f3 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 10:12:52 +0300 Subject: [PATCH 022/181] Implemented the side titles of `RangeOfAqiChart`. --- .../widgets/range_of_aqi_chart.dart | 36 +++++++++++++++++-- .../energy_management_charts_helper.dart | 6 ++-- 2 files changed, 38 insertions(+), 4 deletions(-) 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 934e4c6e..c9c56bdd 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart @@ -2,6 +2,7 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.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 RangeOfAqiChart extends StatelessWidget { final List minValues; @@ -32,8 +33,39 @@ class RangeOfAqiChart extends StatelessWidget { LineChartData( minY: 0, maxY: 301, - gridData: EnergyManagementChartsHelper.gridData(), - titlesData: EnergyManagementChartsHelper.titlesData(context), + gridData: EnergyManagementChartsHelper.gridData( + horizontalInterval: 50, + ), + titlesData: EnergyManagementChartsHelper.titlesData(context).copyWith( + leftTitles: AxisTitles( + sideTitles: SideTitles( + reservedSize: 70, + interval: 51, + showTitles: true, + maxIncluded: true, + getTitlesWidget: (value, meta) { + String text; + if (value >= 300) { + text = '300+'; + } else if (value == 255) { + text = '300'; + } else { + text = ((value / 50).round() * 50).toInt().toString(); + } + return Padding( + padding: const EdgeInsetsDirectional.only(end: 12), + child: Text( + text, + style: context.textTheme.bodySmall?.copyWith( + fontSize: 12, + color: ColorsManager.lightGreyColor, + ), + ), + ); + }, + ), + ), + ), borderData: EnergyManagementChartsHelper.borderData(), betweenBarsData: [ BetweenBarsData( 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 567e03ed..5938c77d 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 @@ -93,12 +93,14 @@ abstract final class EnergyManagementChartsHelper { ); } - static FlGridData gridData() { + static FlGridData gridData({ + double horizontalInterval = 250, + }) { return FlGridData( show: true, drawVerticalLine: false, drawHorizontalLine: true, - horizontalInterval: 250, + horizontalInterval: horizontalInterval, getDrawingHorizontalLine: (value) { return FlLine( color: ColorsManager.greyColor, From 24e3eb2311d827aa68f1d70bacc5f4d72f86eaea Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 10:18:15 +0300 Subject: [PATCH 023/181] extracted titlesData into a private factory method to enahnce readability. --- .../widgets/range_of_aqi_chart.dart | 68 +++++++++++-------- 1 file changed, 38 insertions(+), 30 deletions(-) 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 c9c56bdd..a271d154 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 @@ -36,36 +36,7 @@ class RangeOfAqiChart extends StatelessWidget { gridData: EnergyManagementChartsHelper.gridData( horizontalInterval: 50, ), - titlesData: EnergyManagementChartsHelper.titlesData(context).copyWith( - leftTitles: AxisTitles( - sideTitles: SideTitles( - reservedSize: 70, - interval: 51, - showTitles: true, - maxIncluded: true, - getTitlesWidget: (value, meta) { - String text; - if (value >= 300) { - text = '300+'; - } else if (value == 255) { - text = '300'; - } else { - text = ((value / 50).round() * 50).toInt().toString(); - } - return Padding( - padding: const EdgeInsetsDirectional.only(end: 12), - child: Text( - text, - style: context.textTheme.bodySmall?.copyWith( - fontSize: 12, - color: ColorsManager.lightGreyColor, - ), - ), - ); - }, - ), - ), - ), + titlesData: _titlesData(context), borderData: EnergyManagementChartsHelper.borderData(), betweenBarsData: [ BetweenBarsData( @@ -148,4 +119,41 @@ class RangeOfAqiChart extends StatelessWidget { ), ); } + + FlTitlesData _titlesData(BuildContext context) { + final titlesData = EnergyManagementChartsHelper.titlesData(context); + return titlesData.copyWith( + leftTitles: titlesData.leftTitles.copyWith( + sideTitles: titlesData.leftTitles.sideTitles.copyWith( + reservedSize: 70, + interval: 51, + maxIncluded: true, + getTitlesWidget: (value, meta) { + String text; + if (value >= 300) { + text = '300+'; + } else if (value == 255) { + text = '300'; + } else { + text = ((value / 50).round() * 50).toInt().toString(); + } + return Padding( + padding: const EdgeInsetsDirectional.only(end: 12), + child: FittedBox( + alignment: AlignmentDirectional.centerStart, + fit: BoxFit.scaleDown, + child: Text( + text, + style: context.textTheme.bodySmall?.copyWith( + fontSize: 12, + color: ColorsManager.lightGreyColor, + ), + ), + ), + ); + }, + ), + ), + ); + } } From 791b71276a6887488496d68c8d4e211e9bc3f2ba Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 10:29:38 +0300 Subject: [PATCH 024/181] populated linear data for `RangeOfAqiChart`, for a more pleasant dev experience and debugging. --- .../widgets/range_of_aqi_chart_box.dart | 61 ++++++++++++------- 1 file changed, 40 insertions(+), 21 deletions(-) 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 a8b173d7..685f1665 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 @@ -9,46 +9,65 @@ class RangeOfAqiChartBox extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - padding: const EdgeInsetsDirectional.all(20), - decoration: secondarySection, + padding: const EdgeInsetsDirectional.all(30), + decoration: subSectionContainerDecoration.copyWith( + borderRadius: BorderRadius.circular(30), + ), child: const Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, - spacing: 20, children: [ ChartTitle(title: Text('Range of AQI')), + SizedBox(height: 10), + Divider(), + SizedBox(height: 20), Expanded( child: RangeOfAqiChart( avgValues: [ - 120, - 60, - 110, - 100, - 90, + 50, 70, - 80, 90, - 100, 110, - 120, - 150, - 160 - ], - minValues: [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130], - maxValues: [ 130, - 140, 150, - 160, 170, - 180, 190, - 200, 210, - 220, 230, + 250, + 270, + 290, + ], + minValues: [ + 0, + 20, + 40, + 60, + 80, + 100, + 120, + 140, + 160, + 180, + 200, + 220, 240, ], + maxValues: [ + 100, + 120, + 140, + 160, + 180, + 200, + 220, + 240, + 260, + 280, + 300, + 301, + 301, + ], ), ), ], From 563a3e1cf5a41f6d49b6646609c706a2878ff20c Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 10:31:21 +0300 Subject: [PATCH 025/181] Refactored `RangeOfAqiChart` to consolidate line chart creation into a reusable method, improving code maintainability and reducing duplication. --- .../widgets/range_of_aqi_chart.dart | 58 +++++++++---------- 1 file changed, 29 insertions(+), 29 deletions(-) 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 a271d154..e0a8107f 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 @@ -65,46 +65,26 @@ class RangeOfAqiChart extends StatelessWidget { } LineChartBarData _buildMinLine() { - return LineChartBarData( - spots: List.generate( - minValues.length, - (i) => FlSpot(i.toDouble(), minValues[i]), - ), - isCurved: true, + return _buildLine( + values: minValues, color: const Color(0xFF93AAFD), - barWidth: 3, - isStrokeCapRound: true, - dotData: _buildDotData(const Color(0xFF023DFE)), - belowBarData: BarAreaData(show: false), + dotColor: const Color(0xFF023DFE), ); } LineChartBarData _buildAverageLine() { - return LineChartBarData( - spots: List.generate( - avgValues.length, - (i) => FlSpot(i.toDouble(), avgValues[i]), - ), - isCurved: true, + return _buildLine( + values: avgValues, color: Colors.white, - barWidth: 3, - dotData: const FlDotData(show: false), - belowBarData: BarAreaData(show: false), + dotColor: null, ); } LineChartBarData _buildMaxLine() { - return LineChartBarData( - spots: List.generate( - maxValues.length, - (i) => FlSpot(i.toDouble(), maxValues[i]), - ), - isCurved: true, + return _buildLine( + values: maxValues, color: const Color(0xFF962DFF), - barWidth: 3, - isStrokeCapRound: true, - dotData: _buildDotData(const Color(0xFF5F00BD)), - belowBarData: BarAreaData(show: false), + dotColor: const Color(0xFF5F00BD), ); } @@ -120,6 +100,26 @@ class RangeOfAqiChart extends StatelessWidget { ); } + LineChartBarData _buildLine({ + required List values, + required Color color, + Color? dotColor, + }) { + return LineChartBarData( + spots: List.generate( + values.length, + (i) => FlSpot(i.toDouble(), values[i]), + ), + isCurved: true, + color: color, + barWidth: 4, + isStrokeCapRound: true, + dotData: + dotColor != null ? _buildDotData(dotColor) : const FlDotData(show: false), + belowBarData: BarAreaData(show: false), + ); + } + FlTitlesData _titlesData(BuildContext context) { final titlesData = EnergyManagementChartsHelper.titlesData(context); return titlesData.copyWith( From 33f9add78a7be32d0b6502db6d5268f67076ca17 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 10:34:57 +0300 Subject: [PATCH 026/181] Extracted some logic of `RangeOfAqiChart` into a helper class. --- .../helpers/range_of_aqi_charts_helper.dart | 55 +++++++++ .../widgets/range_of_aqi_chart.dart | 105 ++++-------------- 2 files changed, 77 insertions(+), 83 deletions(-) create mode 100644 lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart diff --git a/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart b/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart new file mode 100644 index 00000000..d00c7357 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart @@ -0,0 +1,55 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.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'; + +abstract final class RangeOfAqiChartsHelper { + const RangeOfAqiChartsHelper._(); + + static const gradientData = <(Color color, String label)>[ + (ColorsManager.goodGreen, 'Good'), + (ColorsManager.moderateYellow, 'Moderate'), + (ColorsManager.poorOrange, 'Poor'), + (ColorsManager.unhealthyRed, 'Unhealthy'), + (ColorsManager.severePink, 'Severe'), + (ColorsManager.hazardousPurple, 'Hazardous'), + ]; + + static FlTitlesData titlesData(BuildContext context) { + final titlesData = EnergyManagementChartsHelper.titlesData(context); + return titlesData.copyWith( + leftTitles: titlesData.leftTitles.copyWith( + sideTitles: titlesData.leftTitles.sideTitles.copyWith( + reservedSize: 70, + interval: 51, + maxIncluded: true, + getTitlesWidget: (value, meta) { + String text; + if (value >= 300) { + text = '300+'; + } else if (value == 255) { + text = '300'; + } else { + text = ((value / 50).round() * 50).toInt().toString(); + } + return Padding( + padding: const EdgeInsetsDirectional.only(end: 12), + child: FittedBox( + alignment: AlignmentDirectional.centerStart, + fit: BoxFit.scaleDown, + child: Text( + text, + style: context.textTheme.bodySmall?.copyWith( + fontSize: 12, + color: ColorsManager.lightGreyColor, + ), + ), + ), + ); + }, + ), + ), + ); + } +} 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 e0a8107f..b179ec4a 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 @@ -1,8 +1,8 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart'; import 'package:syncrow_web/pages/analytics/modules/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 RangeOfAqiChart extends StatelessWidget { final List minValues; @@ -16,15 +16,6 @@ class RangeOfAqiChart extends StatelessWidget { required this.maxValues, }); - static const _gradientColors = [ - ColorsManager.goodGreen, - ColorsManager.moderateYellow, - ColorsManager.poorOrange, - ColorsManager.unhealthyRed, - ColorsManager.severePink, - ColorsManager.hazardousPurple, - ]; - @override Widget build(BuildContext context) { return Stack( @@ -33,10 +24,8 @@ class RangeOfAqiChart extends StatelessWidget { LineChartData( minY: 0, maxY: 301, - gridData: EnergyManagementChartsHelper.gridData( - horizontalInterval: 50, - ), - titlesData: _titlesData(context), + gridData: EnergyManagementChartsHelper.gridData(horizontalInterval: 50), + titlesData: RangeOfAqiChartsHelper.titlesData(context), borderData: EnergyManagementChartsHelper.borderData(), betweenBarsData: [ BetweenBarsData( @@ -45,18 +34,29 @@ class RangeOfAqiChart extends StatelessWidget { gradient: LinearGradient( begin: Alignment.bottomCenter, end: Alignment.topCenter, - colors: _gradientColors - .map( - (e) => e.withValues(alpha: 0.8), - ) - .toList(), + colors: RangeOfAqiChartsHelper.gradientData.map((e) { + final (color, _) = e; + return color.withValues(alpha: 0.8); + }).toList(), ), ), ], lineBarsData: [ - _buildMaxLine(), - _buildAverageLine(), - _buildMinLine(), + _buildLine( + values: maxValues, + color: const Color(0xFF962DFF), + dotColor: const Color(0xFF5F00BD), + ), + _buildLine( + values: avgValues, + color: Colors.white, + dotColor: null, + ), + _buildLine( + values: minValues, + color: const Color(0xFF93AAFD), + dotColor: const Color(0xFF023DFE), + ), ], ), ), @@ -64,30 +64,6 @@ class RangeOfAqiChart extends StatelessWidget { ); } - LineChartBarData _buildMinLine() { - return _buildLine( - values: minValues, - color: const Color(0xFF93AAFD), - dotColor: const Color(0xFF023DFE), - ); - } - - LineChartBarData _buildAverageLine() { - return _buildLine( - values: avgValues, - color: Colors.white, - dotColor: null, - ); - } - - LineChartBarData _buildMaxLine() { - return _buildLine( - values: maxValues, - color: const Color(0xFF962DFF), - dotColor: const Color(0xFF5F00BD), - ); - } - FlDotData _buildDotData(Color color) { return FlDotData( show: true, @@ -119,41 +95,4 @@ class RangeOfAqiChart extends StatelessWidget { belowBarData: BarAreaData(show: false), ); } - - FlTitlesData _titlesData(BuildContext context) { - final titlesData = EnergyManagementChartsHelper.titlesData(context); - return titlesData.copyWith( - leftTitles: titlesData.leftTitles.copyWith( - sideTitles: titlesData.leftTitles.sideTitles.copyWith( - reservedSize: 70, - interval: 51, - maxIncluded: true, - getTitlesWidget: (value, meta) { - String text; - if (value >= 300) { - text = '300+'; - } else if (value == 255) { - text = '300'; - } else { - text = ((value / 50).round() * 50).toInt().toString(); - } - return Padding( - padding: const EdgeInsetsDirectional.only(end: 12), - child: FittedBox( - alignment: AlignmentDirectional.centerStart, - fit: BoxFit.scaleDown, - child: Text( - text, - style: context.textTheme.bodySmall?.copyWith( - fontSize: 12, - color: ColorsManager.lightGreyColor, - ), - ), - ), - ); - }, - ), - ), - ); - } } From 926bcd9a5d5a69ddeeb639baff26a1b8ab9f48cb Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 10:47:25 +0300 Subject: [PATCH 027/181] Extracted lines data into a helper method for ease of readability. --- .../widgets/range_of_aqi_chart.dart | 38 +++++++------------ 1 file changed, 14 insertions(+), 24 deletions(-) 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 b179ec4a..d53fee0e 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 @@ -16,6 +16,12 @@ class RangeOfAqiChart extends StatelessWidget { required this.maxValues, }); + List<(List values, Color color, Color? dotColor)> _lines() => [ + (maxValues, const Color(0xFF962DFF), const Color(0xFF5F00BD)), + (avgValues, Colors.white, null), + (minValues, const Color(0xFF93AAFD), const Color(0xFF023DFE)), + ]; + @override Widget build(BuildContext context) { return Stack( @@ -41,23 +47,10 @@ class RangeOfAqiChart extends StatelessWidget { ), ), ], - lineBarsData: [ - _buildLine( - values: maxValues, - color: const Color(0xFF962DFF), - dotColor: const Color(0xFF5F00BD), - ), - _buildLine( - values: avgValues, - color: Colors.white, - dotColor: null, - ), - _buildLine( - values: minValues, - color: const Color(0xFF93AAFD), - dotColor: const Color(0xFF023DFE), - ), - ], + lineBarsData: _lines().map((e) { + final (values, color, dotColor) = e; + return _buildLine(values: values, color: color, dotColor: dotColor); + }).toList(), ), ), ], @@ -67,7 +60,7 @@ class RangeOfAqiChart extends StatelessWidget { FlDotData _buildDotData(Color color) { return FlDotData( show: true, - getDotPainter: (spot, percent, bar, index) => FlDotCirclePainter( + getDotPainter: (_, __, ___, ____) => FlDotCirclePainter( radius: 2, color: ColorsManager.whiteColors, strokeWidth: 2, @@ -81,17 +74,14 @@ class RangeOfAqiChart extends StatelessWidget { required Color color, Color? dotColor, }) { + const invisibleDot = FlDotData(show: false); return LineChartBarData( - spots: List.generate( - values.length, - (i) => FlSpot(i.toDouble(), values[i]), - ), + spots: List.generate(values.length, (i) => FlSpot(i.toDouble(), values[i])), isCurved: true, color: color, barWidth: 4, isStrokeCapRound: true, - dotData: - dotColor != null ? _buildDotData(dotColor) : const FlDotData(show: false), + dotData: dotColor != null ? _buildDotData(dotColor) : invisibleDot, belowBarData: BarAreaData(show: false), ); } From 902419f9c4e0d62f372a7b7e91995acca93acf2b Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 10:58:05 +0300 Subject: [PATCH 028/181] Created `RangeOfAqi` model. --- lib/pages/analytics/models/range_of_aqi.dart | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 lib/pages/analytics/models/range_of_aqi.dart diff --git a/lib/pages/analytics/models/range_of_aqi.dart b/lib/pages/analytics/models/range_of_aqi.dart new file mode 100644 index 00000000..759666c2 --- /dev/null +++ b/lib/pages/analytics/models/range_of_aqi.dart @@ -0,0 +1,18 @@ +import 'package:equatable/equatable.dart'; + +class RangeOfAqi extends Equatable { + final double min; + final double avg; + final double max; + final DateTime date; + + const RangeOfAqi({ + required this.min, + required this.avg, + required this.max, + required this.date, + }); + + @override + List get props => [min, avg, max, date]; +} From eb8ba1806ca216a57bbe108d629e6c2b03aff31f Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 10:59:07 +0300 Subject: [PATCH 029/181] Created `GetRangeOfAqiParam` model. --- .../analytics/params/get_range_of_aqi_param.dart | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 lib/pages/analytics/params/get_range_of_aqi_param.dart diff --git a/lib/pages/analytics/params/get_range_of_aqi_param.dart b/lib/pages/analytics/params/get_range_of_aqi_param.dart new file mode 100644 index 00000000..27a8bb35 --- /dev/null +++ b/lib/pages/analytics/params/get_range_of_aqi_param.dart @@ -0,0 +1,12 @@ +import 'package:equatable/equatable.dart'; + +class GetRangeOfAqiParam extends Equatable { + final DateTime date; + + const GetRangeOfAqiParam({ + required this.date, + }); + + @override + List get props => [date]; +} \ No newline at end of file From 4a3085e1b45a689bce36b82c2f46c43d16b3e700 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 11:00:57 +0300 Subject: [PATCH 030/181] Created `RangeOfAqiService` along with its fake implementation until the API is ready. --- .../range_of_aqi/fake_range_of_aqi_service.dart | 16 ++++++++++++++++ .../range_of_aqi/range_of_aqi_service.dart | 6 ++++++ 2 files changed, 22 insertions(+) create mode 100644 lib/pages/analytics/services/range_of_aqi/fake_range_of_aqi_service.dart create mode 100644 lib/pages/analytics/services/range_of_aqi/range_of_aqi_service.dart 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 new file mode 100644 index 00000000..7e50e6aa --- /dev/null +++ b/lib/pages/analytics/services/range_of_aqi/fake_range_of_aqi_service.dart @@ -0,0 +1,16 @@ +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'; + +class FakeRangeOfAqiService implements RangeOfAqiService { + @override + Future> load(GetRangeOfAqiParam param) async { + return List.generate(30, (index) { + final date = param.date.add(Duration(days: index)); + final min = (index * 2).toDouble(); + final avg = min + 10; + final max = avg + 10; + return RangeOfAqi(min: min, avg: avg, max: max, date: date); + }); + } +} diff --git a/lib/pages/analytics/services/range_of_aqi/range_of_aqi_service.dart b/lib/pages/analytics/services/range_of_aqi/range_of_aqi_service.dart new file mode 100644 index 00000000..9e1657e3 --- /dev/null +++ b/lib/pages/analytics/services/range_of_aqi/range_of_aqi_service.dart @@ -0,0 +1,6 @@ +import 'package:syncrow_web/pages/analytics/models/range_of_aqi.dart'; +import 'package:syncrow_web/pages/analytics/params/get_range_of_aqi_param.dart'; + +abstract interface class RangeOfAqiService { + Future> load(GetRangeOfAqiParam param); +} \ No newline at end of file From 5c57143ea559d868ab164e34a9f93bec05afeae7 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 11:09:45 +0300 Subject: [PATCH 031/181] Created `RangeOfAqiBloc` along with its events, and state. --- .../blocs/range_of_aqi/range_of_aqi_bloc.dart | 37 +++++++++++++++++++ .../range_of_aqi/range_of_aqi_event.dart | 21 +++++++++++ .../range_of_aqi/range_of_aqi_state.dart | 18 +++++++++ 3 files changed, 76 insertions(+) create mode 100644 lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart create mode 100644 lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_event.dart create mode 100644 lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_state.dart 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 new file mode 100644 index 00000000..e19f4345 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart @@ -0,0 +1,37 @@ +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/params/get_range_of_aqi_param.dart'; +import 'package:syncrow_web/pages/analytics/services/range_of_aqi/range_of_aqi_service.dart'; + +part 'range_of_aqi_event.dart'; +part 'range_of_aqi_state.dart'; + +class RangeOfAqiBloc extends Bloc { + RangeOfAqiBloc(this._rangeOfAqiService) : super(const RangeOfAqiState()) { + on(_onLoadRangeOfAqiEvent); + on(_onClearRangeOfAqiEvent); + } + + final RangeOfAqiService _rangeOfAqiService; + + Future _onLoadRangeOfAqiEvent( + LoadRangeOfAqiEvent event, + Emitter emit, + ) async { + emit(const RangeOfAqiState(status: RangeOfAqiStatus.loading)); + try { + final rangeOfAqi = await _rangeOfAqiService.load(event.param); + emit(RangeOfAqiState(status: RangeOfAqiStatus.loaded, rangeOfAqi: rangeOfAqi)); + } catch (e) { + emit(RangeOfAqiState(status: RangeOfAqiStatus.failure, errorMessage: '$e')); + } + } + + void _onClearRangeOfAqiEvent( + ClearRangeOfAqiEvent event, + Emitter emit, + ) { + emit(const RangeOfAqiState()); + } +} 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 new file mode 100644 index 00000000..8a429587 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_event.dart @@ -0,0 +1,21 @@ +part of 'range_of_aqi_bloc.dart'; + +sealed class RangeOfAqiEvent extends Equatable { + const RangeOfAqiEvent(); + + @override + List get props => []; +} + +class LoadRangeOfAqiEvent extends RangeOfAqiEvent { + const LoadRangeOfAqiEvent(this.param); + + final GetRangeOfAqiParam param; + + @override + List get props => [param]; +} + +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 new file mode 100644 index 00000000..392e98c1 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_state.dart @@ -0,0 +1,18 @@ +part of 'range_of_aqi_bloc.dart'; + +enum RangeOfAqiStatus { initial, loading, loaded, failure } + +final class RangeOfAqiState extends Equatable { + const RangeOfAqiState({ + this.rangeOfAqi = const [], + this.status = RangeOfAqiStatus.initial, + this.errorMessage, + }); + + final RangeOfAqiStatus status; + final List rangeOfAqi; + final String? errorMessage; + + @override + List get props => [status, rangeOfAqi, errorMessage]; +} From 9ab906d24c5f53279c383c501c3f4676279e7329 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 11:10:23 +0300 Subject: [PATCH 032/181] Injected `RangeOfAqiBloc` into `AnalyticsPage`. --- .../analytics/modules/analytics/views/analytics_page.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/pages/analytics/modules/analytics/views/analytics_page.dart b/lib/pages/analytics/modules/analytics/views/analytics_page.dart index 18f86a90..68a531c8 100644 --- a/lib/pages/analytics/modules/analytics/views/analytics_page.dart +++ b/lib/pages/analytics/modules/analytics/views/analytics_page.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/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'; import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_tab/analytics_tab_bloc.dart'; @@ -20,6 +21,7 @@ import 'package:syncrow_web/pages/analytics/services/energy_consumption_per_devi 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'; +import 'package:syncrow_web/pages/analytics/services/range_of_aqi/fake_range_of_aqi_service.dart'; import 'package:syncrow_web/pages/analytics/services/realtime_device_service/firebase_realtime_device_service.dart'; import 'package:syncrow_web/pages/analytics/services/total_energy_consumption/remote_total_energy_consumption_service.dart'; import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart'; @@ -94,6 +96,11 @@ class _AnalyticsPageState extends State { ), ), ), + BlocProvider( + create: (context) => RangeOfAqiBloc( + FakeRangeOfAqiService(), + ), + ), ], child: const AnalyticsPageForm(), ); From d4dd7a19ba6caccefabd66bed22086ae1e44f0e2 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 11:21:42 +0300 Subject: [PATCH 033/181] make the generated fake aqi range data, look better on the chart. --- .../fake_range_of_aqi_service.dart | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) 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 7e50e6aa..63f71a76 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 @@ -7,10 +7,20 @@ class FakeRangeOfAqiService implements RangeOfAqiService { Future> load(GetRangeOfAqiParam param) async { return List.generate(30, (index) { final date = param.date.add(Duration(days: index)); - final min = (index * 2).toDouble(); - final avg = min + 10; - final max = avg + 10; - return RangeOfAqi(min: min, avg: avg, max: max, date: date); + final min = (index * 8).toDouble(); + final avg = min + 40; + final max = avg + 40; + + final cappedMin = min > 301 ? 301.0 : min; + final cappedAvg = avg > 301 ? 301.0 : avg; + final cappedMax = max > 301 ? 301.0 : max; + + return RangeOfAqi( + min: cappedMin, + avg: cappedAvg, + max: cappedMax, + date: date, + ); }); } } From 4af81bcc1096cf579741f6cddd3bcb2e5b83bffb Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 11:22:05 +0300 Subject: [PATCH 034/181] make the aqi range chart read its data from `RangeOfAqiBloc`. --- .../widgets/range_of_aqi_chart.dart | 27 ++++-- .../widgets/range_of_aqi_chart_box.dart | 91 ++++++------------- 2 files changed, 46 insertions(+), 72 deletions(-) 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 d53fee0e..5796c1d8 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 @@ -1,25 +1,34 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/analytics/models/range_of_aqi.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart'; import 'package:syncrow_web/utils/color_manager.dart'; class RangeOfAqiChart extends StatelessWidget { - final List minValues; - final List avgValues; - final List maxValues; + final List chartData; const RangeOfAqiChart({ super.key, - required this.minValues, - required this.avgValues, - required this.maxValues, + required this.chartData, }); List<(List values, Color color, Color? dotColor)> _lines() => [ - (maxValues, const Color(0xFF962DFF), const Color(0xFF5F00BD)), - (avgValues, Colors.white, null), - (minValues, const Color(0xFF93AAFD), const Color(0xFF023DFE)), + ( + chartData.map((e) => e.max).toList(), + const Color(0xFF962DFF), + const Color(0xFF5F00BD) + ), + ( + chartData.map((e) => e.avg).toList(), + Colors.white, + null, + ), + ( + chartData.map((e) => e.min).toList(), + const Color(0xFF93AAFD), + const Color(0xFF023DFE) + ), ]; @override 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 685f1665..86696392 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 @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; +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/energy_management/widgets/chart_title.dart'; +import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart'; import 'package:syncrow_web/utils/style.dart'; class RangeOfAqiChartBox extends StatelessWidget { @@ -8,70 +11,32 @@ class RangeOfAqiChartBox extends StatelessWidget { @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: [ - ChartTitle(title: Text('Range of AQI')), - SizedBox(height: 10), - Divider(), - SizedBox(height: 20), - Expanded( - child: RangeOfAqiChart( - avgValues: [ - 50, - 70, - 90, - 110, - 130, - 150, - 170, - 190, - 210, - 230, - 250, - 270, - 290, - ], - minValues: [ - 0, - 20, - 40, - 60, - 80, - 100, - 120, - 140, - 160, - 180, - 200, - 220, - 240, - ], - maxValues: [ - 100, - 120, - 140, - 160, - 180, - 200, - 220, - 240, - 260, - 280, - 300, - 301, - 301, - ], - ), + 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: [ + AnalyticsErrorWidget(state.errorMessage), + const SizedBox(height: 10), + const ChartTitle(title: Text('Range of AQI')), + const SizedBox(height: 10), + const Divider(), + const SizedBox(height: 20), + Expanded( + child: RangeOfAqiChart( + chartData: state.rangeOfAqi, + ), + ), + ], + ), + ); + }, ); } } From 61acaa17c59322c4a4a55dcf8760048dc59b85cb Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 11:22:11 +0300 Subject: [PATCH 035/181] fixed typo. --- .../modules/air_quality/helpers/range_of_aqi_charts_helper.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart b/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart index d00c7357..9c7ef2f2 100644 --- a/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart +++ b/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart @@ -27,7 +27,7 @@ abstract final class RangeOfAqiChartsHelper { getTitlesWidget: (value, meta) { String text; if (value >= 300) { - text = '300+'; + text = '301+'; } else if (value == 255) { text = '300'; } else { From 7305d511bc8c90e7c958c932bb71d37c6e6391b4 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 11:23:33 +0300 Subject: [PATCH 036/181] Added `spaceUuid` to `GetRangeOfAqiParam` model. --- lib/pages/analytics/params/get_range_of_aqi_param.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 27a8bb35..f55337eb 100644 --- a/lib/pages/analytics/params/get_range_of_aqi_param.dart +++ b/lib/pages/analytics/params/get_range_of_aqi_param.dart @@ -2,11 +2,13 @@ import 'package:equatable/equatable.dart'; class GetRangeOfAqiParam extends Equatable { final DateTime date; + final String spaceUuid; const GetRangeOfAqiParam({ required this.date, + required this.spaceUuid, }); @override - List get props => [date]; + List get props => [date, spaceUuid]; } \ No newline at end of file From 82adbcf4df12622389fc206c6bcb1d08da3f36f1 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 11:24:00 +0300 Subject: [PATCH 037/181] loads and clears aqi range data in `FetchAirQualityDataHelper`. --- .../fetch_air_quality_data_helper.dart | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart b/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart index dd646063..a68e70da 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,8 +1,11 @@ import 'package:flutter/material.dart'; 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/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_analytics_devices_param.dart'; +import 'package:syncrow_web/pages/analytics/params/get_range_of_aqi_param.dart'; abstract final class FetchAirQualityDataHelper { const FetchAirQualityDataHelper._(); @@ -12,11 +15,13 @@ abstract final class FetchAirQualityDataHelper { required String communityUuid, required String spaceUuid, }) { + final date = context.read().state.monthlyDate; loadAnalyticsDevices( context, communityUuid: communityUuid, spaceUuid: spaceUuid, ); + loadRangeOfAqi(context, spaceUuid: spaceUuid, date: date); } static void clearAllData(BuildContext context) { @@ -26,6 +31,8 @@ abstract final class FetchAirQualityDataHelper { context.read().add( const RealtimeDeviceChangesClosed(), ); + + context.read().add(const ClearRangeOfAqiEvent()); } static void loadAnalyticsDevices( @@ -49,4 +56,16 @@ abstract final class FetchAirQualityDataHelper { ), ); } + + static void loadRangeOfAqi( + BuildContext context, { + required String spaceUuid, + required DateTime date, + }) { + context.read().add( + LoadRangeOfAqiEvent( + GetRangeOfAqiParam(date: date, spaceUuid: spaceUuid), + ), + ); + } } From 12e4285b14b0a01ea77199b1907fa56f4fd8d8ee Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 11:24:53 +0300 Subject: [PATCH 038/181] removed unnecessary `Stack` widget from `RangeOfAqiChart`. --- .../widgets/range_of_aqi_chart.dart | 54 +++++++++---------- 1 file changed, 25 insertions(+), 29 deletions(-) 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 5796c1d8..0f941876 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 @@ -33,36 +33,32 @@ class RangeOfAqiChart extends StatelessWidget { @override Widget build(BuildContext context) { - return Stack( - children: [ - LineChart( - LineChartData( - minY: 0, - maxY: 301, - gridData: EnergyManagementChartsHelper.gridData(horizontalInterval: 50), - titlesData: RangeOfAqiChartsHelper.titlesData(context), - borderData: EnergyManagementChartsHelper.borderData(), - betweenBarsData: [ - BetweenBarsData( - fromIndex: 0, - toIndex: 2, - gradient: LinearGradient( - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - colors: RangeOfAqiChartsHelper.gradientData.map((e) { - final (color, _) = e; - return color.withValues(alpha: 0.8); - }).toList(), - ), - ), - ], - lineBarsData: _lines().map((e) { - final (values, color, dotColor) = e; - return _buildLine(values: values, color: color, dotColor: dotColor); - }).toList(), + return LineChart( + LineChartData( + minY: 0, + maxY: 301, + gridData: EnergyManagementChartsHelper.gridData(horizontalInterval: 50), + titlesData: RangeOfAqiChartsHelper.titlesData(context), + borderData: EnergyManagementChartsHelper.borderData(), + betweenBarsData: [ + BetweenBarsData( + fromIndex: 0, + toIndex: 2, + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: RangeOfAqiChartsHelper.gradientData.map((e) { + final (color, _) = e; + return color.withValues(alpha: 0.8); + }).toList(), + ), ), - ), - ], + ], + lineBarsData: _lines().map((e) { + final (values, color, dotColor) = e; + return _buildLine(values: values, color: color, dotColor: dotColor); + }).toList(), + ), ); } From fb4d44450fd78e57c1cb19027a187e9a0968c62a Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 11:25:12 +0300 Subject: [PATCH 039/181] Disabled animation in `RangeOfAqiChart`. --- .../modules/air_quality/widgets/range_of_aqi_chart.dart | 1 + 1 file changed, 1 insertion(+) 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 0f941876..b6284e91 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 @@ -59,6 +59,7 @@ class RangeOfAqiChart extends StatelessWidget { return _buildLine(values: values, color: color, dotColor: dotColor); }).toList(), ), + duration: Duration.zero, ); } From 7e54cfdccd18c67e5bc3d63685610e06858bf421 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 13:25:14 +0300 Subject: [PATCH 040/181] Implemented min, max, average informative cells to `RangeOfAqiChartBox`. --- .../widgets/range_of_aqi_chart.dart | 3 +- .../widgets/range_of_aqi_chart_box.dart | 45 +++++++++++-- .../widgets/chart_informative_cell.dart | 63 +++++++++++++++++++ ...y_consumption_per_device_devices_list.dart | 39 +----------- 4 files changed, 106 insertions(+), 44 deletions(-) create mode 100644 lib/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart 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 b6284e91..4d60f33a 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 @@ -47,9 +47,10 @@ class RangeOfAqiChart extends StatelessWidget { gradient: LinearGradient( begin: Alignment.bottomCenter, end: Alignment.topCenter, + stops: [0.0, 0.2, 0.4, 0.6, 0.8, 1.0], colors: RangeOfAqiChartsHelper.gradientData.map((e) { final (color, _) = e; - return color.withValues(alpha: 0.8); + return color.withValues(alpha: 0.6); }).toList(), ), ), 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 86696392..07390480 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 @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; 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/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/analytics_error_widget.dart'; import 'package:syncrow_web/utils/style.dart'; @@ -24,15 +25,11 @@ class RangeOfAqiChartBox extends StatelessWidget { children: [ AnalyticsErrorWidget(state.errorMessage), const SizedBox(height: 10), - const ChartTitle(title: Text('Range of AQI')), + const RangeOfAqiChartTitle(), const SizedBox(height: 10), const Divider(), const SizedBox(height: 20), - Expanded( - child: RangeOfAqiChart( - chartData: state.rangeOfAqi, - ), - ), + Expanded(child: RangeOfAqiChart(chartData: state.rangeOfAqi)), ], ), ); @@ -40,3 +37,39 @@ class RangeOfAqiChartBox extends StatelessWidget { ); } } + +class RangeOfAqiChartTitle extends StatelessWidget { + const RangeOfAqiChartTitle({super.key}); + + static const List<(Color color, String title, bool hasBorder)> _colors = [ + (Color(0xFF962DFF), 'Max', false), + (Color(0xFF93AAFD), 'Min', false), + (Colors.transparent, 'Avg', true), + ]; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + const ChartTitle(title: Text('Range of AQI')), + const Spacer(), + ..._colors.map( + (e) { + final (color, title, hasBorder) = e; + return Padding( + padding: const EdgeInsetsDirectional.only(end: 16), + child: ChartInformativeCell( + title: Text(title), + color: color, + hasBorder: hasBorder, + ), + ); + }, + ), + const SizedBox(width: 34), + const Text('AQI'), + ], + ); + } +} diff --git a/lib/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart b/lib/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart new file mode 100644 index 00000000..05d2b2b5 --- /dev/null +++ b/lib/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class ChartInformativeCell extends StatelessWidget { + const ChartInformativeCell({ + super.key, + required this.title, + required this.color, + this.hasBorder = false, + }); + + final Widget title; + final Color color; + final bool hasBorder; + + @override + Widget build(BuildContext context) { + return Container( + height: MediaQuery.sizeOf(context).height * 0.0365, + padding: const EdgeInsetsDirectional.symmetric( + vertical: 8, + horizontal: 12, + ), + decoration: BoxDecoration( + borderRadius: BorderRadiusDirectional.circular(8), + border: Border.all( + color: ColorsManager.greyColor, + width: 1, + ), + ), + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.center, + child: Row( + spacing: 6, + children: [ + Container( + height: 8, + width: 8, + decoration: BoxDecoration( + color: color, + border: Border.all(color: ColorsManager.grayBorder), + shape: BoxShape.circle, + ), + ), + // CircleAvatar( + // radius: 4, + // backgroundColor: color, + // ), + DefaultTextStyle( + style: const TextStyle( + color: ColorsManager.blackColor, + fontWeight: FontWeight.w400, + fontSize: 14, + ), + child: title, + ), + ], + ), + ), + ); + } +} 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 e6996f53..b7205424 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 @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/analytics/models/analytics_device.dart'; import 'package:syncrow_web/pages/analytics/models/device_energy_data_model.dart'; -import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart'; class EnergyConsumptionPerDeviceDevicesList extends StatelessWidget { const EnergyConsumptionPerDeviceDevicesList({ @@ -42,42 +42,7 @@ class EnergyConsumptionPerDeviceDevicesList extends StatelessWidget { return Tooltip( message: '${device.name}\n${device.productDevice?.uuid ?? ''}', - child: Container( - height: MediaQuery.sizeOf(context).height * 0.0365, - padding: const EdgeInsetsDirectional.symmetric( - vertical: 8, - horizontal: 12, - ), - decoration: BoxDecoration( - borderRadius: BorderRadiusDirectional.circular(8), - border: Border.all( - color: ColorsManager.greyColor, - width: 1, - ), - ), - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: Alignment.center, - child: Row( - spacing: 6, - children: [ - CircleAvatar( - radius: 4, - backgroundColor: deviceColor, - ), - Text( - device.name, - textAlign: TextAlign.center, - style: const TextStyle( - color: ColorsManager.blackColor, - fontWeight: FontWeight.w400, - fontSize: 14, - ), - ), - ], - ), - ), - ), + child: ChartInformativeCell(title: Text(device.name), color: deviceColor), ); } } From 63ca98895f6ffb5bf106f5dfda185aff88784595 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 13:25:24 +0300 Subject: [PATCH 041/181] moved `RangeOfAqiChartTitle`. --- .../widgets/range_of_aqi_chart_box.dart | 39 +------------------ .../widgets/range_of_aqi_chart_title.dart | 39 +++++++++++++++++++ .../widgets/chart_informative_cell.dart | 6 +-- 3 files changed, 41 insertions(+), 43 deletions(-) create mode 100644 lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart 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 07390480..8e2333fe 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 @@ -2,8 +2,7 @@ import 'package:flutter/material.dart'; 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/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/modules/air_quality/widgets/range_of_aqi_chart_title.dart'; import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart'; import 'package:syncrow_web/utils/style.dart'; @@ -37,39 +36,3 @@ class RangeOfAqiChartBox extends StatelessWidget { ); } } - -class RangeOfAqiChartTitle extends StatelessWidget { - const RangeOfAqiChartTitle({super.key}); - - static const List<(Color color, String title, bool hasBorder)> _colors = [ - (Color(0xFF962DFF), 'Max', false), - (Color(0xFF93AAFD), 'Min', false), - (Colors.transparent, 'Avg', true), - ]; - - @override - Widget build(BuildContext context) { - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - const ChartTitle(title: Text('Range of AQI')), - const Spacer(), - ..._colors.map( - (e) { - final (color, title, hasBorder) = e; - return Padding( - padding: const EdgeInsetsDirectional.only(end: 16), - child: ChartInformativeCell( - title: Text(title), - color: color, - hasBorder: hasBorder, - ), - ); - }, - ), - const SizedBox(width: 34), - const Text('AQI'), - ], - ); - } -} 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 new file mode 100644 index 00000000..cea28c2f --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.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'; + +class RangeOfAqiChartTitle extends StatelessWidget { + const RangeOfAqiChartTitle({super.key}); + + static const List<(Color color, String title, bool hasBorder)> _colors = [ + (Color(0xFF962DFF), 'Max', false), + (Color(0xFF93AAFD), 'Min', false), + (Colors.transparent, 'Avg', true), + ]; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + const ChartTitle(title: Text('Range of AQI')), + const Spacer(), + ..._colors.map( + (e) { + final (color, title, hasBorder) = e; + return Padding( + padding: const EdgeInsetsDirectional.only(end: 16), + child: ChartInformativeCell( + title: Text(title), + color: color, + hasBorder: hasBorder, + ), + ); + }, + ), + const SizedBox(width: 34), + const Text('AQI'), + ], + ); + } +} diff --git a/lib/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart b/lib/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart index 05d2b2b5..eec31998 100644 --- a/lib/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart +++ b/lib/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart @@ -16,7 +16,7 @@ class ChartInformativeCell extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - height: MediaQuery.sizeOf(context).height * 0.0365, + height: MediaQuery.sizeOf(context).height * 0.0385, padding: const EdgeInsetsDirectional.symmetric( vertical: 8, horizontal: 12, @@ -43,10 +43,6 @@ class ChartInformativeCell extends StatelessWidget { shape: BoxShape.circle, ), ), - // CircleAvatar( - // radius: 4, - // backgroundColor: color, - // ), DefaultTextStyle( style: const TextStyle( color: ColorsManager.blackColor, From 5a8ef578c3e9d27f5984372ed07d522bbaf36e14 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 14:16:43 +0300 Subject: [PATCH 042/181] SP-1493-data-formatting --- .../widgets/power_clamp_energy_data_widget.dart | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) 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 8ae6cd7f..e8f802cd 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 @@ -69,13 +69,18 @@ class PowerClampEnergyDataWidget extends StatelessWidget { PowerClampEnergyStatus( iconPath: Assets.powerActiveIcon, title: 'Active', - value: _valueFromCode('EnergyConsumed', generalDataPoints), + value: _valueFromCode('ActivePower', generalDataPoints), unit: 'W', ), PowerClampEnergyStatus( iconPath: Assets.voltMeterIcon, title: 'Current', - value: _valueFromCode('Current', generalDataPoints), + value: _valueFromCode('Current', generalDataPoints) + .replaceAllMapped( + RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), + (Match m) => '${m[1]}.', + ) + .replaceAll('.0', ''), unit: 'A', ), PowerClampEnergyStatus( From 171dc52e280f30313b65b86944e95bebc50159d3 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 15:10:30 +0300 Subject: [PATCH 043/181] Created `AqiTypeDropdown`. --- .../fetch_air_quality_data_helper.dart | 15 +++- .../widgets/aqi_type_dropdown.dart | 80 +++++++++++++++++++ .../widgets/range_of_aqi_chart_title.dart | 21 ++++- .../params/get_range_of_aqi_param.dart | 8 +- 4 files changed, 119 insertions(+), 5 deletions(-) create mode 100644 lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart 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 a68e70da..65e62365 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,6 +1,7 @@ import 'package:flutter/material.dart'; 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/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'; @@ -21,7 +22,12 @@ abstract final class FetchAirQualityDataHelper { communityUuid: communityUuid, spaceUuid: spaceUuid, ); - loadRangeOfAqi(context, spaceUuid: spaceUuid, date: date); + loadRangeOfAqi( + context, + spaceUuid: spaceUuid, + date: date, + aqiType: AqiType.aqi, + ); } static void clearAllData(BuildContext context) { @@ -61,10 +67,15 @@ abstract final class FetchAirQualityDataHelper { BuildContext context, { required String spaceUuid, required DateTime date, + required AqiType aqiType, }) { context.read().add( LoadRangeOfAqiEvent( - GetRangeOfAqiParam(date: date, spaceUuid: spaceUuid), + 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 new file mode 100644 index 00000000..a9374204 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +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'), + pm10('PM10'), + co2('CO2'), + voc('VOC'), + tvoc('TVOC'); + + final String value; + const AqiType(this.value); +} + +class AqiTypeDropdown extends StatefulWidget { + const AqiTypeDropdown({super.key, required this.onChanged}); + + final ValueChanged onChanged; + + @override + State createState() => _AqiTypeDropdownState(); +} + +class _AqiTypeDropdownState extends State { + AqiType? _selectedItem = AqiType.aqi; + + void _updateSelectedItem(AqiType? item) => setState(() => _selectedItem = item); + + static const _defaultPadding = EdgeInsetsDirectional.symmetric( + horizontal: 12, + vertical: 2, + ); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: ColorsManager.greyColor, + width: 1, + ), + ), + child: DropdownButton( + value: _selectedItem, + isDense: true, + isExpanded: false, + selectedItemBuilder: (context) => [ + Text(_selectedItem?.value ?? ''), + ], + borderRadius: BorderRadius.circular(16), + dropdownColor: ColorsManager.whiteColors, + underline: const SizedBox.shrink(), + icon: const RotatedBox( + quarterTurns: 1, + child: Icon(Icons.chevron_right, size: 24), + ), + style: _getTextStyle(context), + padding: _defaultPadding, + items: AqiType.values + .map((e) => DropdownMenuItem(value: e, child: Text(e.value))) + .toList(), + onChanged: (value) { + _updateSelectedItem(value); + widget.onChanged(value); + }, + ), + ); + } + + TextStyle? _getTextStyle(BuildContext context) { + return context.textTheme.labelSmall?.copyWith( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w700, + fontSize: 14, + ); + } +} 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 cea28c2f..d07c96c8 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,6 +1,11 @@ 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/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/space_tree/bloc/space_tree_bloc.dart'; class RangeOfAqiChartTitle extends StatelessWidget { const RangeOfAqiChartTitle({super.key}); @@ -32,7 +37,21 @@ class RangeOfAqiChartTitle extends StatelessWidget { }, ), const SizedBox(width: 34), - const Text('AQI'), + AqiTypeDropdown( + onChanged: (value) { + final spaceTreeState = context.read().state; + final spaceUuid = spaceTreeState.selectedSpaces.firstOrNull; + + if (spaceUuid == null) return; + + FetchAirQualityDataHelper.loadRangeOfAqi( + context, + spaceUuid: spaceUuid, + date: context.read().state.monthlyDate, + aqiType: value ?? AqiType.aqi, + ); + }, + ), ], ); } 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 f55337eb..bbf24658 100644 --- a/lib/pages/analytics/params/get_range_of_aqi_param.dart +++ b/lib/pages/analytics/params/get_range_of_aqi_param.dart @@ -1,14 +1,18 @@ 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 List get props => [date, spaceUuid]; -} \ No newline at end of file +} From 0b4337fb6ccf5940ebee04ec04e3572afa20005e Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 15:17:29 +0300 Subject: [PATCH 044/181] sp-1493-data-formatting-2.0. --- .../widgets/power_clamp_phases_data_widget.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/analytics/modules/energy_management/widgets/power_clamp_phases_data_widget.dart b/lib/pages/analytics/modules/energy_management/widgets/power_clamp_phases_data_widget.dart index dc0aa050..a96a7298 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/power_clamp_phases_data_widget.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/power_clamp_phases_data_widget.dart @@ -55,7 +55,7 @@ class PowerClampPhasesDataWidget extends StatelessWidget { iconPath: Assets.powerActiveIcon, title: 'Active Power', value: _valueFromCode( - code: 'ReactivePower$phaseSuffix', + code: 'ActivePower$phaseSuffix', points: phase?.dataPoints, ), unit: 'W', From 177c7f1030a0b6816995f40749bad2c4b91feaa6 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 15:33:31 +0300 Subject: [PATCH 045/181] Responsiveness of `RangeOfAqiChartTitle`. --- .../widgets/aqi_type_dropdown.dart | 8 +-- .../widgets/range_of_aqi_chart_title.dart | 65 ++++++++++++------- 2 files changed, 46 insertions(+), 27 deletions(-) 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 a9374204..1b519f2e 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart @@ -6,9 +6,10 @@ enum AqiType { aqi('AQI'), pm25('PM2.5'), pm10('PM10'), + hcho('HCHO'), + tvoc('TVOC'), co2('CO2'), - voc('VOC'), - tvoc('TVOC'); + c6h6('C6H6'); final String value; const AqiType(this.value); @@ -47,9 +48,6 @@ class _AqiTypeDropdownState extends State { value: _selectedItem, isDense: true, isExpanded: false, - selectedItemBuilder: (context) => [ - Text(_selectedItem?.value ?? ''), - ], borderRadius: BorderRadius.circular(16), dropdownColor: ColorsManager.whiteColors, underline: const SizedBox.shrink(), 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 d07c96c8..a674acaa 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 @@ -19,38 +19,59 @@ class RangeOfAqiChartTitle extends StatelessWidget { @override Widget build(BuildContext context) { return Row( - mainAxisSize: MainAxisSize.min, children: [ - const ChartTitle(title: Text('Range of AQI')), - const Spacer(), + const Expanded( + flex: 3, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: AlignmentDirectional.centerStart, + child: ChartTitle(title: Text('Range of AQI')), + ), + ), + const Spacer(flex: 3), ..._colors.map( (e) { final (color, title, hasBorder) = e; - return Padding( - padding: const EdgeInsetsDirectional.only(end: 16), - child: ChartInformativeCell( - title: Text(title), - color: color, - hasBorder: hasBorder, + return Expanded( + child: IntrinsicHeight( + child: FittedBox( + fit: BoxFit.fitWidth, + alignment: AlignmentDirectional.centerStart, + child: Padding( + padding: const EdgeInsetsDirectional.only(end: 16), + child: ChartInformativeCell( + title: Text(title), + color: color, + hasBorder: hasBorder, + ), + ), + ), ), ); }, ), - const SizedBox(width: 34), - AqiTypeDropdown( - onChanged: (value) { - final spaceTreeState = context.read().state; - final spaceUuid = spaceTreeState.selectedSpaces.firstOrNull; + const Spacer(), + Expanded( + flex: 2, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: AlignmentDirectional.centerEnd, + 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, - ); - }, + FetchAirQualityDataHelper.loadRangeOfAqi( + context, + spaceUuid: spaceUuid, + date: context.read().state.monthlyDate, + aqiType: value ?? AqiType.aqi, + ); + }, + ), + ), ), ], ); From 296b03e1aa41bfa7ac407d796c1360d5cca19d26 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 15:54:56 +0300 Subject: [PATCH 046/181] shows month data instead of index on bottom titles of `RangeOfAqiChart`. --- .../helpers/range_of_aqi_charts_helper.dart | 14 +++++++++++++- .../air_quality/widgets/range_of_aqi_chart.dart | 3 ++- .../range_of_aqi/fake_range_of_aqi_service.dart | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart b/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart index 9c7ef2f2..5a455b88 100644 --- a/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart +++ b/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart @@ -1,5 +1,6 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/analytics/models/range_of_aqi.dart'; import 'package:syncrow_web/pages/analytics/modules/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'; @@ -16,9 +17,20 @@ abstract final class RangeOfAqiChartsHelper { (ColorsManager.hazardousPurple, 'Hazardous'), ]; - static FlTitlesData titlesData(BuildContext context) { + static FlTitlesData titlesData(BuildContext context, List data) { final titlesData = EnergyManagementChartsHelper.titlesData(context); return titlesData.copyWith( + bottomTitles: titlesData.bottomTitles.copyWith( + sideTitles: titlesData.bottomTitles.sideTitles.copyWith( + getTitlesWidget: (value, meta) => Text( + data.isNotEmpty ? data[value.toInt()].date.day.toString() : '', + style: context.textTheme.bodySmall?.copyWith( + fontSize: 12, + color: ColorsManager.lightGreyColor, + ), + ), + ), + ), leftTitles: titlesData.leftTitles.copyWith( sideTitles: titlesData.leftTitles.sideTitles.copyWith( reservedSize: 70, 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 4d60f33a..b4ca17f4 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 @@ -38,8 +38,9 @@ class RangeOfAqiChart extends StatelessWidget { minY: 0, maxY: 301, gridData: EnergyManagementChartsHelper.gridData(horizontalInterval: 50), - titlesData: RangeOfAqiChartsHelper.titlesData(context), + titlesData: RangeOfAqiChartsHelper.titlesData(context, chartData), borderData: EnergyManagementChartsHelper.borderData(), + lineTouchData: EnergyManagementChartsHelper.lineTouchData(), betweenBarsData: [ BetweenBarsData( fromIndex: 0, 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 63f71a76..434826d8 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 @@ -6,7 +6,7 @@ class FakeRangeOfAqiService implements RangeOfAqiService { @override Future> load(GetRangeOfAqiParam param) async { return List.generate(30, (index) { - final date = param.date.add(Duration(days: index)); + final date = DateTime(2025, 5, 1).add(Duration(days: index)); final min = (index * 8).toDouble(); final avg = min + 40; final max = avg + 40; From ec7b0aa0787b19f07b892f4e2b6b941f6360c32d Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 15:58:34 +0300 Subject: [PATCH 047/181] shows `AnalyticsErrorWidget` and spacing under it only when there is an error. --- .../modules/air_quality/widgets/range_of_aqi_chart_box.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 8e2333fe..bfe498c5 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 @@ -22,8 +22,10 @@ class RangeOfAqiChartBox extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - AnalyticsErrorWidget(state.errorMessage), - const SizedBox(height: 10), + if (state.errorMessage != null) ...[ + AnalyticsErrorWidget(state.errorMessage), + const SizedBox(height: 10), + ], const RangeOfAqiChartTitle(), const SizedBox(height: 10), const Divider(), From cb4956f915cfc31d25ede9f32b42c745ac91d908 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 16:56:25 +0300 Subject: [PATCH 048/181] made range of aqi fake data random and not linear. --- .../fake_range_of_aqi_service.dart | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) 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 434826d8..328e4433 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 @@ -5,20 +5,22 @@ import 'package:syncrow_web/pages/analytics/services/range_of_aqi/range_of_aqi_s class FakeRangeOfAqiService implements RangeOfAqiService { @override Future> load(GetRangeOfAqiParam param) async { + final random = DateTime.now().millisecondsSinceEpoch; + return List.generate(30, (index) { final date = DateTime(2025, 5, 1).add(Duration(days: index)); - final min = (index * 8).toDouble(); - final avg = min + 40; - final max = avg + 40; - final cappedMin = min > 301 ? 301.0 : min; - final cappedAvg = avg > 301 ? 301.0 : avg; - final cappedMax = max > 301 ? 301.0 : max; + final min = ((random + index * 17) % 200).toDouble(); + final avgDelta = ((random + index * 23) % 50).toDouble() + 20; + final maxDelta = ((random + index * 31) % 50).toDouble() + 30; + + final avg = (min + avgDelta).clamp(0.0, 301.0); + final max = (avg + maxDelta).clamp(0.0, 301.0); return RangeOfAqi( - min: cappedMin, - avg: cappedAvg, - max: cappedMax, + min: min, + avg: avg, + max: max, date: date, ); }); From 9546d7bdd1e3308604b9a9b3e8146dee37350c13 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 16:56:38 +0300 Subject: [PATCH 049/181] fixed titles widget for bottom title. --- .../helpers/range_of_aqi_charts_helper.dart | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart b/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart index 5a455b88..1bff8981 100644 --- a/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart +++ b/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart @@ -22,11 +22,14 @@ abstract final class RangeOfAqiChartsHelper { return titlesData.copyWith( bottomTitles: titlesData.bottomTitles.copyWith( sideTitles: titlesData.bottomTitles.sideTitles.copyWith( - getTitlesWidget: (value, meta) => Text( - data.isNotEmpty ? data[value.toInt()].date.day.toString() : '', - style: context.textTheme.bodySmall?.copyWith( - fontSize: 12, - color: ColorsManager.lightGreyColor, + getTitlesWidget: (value, meta) => Padding( + padding: const EdgeInsetsDirectional.only(top: 20.0), + child: Text( + data.isNotEmpty ? data[value.toInt()].date.day.toString() : '', + style: context.textTheme.bodySmall?.copyWith( + fontSize: 12, + color: ColorsManager.lightGreyColor, + ), ), ), ), From bee8652d03e606cab39c72d101836e0ca6c80e76 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 16:59:44 +0300 Subject: [PATCH 050/181] responsivness --- .../air_quality/widgets/air_quality_end_side_widget.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart index 2d6ace36..106685c1 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart @@ -51,7 +51,6 @@ class AirQualityEndSideWidget extends StatelessWidget { Expanded( flex: 3, child: FittedBox( - fit: BoxFit.scaleDown, alignment: AlignmentDirectional.centerStart, child: SelectableText( 'AQI Sensor', @@ -65,9 +64,8 @@ class AirQualityEndSideWidget extends StatelessWidget { ), const Spacer(), Expanded( - flex: 2, + flex: 4, child: FittedBox( - fit: BoxFit.scaleDown, alignment: AlignmentDirectional.centerEnd, child: AnalyticsDeviceDropdown( onChanged: (value) { From 8a5173f42986c4865291bf03072dd57c0e05f2b4 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 27 May 2025 09:35:22 +0300 Subject: [PATCH 051/181] made font size of `AqiTypeDropdown` slightly smaller. --- .../air_quality/widgets/aqi_type_dropdown.dart | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) 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 1b519f2e..ea85f075 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 @@ -29,11 +29,6 @@ class _AqiTypeDropdownState extends State { void _updateSelectedItem(AqiType? item) => setState(() => _selectedItem = item); - static const _defaultPadding = EdgeInsetsDirectional.symmetric( - horizontal: 12, - vertical: 2, - ); - @override Widget build(BuildContext context) { return Container( @@ -47,7 +42,6 @@ class _AqiTypeDropdownState extends State { child: DropdownButton( value: _selectedItem, isDense: true, - isExpanded: false, borderRadius: BorderRadius.circular(16), dropdownColor: ColorsManager.whiteColors, underline: const SizedBox.shrink(), @@ -56,7 +50,10 @@ class _AqiTypeDropdownState extends State { child: Icon(Icons.chevron_right, size: 24), ), style: _getTextStyle(context), - padding: _defaultPadding, + padding: const EdgeInsetsDirectional.symmetric( + horizontal: 12, + vertical: 2, + ), items: AqiType.values .map((e) => DropdownMenuItem(value: e, child: Text(e.value))) .toList(), @@ -72,7 +69,7 @@ class _AqiTypeDropdownState extends State { return context.textTheme.labelSmall?.copyWith( color: ColorsManager.textPrimaryColor, fontWeight: FontWeight.w700, - fontSize: 14, + fontSize: 12, ); } } From 1b0d8d446c653828c0272692038b3f9d7b11c55b Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 27 May 2025 09:47:06 +0300 Subject: [PATCH 052/181] modified flex's values. --- .../analytics/modules/air_quality/views/air_quality_view.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 be3b9b04..03df3ba9 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 @@ -41,7 +41,7 @@ class AirQualityView extends StatelessWidget { spacing: 32, children: [ Expanded( - flex: 2, + flex: 5, child: Column( spacing: 20, children: [ @@ -50,7 +50,7 @@ class AirQualityView extends StatelessWidget { ], ), ), - Expanded(child: AirQualityEndSideWidget()), + Expanded(flex: 2, child: AirQualityEndSideWidget()), ], ), ), From 50fc5f956291ccf60f79c8abcf966a81e0c3a992 Mon Sep 17 00:00:00 2001 From: mohammad Date: Tue, 27 May 2025 09:54:21 +0300 Subject: [PATCH 053/181] Add 'PC' device to routine --- assets/icons/energy_consumed_icon.svg | 10 + .../all_devices/models/devices_model.dart | 57 +++ .../dialog_helper/device_dialog_helper.dart | 11 + .../routines/models/device_functions.dart | 25 +- .../models/pc/energy_clamp_functions.dart | 416 ++++++++++++++++++ .../pc/enrgy_clamp_operational_value.dart | 11 + .../widgets/custom_routines_textbox.dart | 89 ++-- lib/pages/routines/widgets/if_container.dart | 16 +- .../routines/widgets/routine_devices.dart | 1 + .../widgets/routine_dialogs/ac_dialog.dart | 88 ++-- .../cps_dialog_slider_selector.dart | 3 +- .../enargy_operational_values_list.dart | 86 ++++ .../energy_clamp_dialog.dart | 246 +++++++++++ .../energy_value_selector_widget.dart | 85 ++++ .../wps_value_selector_widget.dart | 2 +- .../routines/widgets/then_container.dart | 3 +- lib/utils/constants/assets.dart | 5 +- lib/utils/enum/device_types.dart | 2 + 18 files changed, 1076 insertions(+), 80 deletions(-) create mode 100644 assets/icons/energy_consumed_icon.svg create mode 100644 lib/pages/routines/models/pc/energy_clamp_functions.dart create mode 100644 lib/pages/routines/models/pc/enrgy_clamp_operational_value.dart create mode 100644 lib/pages/routines/widgets/routine_dialogs/power_clamp_enargy/enargy_operational_values_list.dart create mode 100644 lib/pages/routines/widgets/routine_dialogs/power_clamp_enargy/energy_clamp_dialog.dart create mode 100644 lib/pages/routines/widgets/routine_dialogs/power_clamp_enargy/energy_value_selector_widget.dart diff --git a/assets/icons/energy_consumed_icon.svg b/assets/icons/energy_consumed_icon.svg new file mode 100644 index 00000000..d457619c --- /dev/null +++ b/assets/icons/energy_consumed_icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/lib/pages/device_managment/all_devices/models/devices_model.dart b/lib/pages/device_managment/all_devices/models/devices_model.dart index de1b7632..808a683f 100644 --- a/lib/pages/device_managment/all_devices/models/devices_model.dart +++ b/lib/pages/device_managment/all_devices/models/devices_model.dart @@ -12,6 +12,7 @@ import 'package:syncrow_web/pages/routines/models/gang_switches/one_gang_switch/ import 'package:syncrow_web/pages/routines/models/gang_switches/three_gang_switch/three_gang_switch.dart'; import 'package:syncrow_web/pages/routines/models/gang_switches/two_gang_switch/two_gang_switch.dart'; import 'package:syncrow_web/pages/routines/models/gateway.dart'; +import 'package:syncrow_web/pages/routines/models/pc/energy_clamp_functions.dart'; import 'package:syncrow_web/pages/routines/models/water_heater/water_heater_functions.dart'; import 'package:syncrow_web/pages/routines/models/wps/wps_functions.dart'; import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/ceiling_sensor/ceiling_sensor_helper.dart'; @@ -248,6 +249,8 @@ SOS tempIcon = Assets.waterLeakNormal; } else if (type == DeviceType.NCPS) { tempIcon = Assets.sensors; + } else if (type == DeviceType.PC) { + tempIcon = Assets.powerClamp; } else { tempIcon = Assets.logoHorizontal; } @@ -393,6 +396,59 @@ SOS BacklightFunction( deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'), ]; + case 'PC': + return [ + TotalEnergyConsumedStatusFunction( + deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'), + TotalActivePowerConsumedStatusFunction( + deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'), + VoltagePhaseSequenceDetectionFunction( + deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'), + TotalCurrentStatusFunction( + deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'), + FrequencyStatusFunction( + deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'), + + // Phase A + EnergyConsumedAStatusFunction( + deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'), + ActivePowerAStatusFunction( + deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'), + VoltageAStatusFunction( + deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'), + PowerFactorAStatusFunction( + deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'), + CurrentAStatusFunction( + deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'), + + // Phase B + EnergyConsumedBStatusFunction( + deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'), + ActivePowerBStatusFunction( + deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'), + VoltageBStatusFunction( + deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'), + CurrentBStatusFunction( + deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'), + PowerFactorBStatusFunction( + deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'), + + // Phase C + EnergyConsumedCStatusFunction( + deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'), + ActivePowerCStatusFunction( + deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'), + VoltageCStatusFunction( + deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'), + CurrentCStatusFunction( + deviceId: uuid ?? '', + deviceName: name ?? '', + type: 'IF'), + PowerFactorCStatusFunction( + deviceId: uuid ?? '', + deviceName: name ?? '', + type: 'IF'), + ]; default: return []; @@ -526,5 +582,6 @@ SOS "GD": DeviceType.GarageDoor, "WL": DeviceType.WaterLeak, "NCPS": DeviceType.NCPS, + "PC": DeviceType.PC, }; } diff --git a/lib/pages/routines/helper/dialog_helper/device_dialog_helper.dart b/lib/pages/routines/helper/dialog_helper/device_dialog_helper.dart index bdba5797..df4683d8 100644 --- a/lib/pages/routines/helper/dialog_helper/device_dialog_helper.dart +++ b/lib/pages/routines/helper/dialog_helper/device_dialog_helper.dart @@ -7,6 +7,7 @@ import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/ceiling_senso import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/flush_presence_sensor/flush_presence_sensor.dart'; import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/gateway/gateway_helper.dart'; import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/one_gang_switch_dialog.dart'; +import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/power_clamp_enargy/energy_clamp_dialog.dart'; import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/three_gang_switch_dialog.dart'; import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/two_gang_switch_dialog.dart'; import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/wall_sensor/wall_presence_sensor.dart'; @@ -137,6 +138,16 @@ class DeviceDialogHelper { device: data['device'], ); + case 'PC': + return EnergyClampDialog.showEnergyClampFunctionsDialog( + context: context, + functions: functions, + uniqueCustomId: data['uniqueCustomId'], + deviceSelectedFunctions: deviceSelectedFunctions, + dialogType: dialogType, + device: data['device'], + ); + default: return null; } diff --git a/lib/pages/routines/models/device_functions.dart b/lib/pages/routines/models/device_functions.dart index b895dccc..40b26304 100644 --- a/lib/pages/routines/models/device_functions.dart +++ b/lib/pages/routines/models/device_functions.dart @@ -9,7 +9,6 @@ abstract class DeviceFunction { final double? max; final double? min; - DeviceFunction({ required this.deviceId, required this.deviceName, @@ -114,4 +113,28 @@ class DeviceFunctionData { max.hashCode ^ min.hashCode; } + + DeviceFunctionData copyWith({ + String? entityId, + String? functionCode, + String? operationName, + String? condition, + dynamic value, + double? step, + String? unit, + double? max, + double? min, + }) { + return DeviceFunctionData( + entityId: entityId ?? this.entityId, + functionCode: functionCode ?? this.functionCode, + operationName: operationName ?? this.operationName, + condition: condition ?? this.condition, + value: value ?? this.value, + step: step ?? this.step, + unit: unit ?? this.unit, + max: max ?? this.max, + min: min ?? this.min, + ); + } } diff --git a/lib/pages/routines/models/pc/energy_clamp_functions.dart b/lib/pages/routines/models/pc/energy_clamp_functions.dart new file mode 100644 index 00000000..4bf3ddd8 --- /dev/null +++ b/lib/pages/routines/models/pc/energy_clamp_functions.dart @@ -0,0 +1,416 @@ +import 'package:syncrow_web/pages/device_managment/power_clamp/models/power_clamp_batch_model.dart'; +import 'package:syncrow_web/pages/routines/models/device_functions.dart'; +import 'package:syncrow_web/pages/routines/models/pc/enrgy_clamp_operational_value.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +abstract class EnergyClampFunctions extends DeviceFunction { + final String type; + + EnergyClampFunctions({ + required super.deviceId, + required super.deviceName, + required super.code, + required super.operationName, + required super.icon, + required this.type, + super.step, + super.unit, + super.max, + super.min, + }); + + List getOperationalValues(); +} + +// General & shared +class TotalEnergyConsumedStatusFunction extends EnergyClampFunctions { + TotalEnergyConsumedStatusFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'EnergyConsumed', + operationName: 'Total Energy Consumed', + icon: Assets.energyConsumedIcon, + min: 0.00, + max: 20000000.00, + step: 1, + unit: "kWh", + ); + + @override + List getOperationalValues() => []; +} + +class TotalActivePowerConsumedStatusFunction extends EnergyClampFunctions { + TotalActivePowerConsumedStatusFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'ActivePower', + operationName: 'Total Active Power', + icon: Assets.powerActiveIcon, + min: -19800000, + max: 19800000, + step: 0.1, + unit: "kW", + ); + + @override + List getOperationalValues() => []; +} + +class VoltagePhaseSequenceDetectionFunction extends EnergyClampFunctions { + VoltagePhaseSequenceDetectionFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'voltage_phase_seq', + operationName: 'Voltage phase sequence detection', + icon: Assets.voltageIcon, + ); + + @override + List getOperationalValues() => [ + EnergyClampOperationalValue( + icon: Assets.voltageIcon, description: '0', value: '0'), + EnergyClampOperationalValue( + icon: Assets.voltageIcon, description: '1', value: '1'), + EnergyClampOperationalValue( + icon: Assets.voltageIcon, description: '2', value: '2'), + EnergyClampOperationalValue( + icon: Assets.voltageIcon, description: '3', value: '3'), + EnergyClampOperationalValue( + icon: Assets.voltageIcon, description: '4', value: '4'), + EnergyClampOperationalValue( + icon: Assets.voltageIcon, description: '5', value: '5'), + ]; +} + +class TotalCurrentStatusFunction extends EnergyClampFunctions { + TotalCurrentStatusFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'Current', + operationName: 'Total Current', + icon: Assets.voltMeterIcon, + min: 0.000, + max: 9000.000, + step: 1, + unit: "A", + ); + + @override + List getOperationalValues() => []; +} + +class FrequencyStatusFunction extends EnergyClampFunctions { + FrequencyStatusFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'Frequency', + operationName: 'Frequency', + icon: Assets.frequencyIcon, + min: 0, + max: 80, + step: 1, + unit: "Hz", + ); + + @override + List getOperationalValues() => []; +} + +// Phase A +class EnergyConsumedAStatusFunction extends EnergyClampFunctions { + EnergyConsumedAStatusFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'EnergyConsumedA', + operationName: 'Energy Consumed A', + icon: Assets.energyConsumedIcon, + min: 0.00, + max: 20000000.00, + step: 1, + unit: "kWh", + ); + + @override + List getOperationalValues() => []; +} + +class ActivePowerAStatusFunction extends EnergyClampFunctions { + ActivePowerAStatusFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'ActivePowerA', + operationName: 'Active Power A', + icon: Assets.powerActiveIcon, + min: 200, + max: 300, + step: 1, + unit: "kW", + ); + + @override + List getOperationalValues() => []; +} + +class VoltageAStatusFunction extends EnergyClampFunctions { + VoltageAStatusFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'VoltageA', + operationName: 'Voltage A', + icon: Assets.voltageIcon, + min: 0.0, + max: 500, + step: 1, + unit: "V", + ); + + @override + List getOperationalValues() => []; +} + +class PowerFactorAStatusFunction extends EnergyClampFunctions { + PowerFactorAStatusFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'PowerFactorA', + operationName: 'Power Factor A', + icon: Assets.speedoMeter, + min: 0.00, + max: 1.00, + step: 0.1, + unit: "", + ); + + @override + List getOperationalValues() => []; +} + +class CurrentAStatusFunction extends EnergyClampFunctions { + CurrentAStatusFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'CurrentA', + operationName: 'Current A', + icon: Assets.voltMeterIcon, + min: 0.000, + max: 3000.000, + step: 1, + unit: "A", + ); + + @override + List getOperationalValues() => []; +} + +// Phase B +class EnergyConsumedBStatusFunction extends EnergyClampFunctions { + EnergyConsumedBStatusFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'EnergyConsumedB', + operationName: 'Energy Consumed B', + icon: Assets.energyConsumedIcon, + min: 0.00, + max: 20000000.00, + step: 1, + unit: "kWh", + ); + + @override + List getOperationalValues() => []; +} + +class ActivePowerBStatusFunction extends EnergyClampFunctions { + ActivePowerBStatusFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'ActivePowerB', + operationName: 'Active Power B', + icon: Assets.powerActiveIcon, + min: -6600000, + max: 6600000, + step: 1, + unit: "kW", + ); + + @override + List getOperationalValues() => []; +} + +class VoltageBStatusFunction extends EnergyClampFunctions { + VoltageBStatusFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'VoltageB', + operationName: 'Voltage B', + icon: Assets.voltageIcon, + min: 0.0, + max: 500, + step: 1, + unit: "V", + ); + + @override + List getOperationalValues() => []; +} + +class CurrentBStatusFunction extends EnergyClampFunctions { + CurrentBStatusFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'CurrentB', + operationName: 'Current B', + icon: Assets.voltMeterIcon, + min: 0.000, + max: 3000.000, + step: 1, + unit: "A", + ); + + @override + List getOperationalValues() => []; +} + +class PowerFactorBStatusFunction extends EnergyClampFunctions { + PowerFactorBStatusFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'PowerFactorB', + operationName: 'Power Factor B', + icon: Assets.speedoMeter, + min: 0.0, + max: 1.0, + step: 0.1, + unit: "", + ); + + @override + List getOperationalValues() => []; +} + +// Phase C +class EnergyConsumedCStatusFunction extends EnergyClampFunctions { + EnergyConsumedCStatusFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'EnergyConsumedC', + operationName: 'Energy Consumed C', + icon: Assets.energyConsumedIcon, + min: 0.00, + max: 20000000.00, + step: 1, + unit: "kWh", + ); + + @override + List getOperationalValues() => []; +} + +class ActivePowerCStatusFunction extends EnergyClampFunctions { + ActivePowerCStatusFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'ActivePowerC', + operationName: 'Active Power C', + icon: Assets.powerActiveIcon, + min: -6600000, + max: 6600000, + step: 1, + unit: "kW", + ); + + @override + List getOperationalValues() => []; +} + +class VoltageCStatusFunction extends EnergyClampFunctions { + VoltageCStatusFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'VoltageC', + operationName: 'Voltage C', + icon: Assets.voltageIcon, + min: 0.00, + max: 500, + step: 0.1, + unit: "V", + ); + + @override + List getOperationalValues() => []; +} + +class CurrentCStatusFunction extends EnergyClampFunctions { + CurrentCStatusFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'CurrentC', + operationName: 'Current C', + icon: Assets.voltMeterIcon, + min: 0.000, + max: 3000.000, + step: 0.1, + unit: "A", + ); + + @override + List getOperationalValues() => []; +} + +class PowerFactorCStatusFunction extends EnergyClampFunctions { + PowerFactorCStatusFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'PowerFactorC', + operationName: 'Power Factor C', + icon: Assets.speedoMeter, + min: 0.00, + max: 1.00, + step: 0.1, + unit: "", + ); + + @override + List getOperationalValues() => []; +} diff --git a/lib/pages/routines/models/pc/enrgy_clamp_operational_value.dart b/lib/pages/routines/models/pc/enrgy_clamp_operational_value.dart new file mode 100644 index 00000000..5d89acf6 --- /dev/null +++ b/lib/pages/routines/models/pc/enrgy_clamp_operational_value.dart @@ -0,0 +1,11 @@ +class EnergyClampOperationalValue { + final String icon; + final String description; + final dynamic value; + + EnergyClampOperationalValue({ + required this.icon, + required this.description, + required this.value, + }); +} diff --git a/lib/pages/routines/widgets/custom_routines_textbox.dart b/lib/pages/routines/widgets/custom_routines_textbox.dart index e9ada1c2..f0767df4 100644 --- a/lib/pages/routines/widgets/custom_routines_textbox.dart +++ b/lib/pages/routines/widgets/custom_routines_textbox.dart @@ -40,6 +40,7 @@ class CustomRoutinesTextbox extends StatefulWidget { class _CustomRoutinesTextboxState extends State { late final TextEditingController _controller; + bool hasError = false; String? errorMessage; @@ -55,29 +56,63 @@ class _CustomRoutinesTextboxState extends State { } } + bool _isInitialized = false; + @override void initState() { super.initState(); - int decimalPlaces = getDecimalPlaces(widget.stepIncreaseAmount); - double initialValue; - if (widget.initialValue != null && - widget.initialValue is num && - (widget.initialValue as num) == 0) { - initialValue = 0.0; + _initializeController(); + } + + void _initializeController() { + final decimalPlaces = getDecimalPlaces(widget.stepIncreaseAmount); + final dynamic initialValue = widget.initialValue; + double parsedValue; + + if (initialValue is num) { + parsedValue = initialValue.toDouble(); + } else if (initialValue is String) { + parsedValue = double.tryParse(initialValue) ?? widget.sliderRange.$1; } else { - initialValue = double.tryParse(widget.displayedValue) ?? 0.0; + parsedValue = widget.sliderRange.$1; } + _controller = TextEditingController( - text: initialValue.toStringAsFixed(decimalPlaces), + text: parsedValue.toStringAsFixed(decimalPlaces), ); + _isInitialized = true; } @override - void dispose() { - _controller.dispose(); - super.dispose(); + void didUpdateWidget(CustomRoutinesTextbox oldWidget) { + super.didUpdateWidget(oldWidget); + + if (widget.initialValue != oldWidget.initialValue && _isInitialized) { + final decimalPlaces = getDecimalPlaces(widget.stepIncreaseAmount); + final dynamic initialValue = widget.initialValue; + double newValue; + + if (initialValue is num) { + newValue = initialValue.toDouble(); + } else if (initialValue is String) { + newValue = double.tryParse(initialValue) ?? widget.sliderRange.$1; + } else { + newValue = widget.sliderRange.$1; + } + + final newValueText = newValue.toStringAsFixed(decimalPlaces); + if (_controller.text != newValueText) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _controller.text = newValueText; + _controller.selection = + TextSelection.collapsed(offset: _controller.text.length); + }); + } + } } + + void _validateInput(String value) { final doubleValue = double.tryParse(value); if (doubleValue == null) { @@ -121,18 +156,6 @@ class _CustomRoutinesTextboxState extends State { } } - @override - void didUpdateWidget(CustomRoutinesTextbox oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.initialValue != oldWidget.initialValue) { - if (widget.initialValue != null && - widget.initialValue is num && - (widget.initialValue as num) == 0) { - int decimalPlaces = getDecimalPlaces(widget.stepIncreaseAmount); - _controller.text = 0.0.toStringAsFixed(decimalPlaces); - } - } - } void _correctAndUpdateValue(String value) { final doubleValue = double.tryParse(value) ?? 0.0; @@ -227,9 +250,15 @@ class _CustomRoutinesTextboxState extends State { color: ColorsManager.blackColor, ), keyboardType: TextInputType.number, - inputFormatters: widget.withSpecialChar == true - ? [FilteringTextInputFormatter.digitsOnly] - : null, + inputFormatters: [ + FilteringTextInputFormatter.allow( + widget.withSpecialChar + ? RegExp(r'^-?\d*\.?\d{0,' + + decimalPlaces.toString() + + r'}$') + : RegExp(r'\d+'), + ), + ], decoration: const InputDecoration( border: InputBorder.none, isDense: true, @@ -268,8 +297,9 @@ class _CustomRoutinesTextboxState extends State { const SizedBox(height: 16), Padding( padding: const EdgeInsets.symmetric(horizontal: 32), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + child: Wrap( + alignment: WrapAlignment.spaceBetween, + direction: Axis.horizontal, children: [ Text( 'Min. ${widget.sliderRange.$1.toInt()}${widget.unit}', @@ -279,6 +309,9 @@ class _CustomRoutinesTextboxState extends State { fontWeight: FontWeight.w400, ), ), + const SizedBox( + width: 50, + ), Text( 'Max. ${widget.sliderRange.$2.toInt()}${widget.unit}', style: context.textTheme.bodySmall?.copyWith( diff --git a/lib/pages/routines/widgets/if_container.dart b/lib/pages/routines/widgets/if_container.dart index 0d7e9717..da77c7c2 100644 --- a/lib/pages/routines/widgets/if_container.dart +++ b/lib/pages/routines/widgets/if_container.dart @@ -78,9 +78,9 @@ class IfContainer extends StatelessWidget { 'CPS', 'NCPS', 'WH', + 'PC', ].contains(state.ifItems[index] ['productType'])) { - context.read().add( AddToIfContainer( state.ifItems[index], false)); @@ -137,8 +137,18 @@ class IfContainer extends StatelessWidget { context .read() .add(AddToIfContainer(mutableData, false)); - } else if (!['AC', '1G', '2G', '3G', 'WPS', 'GW', 'CPS', 'NCPS','WH'] - .contains(mutableData['productType'])) { + } else if (![ + 'AC', + '1G', + '2G', + '3G', + 'WPS', + 'GW', + 'CPS', + 'NCPS', + 'WH', + 'PC', + ].contains(mutableData['productType'])) { context .read() .add(AddToIfContainer(mutableData, false)); diff --git a/lib/pages/routines/widgets/routine_devices.dart b/lib/pages/routines/widgets/routine_devices.dart index 11a52ba7..f0b77467 100644 --- a/lib/pages/routines/widgets/routine_devices.dart +++ b/lib/pages/routines/widgets/routine_devices.dart @@ -27,6 +27,7 @@ class _RoutineDevicesState extends State { 'CPS', 'NCPS', 'WH', + 'PC', }; @override diff --git a/lib/pages/routines/widgets/routine_dialogs/ac_dialog.dart b/lib/pages/routines/widgets/routine_dialogs/ac_dialog.dart index fc58500e..cbf13178 100644 --- a/lib/pages/routines/widgets/routine_dialogs/ac_dialog.dart +++ b/lib/pages/routines/widgets/routine_dialogs/ac_dialog.dart @@ -74,25 +74,24 @@ class ACHelper { SizedBox( width: selectedFunction != null ? 320 : 360, child: _buildFunctionsList( - context: context, - acFunctions: acFunctions, - device: device, - onFunctionSelected: - (functionCode, operationName) { - RoutineTapFunctionHelper.onTapFunction( - context, - functionCode: functionCode, - functionOperationName: operationName, - functionValueDescription: - selectedFunctionData.valueDescription, - deviceUuid: device?.uuid, - codesToAddIntoFunctionsWithDefaultValue: [ - 'temp_set', - 'temp_current', - ], - defaultValue: 0); - }, - ), + context: context, + acFunctions: acFunctions, + onFunctionSelected: + (functionCode, operationName) { + RoutineTapFunctionHelper.onTapFunction( + context, + functionCode: functionCode, + functionOperationName: operationName, + functionValueDescription: + selectedFunctionData + .valueDescription, + deviceUuid: device?.uuid, + codesToAddIntoFunctionsWithDefaultValue: [ + 'temp_set', + 'temp_current', + ], + defaultValue: 0); + }), ), // Value selector if (selectedFunction != null) @@ -150,7 +149,6 @@ class ACHelper { required BuildContext context, required List acFunctions, required Function(String, String) onFunctionSelected, - required AllDevicesModel? device, }) { return ListView.separated( shrinkWrap: false, @@ -193,7 +191,6 @@ class ACHelper { ); } - /// Build value selector for AC functions dialog static Widget _buildValueSelector({ required BuildContext context, required String selectedFunction, @@ -207,19 +204,19 @@ class ACHelper { acFunctions.firstWhere((f) => f.code == selectedFunction); if (selectedFunction == 'temp_set' || selectedFunction == 'temp_current') { - // Convert stored integer value to display value final displayValue = - (selectedFunctionData?.value ?? selectedFn.min ?? 0) / 10; + (selectedFunctionData?.value ?? selectedFn.min!) / 10; final minValue = selectedFn.min! / 10; final maxValue = selectedFn.max! / 10; + return CustomRoutinesTextbox( withSpecialChar: true, dividendOfRange: maxValue, currentCondition: selectedFunctionData?.condition, dialogType: selectedFn.type, sliderRange: (minValue, maxValue), - displayedValue: displayValue.toStringAsFixed(1), - initialValue: displayValue.toDouble(), + displayedValue: displayValue.toString(), + initialValue: displayValue, unit: selectedFn.unit!, onConditionChanged: (condition) => context.read().add( AddFunction( @@ -228,7 +225,7 @@ class ACHelper { functionCode: selectedFunction, operationName: selectedFn.operationName, condition: condition, - value: 0, + value: (displayValue * 10).round(), step: selectedFn.step, unit: selectedFn.unit, max: selectedFn.max, @@ -236,28 +233,33 @@ class ACHelper { ), ), ), - onTextChanged: (value) => context.read().add( - AddFunction( - functionData: DeviceFunctionData( - entityId: device?.uuid ?? '', - functionCode: selectedFunction, - operationName: selectedFn.operationName, - value: (value * 10).round(), // Store as integer - condition: selectedFunctionData?.condition, - step: selectedFn.step, - unit: selectedFn.unit, - max: selectedFn.max, - min: selectedFn.min, + onTextChanged: (value) { + final numericValue = double.tryParse(value.toString()) ?? minValue; + context.read().add( + AddFunction( + functionData: DeviceFunctionData( + entityId: device?.uuid ?? '', + functionCode: selectedFunction, + operationName: selectedFn.operationName, + value: (numericValue * 10).round(), + condition: selectedFunctionData?.condition, + step: selectedFn.step, + unit: selectedFn.unit, + max: selectedFn.max, + min: selectedFn.min, + ), ), - ), - ), - stepIncreaseAmount: selectedFn.step! / 10, // Convert step for display + ); + }, + stepIncreaseAmount: selectedFn.step! / 10, ); } + // Rest of your existing code for other value selectors + final values = selectedFn.getOperationalValues(); return _buildOperationalValuesList( context: context, - values: selectedFn.getOperationalValues(), + values: values, selectedValue: selectedFunctionData?.value, device: device, operationName: operationName, @@ -311,7 +313,7 @@ class ACHelper { // ); // } - // /// Build condition toggle for AC functions dialog + /// Build condition toggle for AC functions dialog // static Widget _buildConditionToggle( // BuildContext context, // String? currentCondition, diff --git a/lib/pages/routines/widgets/routine_dialogs/ceiling_sensor/cps_dialog_slider_selector.dart b/lib/pages/routines/widgets/routine_dialogs/ceiling_sensor/cps_dialog_slider_selector.dart index 3d2473c9..f26bd52a 100644 --- a/lib/pages/routines/widgets/routine_dialogs/ceiling_sensor/cps_dialog_slider_selector.dart +++ b/lib/pages/routines/widgets/routine_dialogs/ceiling_sensor/cps_dialog_slider_selector.dart @@ -6,7 +6,6 @@ import 'package:syncrow_web/pages/routines/models/ceiling_presence_sensor_functi import 'package:syncrow_web/pages/routines/models/device_functions.dart'; import 'package:syncrow_web/pages/routines/widgets/custom_routines_textbox.dart'; import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/ceiling_sensor/cps_slider_helpers.dart'; -import 'package:syncrow_web/pages/routines/widgets/slider_value_selector.dart'; class CpsDialogSliderSelector extends StatelessWidget { const CpsDialogSliderSelector({ @@ -33,7 +32,7 @@ class CpsDialogSliderSelector extends StatelessWidget { @override Widget build(BuildContext context) { return CustomRoutinesTextbox( - withSpecialChar: false, + withSpecialChar: true, currentCondition: selectedFunctionData.condition, dialogType: dialogType, sliderRange: diff --git a/lib/pages/routines/widgets/routine_dialogs/power_clamp_enargy/enargy_operational_values_list.dart b/lib/pages/routines/widgets/routine_dialogs/power_clamp_enargy/enargy_operational_values_list.dart new file mode 100644 index 00000000..2b8ba68f --- /dev/null +++ b/lib/pages/routines/widgets/routine_dialogs/power_clamp_enargy/enargy_operational_values_list.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; +import 'package:syncrow_web/pages/routines/bloc/functions_bloc/functions_bloc_bloc.dart'; +import 'package:syncrow_web/pages/routines/models/device_functions.dart'; +import 'package:syncrow_web/pages/routines/models/pc/enrgy_clamp_operational_value.dart'; + +class EnergyOperationalValuesList extends StatelessWidget { + final List values; + final dynamic selectedValue; + final AllDevicesModel? device; + final String operationName; + final String selectCode; + + const EnergyOperationalValuesList({ + required this.values, + required this.selectedValue, + required this.device, + required this.operationName, + required this.selectCode, + super.key, + }); + + @override + Widget build(BuildContext context) { + return ListView.builder( + padding: const EdgeInsets.all(20), + itemCount: values.length, + itemBuilder: (context, index) => _buildValueItem(context, values[index]), + ); + } + + Widget _buildValueItem( + BuildContext context, EnergyClampOperationalValue value) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildValueIcon(context, value), + Expanded(child: _buildValueDescription(value)), + _buildValueRadio(context, value), + ], + ), + ); + } + + Widget _buildValueIcon(context, EnergyClampOperationalValue value) { + return Column( + children: [ + SvgPicture.asset(value.icon, width: 25, height: 25), + ], + ); + } + + Widget _buildValueDescription(EnergyClampOperationalValue value) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text(value.description), + ); + } + + Widget _buildValueRadio(context, EnergyClampOperationalValue value) { + return Radio( + value: value.value, + groupValue: selectedValue, + onChanged: (_) => _selectValue(context, value.value), + ); + } + + void _selectValue(BuildContext context, dynamic value) { + context.read().add( + AddFunction( + functionData: DeviceFunctionData( + entityId: device?.uuid ?? '', + functionCode: selectCode, + operationName: operationName, + value: value, + ), + ), + ); + } + + +} 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 new file mode 100644 index 00000000..c5bf8828 --- /dev/null +++ b/lib/pages/routines/widgets/routine_dialogs/power_clamp_enargy/energy_clamp_dialog.dart @@ -0,0 +1,246 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; +import 'package:syncrow_web/pages/routines/bloc/functions_bloc/functions_bloc_bloc.dart'; +import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routines/models/device_functions.dart'; +import 'package:syncrow_web/pages/routines/models/pc/energy_clamp_functions.dart'; +import 'package:syncrow_web/pages/routines/widgets/dialog_footer.dart'; +import 'package:syncrow_web/pages/routines/widgets/dialog_header.dart'; +import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/helpers/routine_tap_function_helper.dart'; +import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/power_clamp_enargy/energy_value_selector_widget.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class EnergyClampDialog extends StatefulWidget { + final List functions; + final AllDevicesModel? device; + final List? deviceSelectedFunctions; + final String? uniqueCustomId; + final String? dialogType; + final bool removeComparetors; + + const EnergyClampDialog({ + super.key, + required this.functions, + this.device, + this.deviceSelectedFunctions, + this.uniqueCustomId, + this.dialogType, + this.removeComparetors = false, + }); + + static Future?> showEnergyClampFunctionsDialog({ + required BuildContext context, + required List functions, + AllDevicesModel? device, + List? deviceSelectedFunctions, + String? uniqueCustomId, + String? dialogType, + bool removeComparetors = false, + }) async { + return showDialog?>( + context: context, + builder: (context) => EnergyClampDialog( + functions: functions, + device: device, + deviceSelectedFunctions: deviceSelectedFunctions, + uniqueCustomId: uniqueCustomId, + removeComparetors: removeComparetors, + dialogType: dialogType, + ), + ); + } + + @override + State createState() => _EnergyClampDialogState(); +} + +class _EnergyClampDialogState extends State { + late final List _functions; + + @override + void initState() { + super.initState(); + _functions = + widget.functions.whereType().where((function) { + if (widget.dialogType == 'THEN') { + return function.type == 'THEN' || function.type == 'BOTH'; + } + return function.type == 'IF' || function.type == 'BOTH'; + }).toList(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => FunctionBloc() + ..add(InitializeFunctions(widget.deviceSelectedFunctions ?? [])), + child: _buildDialogContent(), + ); + } + + Widget _buildDialogContent() { + return AlertDialog( + contentPadding: EdgeInsets.zero, + content: BlocBuilder( + builder: (context, state) { + final selectedFunction = state.selectedFunction; + return Container( + width: selectedFunction != null ? 600 : 360, + height: 450, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + padding: const EdgeInsets.only(top: 20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const DialogHeader('Energy Clamp Conditions'), + Expanded(child: _buildMainContent(context, state)), + _buildDialogFooter(context, state), + ], + ), + ); + }, + ), + ); + } + + Widget _buildMainContent(BuildContext context, FunctionBlocState state) { + return Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildFunctionList(context, state), + if (state.selectedFunction != null) _buildValueSelector(context, state), + ], + ); + } + + Widget _buildFunctionList(BuildContext context, FunctionBlocState state) { + final selectedFunction = state.selectedFunction; + final selectedFunctionData = state.addedFunctions.firstWhere( + (f) => f.functionCode == selectedFunction, + orElse: () => DeviceFunctionData( + entityId: '', + functionCode: selectedFunction ?? '', + operationName: '', + value: null, + ), + ); + return SizedBox( + width: 360, + child: ListView.separated( + shrinkWrap: false, + physics: const AlwaysScrollableScrollPhysics(), + itemCount: _functions.length, + separatorBuilder: (context, index) => const Padding( + padding: EdgeInsets.symmetric(horizontal: 40.0), + child: Divider(color: ColorsManager.dividerColor), + ), + itemBuilder: (context, index) { + final function = _functions[index]; + return ListTile( + leading: SvgPicture.asset( + function.icon, + width: 24, + height: 24, + placeholderBuilder: (context) => const SizedBox( + width: 24, + height: 24, + ), + ), + title: Text( + function.operationName, + style: context.textTheme.bodyMedium, + ), + trailing: const Icon( + Icons.arrow_forward_ios, + size: 16, + color: ColorsManager.textGray, + ), + onTap: () => RoutineTapFunctionHelper.onTapFunction( + context, + functionCode: function.code, + functionOperationName: function.operationName, + functionValueDescription: selectedFunctionData.valueDescription, + deviceUuid: widget.device?.uuid, + codesToAddIntoFunctionsWithDefaultValue: [ + 'VoltageA', + 'CurrentA', + 'ActivePowerA', + 'PowerFactorA', + 'ReactivePowerA', + 'EnergyConsumedA', + 'VoltageB', + 'CurrentB', + 'ActivePowerB', + 'PowerFactorB', + 'ReactivePowerB', + 'EnergyConsumedB', + 'VoltageC', + 'CurrentC', + 'ActivePowerC', + 'PowerFactorC', + 'ReactivePowerC', + 'EnergyConsumedC', + 'EnergyConsumed', + 'Current', + 'ActivePower', + 'ReactivePower', + 'Frequency', + ], + ), + ); + }, + ), + ); + } + + Widget _buildValueSelector(BuildContext context, FunctionBlocState state) { + final selectedFunction = state.selectedFunction!; + final functionData = state.addedFunctions.firstWhere( + (f) => f.functionCode == selectedFunction, + orElse: () => DeviceFunctionData( + entityId: '', + functionCode: selectedFunction, + operationName: state.selectedOperationName ?? '', + value: null, + ), + ); + + return Expanded( + child: EnergyValueSelectorWidget( + selectedFunction: selectedFunction, + functionData: functionData, + functions: _functions, + device: widget.device, + dialogType: widget.dialogType!, + removeComparators: widget.removeComparetors, + ), + ); + } + + Widget _buildDialogFooter(BuildContext context, FunctionBlocState state) { + return DialogFooter( + onCancel: () => Navigator.pop(context), + onConfirm: state.addedFunctions.isNotEmpty + ? () { + context.read().add( + AddFunctionToRoutine( + state.addedFunctions, + widget.uniqueCustomId!, + ), + ); + Navigator.pop( + context, + {'deviceId': widget.functions.first.deviceId}, + ); + } + : null, + isConfirmEnabled: state.selectedFunction != null, + ); + } +} diff --git a/lib/pages/routines/widgets/routine_dialogs/power_clamp_enargy/energy_value_selector_widget.dart b/lib/pages/routines/widgets/routine_dialogs/power_clamp_enargy/energy_value_selector_widget.dart new file mode 100644 index 00000000..696251a1 --- /dev/null +++ b/lib/pages/routines/widgets/routine_dialogs/power_clamp_enargy/energy_value_selector_widget.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; +import 'package:syncrow_web/pages/routines/bloc/functions_bloc/functions_bloc_bloc.dart'; +import 'package:syncrow_web/pages/routines/models/device_functions.dart'; +import 'package:syncrow_web/pages/routines/models/pc/energy_clamp_functions.dart'; +import 'package:syncrow_web/pages/routines/widgets/custom_routines_textbox.dart'; +import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/power_clamp_enargy/enargy_operational_values_list.dart'; + +class EnergyValueSelectorWidget extends StatelessWidget { + final String selectedFunction; + final DeviceFunctionData functionData; + final List functions; + final AllDevicesModel? device; + final String dialogType; + final bool removeComparators; + + const EnergyValueSelectorWidget({ + required this.selectedFunction, + required this.functionData, + required this.functions, + required this.device, + required this.dialogType, + required this.removeComparators, + super.key, + }); + + @override + Widget build(BuildContext context) { + final selectedFn = + functions.firstWhere((f) => f.code == selectedFunction); + final values = selectedFn.getOperationalValues(); + final step = selectedFn.step ?? 1.0; + final _unit = selectedFn.unit ?? ''; + final (double, double) sliderRange = + (selectedFn.min ?? 0.0, selectedFn.max ?? 100.0); + + if (_isSliderFunction(selectedFunction)) { + return CustomRoutinesTextbox( + withSpecialChar: false, + currentCondition: functionData.condition, + dialogType: dialogType, + sliderRange: sliderRange, + displayedValue: functionData.value, + initialValue: functionData.value ?? 0.0, + onConditionChanged: (condition) => context.read().add( + AddFunction( + functionData: DeviceFunctionData( + entityId: device?.uuid ?? '', + functionCode: selectedFunction, + operationName: functionData.operationName, + condition: condition, + value: functionData.value ?? 0, + ), + ), + ), + onTextChanged: (value) => context.read().add( + AddFunction( + functionData: DeviceFunctionData( + entityId: device?.uuid ?? '', + functionCode: selectedFunction, + operationName: functionData.operationName, + value: value.toInt(), + condition: functionData.condition, + ), + ), + ), + unit: _unit, + dividendOfRange: 1, + stepIncreaseAmount: step, + ); + } + + return EnergyOperationalValuesList( + values: values, + selectedValue: functionData.value, + device: device, + operationName: selectedFn.operationName, + selectCode: selectedFunction, + ); + } + + bool _isSliderFunction(String function) => + !['voltage_phase_seq'].contains(function); +} diff --git a/lib/pages/routines/widgets/routine_dialogs/wall_sensor/wps_value_selector_widget.dart b/lib/pages/routines/widgets/routine_dialogs/wall_sensor/wps_value_selector_widget.dart index 677c26ee..61a7959b 100644 --- a/lib/pages/routines/widgets/routine_dialogs/wall_sensor/wps_value_selector_widget.dart +++ b/lib/pages/routines/widgets/routine_dialogs/wall_sensor/wps_value_selector_widget.dart @@ -33,7 +33,7 @@ class WpsValueSelectorWidget extends StatelessWidget { if (_isSliderFunction(selectedFunction)) { return CustomRoutinesTextbox( - withSpecialChar: false, + withSpecialChar: true, currentCondition: functionData.condition, dialogType: dialogType, sliderRange: sliderRange, diff --git a/lib/pages/routines/widgets/then_container.dart b/lib/pages/routines/widgets/then_container.dart index 0324d562..d9eee4c4 100644 --- a/lib/pages/routines/widgets/then_container.dart +++ b/lib/pages/routines/widgets/then_container.dart @@ -242,7 +242,8 @@ class ThenContainer extends StatelessWidget { 'GW', 'CPS', "NCPS", - "WH" + "WH", + 'PC', ].contains(mutableData['productType'])) { context.read().add(AddToThenContainer(mutableData)); } diff --git a/lib/utils/constants/assets.dart b/lib/utils/constants/assets.dart index a1d782f2..8707e7fd 100644 --- a/lib/utils/constants/assets.dart +++ b/lib/utils/constants/assets.dart @@ -481,5 +481,8 @@ class Assets { static const String indentLevelIcon = 'assets/icons/indent_level_icon.svg'; static const String triggerLevelIcon = 'assets/icons/trigger_level_icon.svg'; static const String blankCalendar = 'assets/icons/blank_calendar.svg'; - static const String refreshStatusIcon = 'assets/icons/refresh_status_icon.svg'; + static const String refreshStatusIcon = + 'assets/icons/refresh_status_icon.svg'; + static const String energyConsumedIcon = + 'assets/icons/energy_consumed_icon.svg'; } diff --git a/lib/utils/enum/device_types.dart b/lib/utils/enum/device_types.dart index 7ad8e02c..9bfd322f 100644 --- a/lib/utils/enum/device_types.dart +++ b/lib/utils/enum/device_types.dart @@ -19,6 +19,7 @@ enum DeviceType { WaterLeak, NCPS, DoorSensor, + PC, Other, } /* @@ -59,4 +60,5 @@ Map devicesTypesMap = { 'GD': DeviceType.GarageDoor, 'WL': DeviceType.WaterLeak, 'NCPS': DeviceType.NCPS, + 'PC': DeviceType.PC, }; From f5d926f5a2171d684016c7445379c6e8e9b59dfe Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 27 May 2025 12:21:59 +0300 Subject: [PATCH 054/181] modify left side titles. --- .../helpers/range_of_aqi_charts_helper.dart | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart b/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart index 1bff8981..381a8d03 100644 --- a/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart +++ b/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart @@ -37,17 +37,10 @@ abstract final class RangeOfAqiChartsHelper { leftTitles: titlesData.leftTitles.copyWith( sideTitles: titlesData.leftTitles.sideTitles.copyWith( reservedSize: 70, - interval: 51, - maxIncluded: true, + interval: 50, + maxIncluded: false, getTitlesWidget: (value, meta) { - String text; - if (value >= 300) { - text = '301+'; - } else if (value == 255) { - text = '300'; - } else { - text = ((value / 50).round() * 50).toInt().toString(); - } + final text = value >= 300 ? '301+' : value.toInt().toString(); return Padding( padding: const EdgeInsetsDirectional.only(end: 12), child: FittedBox( From 3ac5254abf8b61ef61a7daf63df2efdbabcf6fc7 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 27 May 2025 12:26:21 +0300 Subject: [PATCH 055/181] fixed bug in total energy consumption chart. --- .../widgets/total_energy_consumption_chart.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart.dart b/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart.dart index 204261df..1f1f9a3f 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart.dart @@ -35,9 +35,6 @@ class TotalEnergyConsumptionChart extends StatelessWidget { List get _lineBarsData { return [ LineChartBarData( - preventCurveOvershootingThreshold: 0.1, - curveSmoothness: 0.55, - preventCurveOverShooting: true, spots: chartData .asMap() .entries From d90d3d4026aa234032f79c7a5ff6aaff22491afc Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 27 May 2025 12:29:06 +0300 Subject: [PATCH 056/181] added loading state to range of aqi chart. --- .../widgets/range_of_aqi_chart_box.dart | 4 ++- .../widgets/range_of_aqi_chart_title.dart | 5 +++- .../fake_range_of_aqi_service.dart | 30 ++++++++++--------- 3 files changed, 23 insertions(+), 16 deletions(-) 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 bfe498c5..0fe4c4bd 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 @@ -26,7 +26,9 @@ class RangeOfAqiChartBox extends StatelessWidget { AnalyticsErrorWidget(state.errorMessage), const SizedBox(height: 10), ], - const RangeOfAqiChartTitle(), + RangeOfAqiChartTitle( + isLoading: state.status == RangeOfAqiStatus.loading, + ), const SizedBox(height: 10), const Divider(), const SizedBox(height: 20), 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 a674acaa..04cefd6c 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 @@ -5,10 +5,12 @@ import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type 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({super.key}); + const RangeOfAqiChartTitle({required this.isLoading, super.key}); + final bool isLoading; static const List<(Color color, String title, bool hasBorder)> _colors = [ (Color(0xFF962DFF), 'Max', false), @@ -20,6 +22,7 @@ class RangeOfAqiChartTitle extends StatelessWidget { Widget build(BuildContext context) { return Row( children: [ + ChartsLoadingWidget(isLoading: isLoading), const Expanded( flex: 3, child: FittedBox( 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 328e4433..13173c94 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 @@ -5,24 +5,26 @@ import 'package:syncrow_web/pages/analytics/services/range_of_aqi/range_of_aqi_s class FakeRangeOfAqiService implements RangeOfAqiService { @override Future> load(GetRangeOfAqiParam param) async { - final random = DateTime.now().millisecondsSinceEpoch; + return await Future.delayed(const Duration(milliseconds: 800), () { + final random = DateTime.now().millisecondsSinceEpoch; - return List.generate(30, (index) { - final date = DateTime(2025, 5, 1).add(Duration(days: index)); + return List.generate(30, (index) { + final date = DateTime(2025, 5, 1).add(Duration(days: index)); - final min = ((random + index * 17) % 200).toDouble(); - final avgDelta = ((random + index * 23) % 50).toDouble() + 20; - final maxDelta = ((random + index * 31) % 50).toDouble() + 30; + final min = ((random + index * 17) % 200).toDouble(); + final avgDelta = ((random + index * 23) % 50).toDouble() + 20; + final maxDelta = ((random + index * 31) % 50).toDouble() + 30; - final avg = (min + avgDelta).clamp(0.0, 301.0); - final max = (avg + maxDelta).clamp(0.0, 301.0); + 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, - date: date, - ); + return RangeOfAqi( + min: min, + avg: avg, + max: max, + date: date, + ); + }); }); } } From 043820f84faf1a2848e0de598c8cd460513bfbdb Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 27 May 2025 12:33:48 +0300 Subject: [PATCH 057/181] does not emit an entirely new state when we already have chart data on loading. --- .../air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 e19f4345..febbcf58 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 @@ -19,7 +19,12 @@ class RangeOfAqiBloc extends Bloc { LoadRangeOfAqiEvent event, Emitter emit, ) async { - emit(const RangeOfAqiState(status: RangeOfAqiStatus.loading)); + emit( + RangeOfAqiState( + status: RangeOfAqiStatus.loading, + rangeOfAqi: state.rangeOfAqi, + ), + ); try { final rangeOfAqi = await _rangeOfAqiService.load(event.param); emit(RangeOfAqiState(status: RangeOfAqiStatus.loaded, rangeOfAqi: rangeOfAqi)); From 1aa7bf216232990bd913f9afc6cd4ab14be061d3 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 27 May 2025 12:37:36 +0300 Subject: [PATCH 058/181] fixed charts clipping overflow in chart. --- .../modules/air_quality/widgets/range_of_aqi_chart.dart | 1 + .../widgets/energy_consumption_by_phases_chart.dart | 1 + .../widgets/energy_consumption_per_device_chart.dart | 1 + .../widgets/total_energy_consumption_chart.dart | 2 ++ 4 files changed, 5 insertions(+) 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 b4ca17f4..887cdb69 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 @@ -37,6 +37,7 @@ class RangeOfAqiChart extends StatelessWidget { LineChartData( minY: 0, maxY: 301, + clipData: const FlClipData.vertical(), gridData: EnergyManagementChartsHelper.gridData(horizontalInterval: 50), titlesData: RangeOfAqiChartsHelper.titlesData(context, chartData), borderData: EnergyManagementChartsHelper.borderData(), diff --git a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_by_phases_chart.dart b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_by_phases_chart.dart index 1497d0fd..0019d3b5 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_by_phases_chart.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_by_phases_chart.dart @@ -18,6 +18,7 @@ class EnergyConsumptionByPhasesChart extends StatelessWidget { Widget build(BuildContext context) { return BarChart( BarChartData( + gridData: EnergyManagementChartsHelper.gridData().copyWith( checkToShowHorizontalLine: (value) => true, horizontalInterval: 250, diff --git a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart.dart b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart.dart index fcf7d384..1e74ad31 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart.dart @@ -12,6 +12,7 @@ class EnergyConsumptionPerDeviceChart extends StatelessWidget { Widget build(BuildContext context) { return LineChart( LineChartData( + clipData: const FlClipData.vertical(), titlesData: EnergyManagementChartsHelper.titlesData( context, leftTitlesInterval: 250, diff --git a/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart.dart b/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart.dart index 1f1f9a3f..85b95c29 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart.dart @@ -14,6 +14,7 @@ class TotalEnergyConsumptionChart extends StatelessWidget { return Expanded( child: LineChart( LineChartData( + clipData: const FlClipData.vertical(), titlesData: EnergyManagementChartsHelper.titlesData( context, leftTitlesInterval: 250, @@ -28,6 +29,7 @@ class TotalEnergyConsumptionChart extends StatelessWidget { ), duration: Duration.zero, curve: Curves.easeIn, + ), ); } From a1142eb38c2fb85cab8ee7620ed72e77b820f989 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 27 May 2025 13:17:05 +0300 Subject: [PATCH 059/181] gave range of aqi chart a tooltip that shows the necessary data. --- .../helpers/range_of_aqi_charts_helper.dart | 52 +++++++++++++++++++ .../widgets/range_of_aqi_chart.dart | 6 +-- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart b/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart index 381a8d03..21cb2a9e 100644 --- a/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart +++ b/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart @@ -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/range_of_aqi.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart'; import 'package:syncrow_web/utils/color_manager.dart'; @@ -60,4 +61,55 @@ abstract final class RangeOfAqiChartsHelper { ), ); } + + static List getTooltipItems( + List touchedSpots, + List chartData, + ) { + return touchedSpots.asMap().entries.map((entry) { + final index = entry.key; + final spot = entry.value; + + final label = switch (spot.barIndex) { + 0 => 'Max', + 1 => 'Avg', + 2 => 'Min', + _ => '', + }; + + final date = DateFormat('dd/MM').format(chartData[spot.x.toInt()].date); + + return LineTooltipItem( + index == 0 ? '$date\n' : '', + const TextStyle( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w600, + fontSize: 12, + ), + children: [ + TextSpan(text: '$label: ${spot.y.toStringAsFixed(0)}'), + ], + ); + }).toList(); + } + + static LineTouchData lineTouchData( + List chartData, + ) { + return LineTouchData( + touchTooltipData: LineTouchTooltipData( + getTooltipColor: (touchTooltipItem) => ColorsManager.whiteColors, + tooltipBorder: const BorderSide( + color: ColorsManager.semiTransparentBlack, + ), + tooltipRoundedRadius: 16, + showOnTopOfTheChartBoxArea: false, + tooltipPadding: const EdgeInsets.all(8), + getTooltipItems: (touchedSpots) => RangeOfAqiChartsHelper.getTooltipItems( + touchedSpots, + chartData, + ), + ), + ); + } } 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 887cdb69..2cefaa8e 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 @@ -17,7 +17,7 @@ class RangeOfAqiChart extends StatelessWidget { ( chartData.map((e) => e.max).toList(), const Color(0xFF962DFF), - const Color(0xFF5F00BD) + const Color(0xFF5F00BD), ), ( chartData.map((e) => e.avg).toList(), @@ -27,7 +27,7 @@ class RangeOfAqiChart extends StatelessWidget { ( chartData.map((e) => e.min).toList(), const Color(0xFF93AAFD), - const Color(0xFF023DFE) + const Color(0xFF023DFE), ), ]; @@ -41,7 +41,7 @@ class RangeOfAqiChart extends StatelessWidget { gridData: EnergyManagementChartsHelper.gridData(horizontalInterval: 50), titlesData: RangeOfAqiChartsHelper.titlesData(context, chartData), borderData: EnergyManagementChartsHelper.borderData(), - lineTouchData: EnergyManagementChartsHelper.lineTouchData(), + lineTouchData: RangeOfAqiChartsHelper.lineTouchData(chartData), betweenBarsData: [ BetweenBarsData( fromIndex: 0, From 03009ed27622c10be0396df7731f6d057c7bb42f Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 27 May 2025 13:21:42 +0300 Subject: [PATCH 060/181] made a `RangeOfAqiChart._lines` a getter. --- .../modules/air_quality/widgets/range_of_aqi_chart.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 2cefaa8e..9423c30d 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,7 +13,7 @@ class RangeOfAqiChart extends StatelessWidget { required this.chartData, }); - List<(List values, Color color, Color? dotColor)> _lines() => [ + List<(List values, Color color, Color? dotColor)> get _lines => [ ( chartData.map((e) => e.max).toList(), const Color(0xFF962DFF), @@ -57,7 +57,7 @@ class RangeOfAqiChart extends StatelessWidget { ), ), ], - lineBarsData: _lines().map((e) { + lineBarsData: _lines.map((e) { final (values, color, dotColor) = e; return _buildLine(values: values, color: color, dotColor: dotColor); }).toList(), From 95eca869c9a0c7d06bc6d58a3bef0c4265d9597f Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 27 May 2025 15:12:11 +0300 Subject: [PATCH 061/181] Implemented `AqiSubValueWidget`. --- .../air_quality/views/air_quality_view.dart | 7 +- .../widgets/air_quality_end_side_widget.dart | 41 ++++++ .../widgets/aqi_sub_value_widget.dart | 139 ++++++++++++++++++ 3 files changed, 185 insertions(+), 2 deletions(-) create mode 100644 lib/pages/analytics/modules/air_quality/widgets/aqi_sub_value_widget.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 38f62cd7..f2b485f7 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 @@ -40,7 +40,7 @@ class AirQualityView extends StatelessWidget { spacing: 32, children: [ Expanded( - flex: 2, + flex: 5, child: Column( spacing: 20, children: [ @@ -49,7 +49,10 @@ class AirQualityView extends StatelessWidget { ], ), ), - Expanded(child: AirQualityEndSideWidget()), + Expanded( + flex: 3, + child: AirQualityEndSideWidget(), + ), ], ), ), diff --git a/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart index 2d6ace36..8c4bc9c1 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.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/widgets/aqi_sub_value_widget.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/helpers/fetch_energy_management_data_helper.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/analytics_device_dropdown.dart'; @@ -39,6 +40,46 @@ class AirQualityEndSideWidget extends StatelessWidget { fontSize: 12, ), ), + const Divider(), + Container( + decoration: secondarySection.copyWith(boxShadow: const []), + padding: const EdgeInsetsDirectional.all(20), + child: const Column( + spacing: 6, + children: [ + AqiSubValueWidget( + label: 'PM2.5', + value: 19, + unit: 'µg/m³', + ), + AqiSubValueWidget( + label: 'PM10', + value: 42, + unit: 'µg/m³', + ), + AqiSubValueWidget( + label: 'CO2', + value: 610, + unit: 'ppm', + ), + AqiSubValueWidget( + label: 'VOC', + value: 1, + unit: 'mg/m³', + ), + AqiSubValueWidget( + label: 'O3', + value: 55, + unit: 'µg/m³', + ), + AqiSubValueWidget( + label: 'NO2', + value: 18, + unit: 'µg/m³', + ), + ], + ), + ), ], ), ); diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_sub_value_widget.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_sub_value_widget.dart new file mode 100644 index 00000000..d9843bca --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_sub_value_widget.dart @@ -0,0 +1,139 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +final class _AqiRange { + const _AqiRange({ + required this.max, + required this.color, + }); + + final int max; + final Color color; +} + +class AqiSubValueWidget extends StatelessWidget { + final String label; + final int value; + final String unit; + + const AqiSubValueWidget({ + super.key, + required this.label, + required this.value, + required this.unit, + }); + + static const List<_AqiRange> _ranges = [ + _AqiRange(max: 12, color: ColorsManager.green), + _AqiRange(max: 35, color: Color(0xFFFFF176)), + _AqiRange(max: 55, color: Color(0xFFFFD54F)), + _AqiRange(max: 150, color: Color(0xFFE57373)), + _AqiRange(max: 250, color: Color(0xFFBA68C8)), + _AqiRange(max: 500, color: Color(0xFFB39DDB)), + ]; + + int _getActiveSegment(int value) { + for (int i = 0; i < _ranges.length; i++) { + if (value <= _ranges[i].max) return i; + } + return _ranges.length - 1; + } + + @override + Widget build(BuildContext context) { + final activeSegment = _getActiveSegment(value); + return Container( + padding: const EdgeInsetsDirectional.all(10), + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + spacing: MediaQuery.sizeOf(context).width * 0.0075, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildLabel(context), + _buildSegmentedBar(activeSegment), + _buildValueAndUnit(context), + ], + ), + ); + } + + Widget _buildValueAndUnit(BuildContext context) { + return Expanded( + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: AlignmentDirectional.centerEnd, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + value.toString(), + style: context.textTheme.titleMedium?.copyWith( + color: ColorsManager.blackColor, + fontWeight: FontWeight.w400, + fontSize: 14, + ), + ), + Text( + unit, + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.blackColor, + fontWeight: FontWeight.w400, + fontSize: 10, + ), + ), + ], + ), + ), + ); + } + + Widget _buildSegmentedBar(int activeSegment) { + return Expanded( + flex: 4, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + spacing: 4, + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate(_ranges.length, (index) { + final isActive = index == activeSegment; + final color = _ranges[index].color.withValues( + alpha: isActive ? 1.0 : 0.25, + ); + return Expanded( + child: Container( + height: 5, + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(4), + ), + ), + ); + }), + ), + ), + ); + } + + Widget _buildLabel(BuildContext context) { + return Expanded( + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: AlignmentDirectional.centerStart, + child: Text( + label, + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.blackColor, + fontWeight: FontWeight.w400, + fontSize: 14, + ), + ), + ), + ); + } +} From 83363b4c50efa294bf06774d5bbebcb4cb46031c Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 27 May 2025 15:15:29 +0300 Subject: [PATCH 062/181] Made `RangeOfAqiChart._lines` colors use `ColorsManager` colors instead of statically defining them in the widget itself using Hex codes. --- .../modules/air_quality/widgets/range_of_aqi_chart.dart | 8 ++++---- lib/utils/color_manager.dart | 4 ++++ 2 files changed, 8 insertions(+), 4 deletions(-) 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 9423c30d..08a036c0 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 @@ -16,8 +16,8 @@ class RangeOfAqiChart extends StatelessWidget { List<(List values, Color color, Color? dotColor)> get _lines => [ ( chartData.map((e) => e.max).toList(), - const Color(0xFF962DFF), - const Color(0xFF5F00BD), + ColorsManager.maxPurple, + ColorsManager.maxPurpleDot, ), ( chartData.map((e) => e.avg).toList(), @@ -26,8 +26,8 @@ class RangeOfAqiChart extends StatelessWidget { ), ( chartData.map((e) => e.min).toList(), - const Color(0xFF93AAFD), - const Color(0xFF023DFE), + ColorsManager.minBlue, + ColorsManager.minBlueDot, ), ]; diff --git a/lib/utils/color_manager.dart b/lib/utils/color_manager.dart index c2e4e60d..41ceb29a 100644 --- a/lib/utils/color_manager.dart +++ b/lib/utils/color_manager.dart @@ -79,4 +79,8 @@ abstract class ColorsManager { static const Color unhealthyRed = Color(0xFFD40000); static const Color severePink = Color(0xFFD40094); static const Color hazardousPurple = Color(0xFFBA01FD); + static const Color maxPurple = Color(0xFF962DFF); + static const Color maxPurpleDot = Color(0xFF5F00BD); + static const Color minBlue = Color(0xFF93AAFD); + static const Color minBlueDot = Color(0xFF023DFE); } From 7726ceecb85a50028f7a816d591505ac5b14c2a3 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 27 May 2025 15:21:45 +0300 Subject: [PATCH 063/181] made `AqiSubValueWidget` use the correct colors. --- .../modules/air_quality/views/air_quality_view.dart | 2 +- .../air_quality/widgets/aqi_sub_value_widget.dart | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) 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 9cf35128..746a5754 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 @@ -51,7 +51,7 @@ class AirQualityView extends StatelessWidget { ), ), Expanded( - flex: 3, + flex: 2, child: AirQualityEndSideWidget(), ), ], diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_sub_value_widget.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_sub_value_widget.dart index d9843bca..c4ed3b15 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_sub_value_widget.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_sub_value_widget.dart @@ -25,12 +25,12 @@ class AqiSubValueWidget extends StatelessWidget { }); static const List<_AqiRange> _ranges = [ - _AqiRange(max: 12, color: ColorsManager.green), - _AqiRange(max: 35, color: Color(0xFFFFF176)), - _AqiRange(max: 55, color: Color(0xFFFFD54F)), - _AqiRange(max: 150, color: Color(0xFFE57373)), - _AqiRange(max: 250, color: Color(0xFFBA68C8)), - _AqiRange(max: 500, color: Color(0xFFB39DDB)), + _AqiRange(max: 12, color: ColorsManager.goodGreen), + _AqiRange(max: 35, color: ColorsManager.poorOrange), + _AqiRange(max: 55, color: ColorsManager.poorOrange), + _AqiRange(max: 150, color: ColorsManager.unhealthyRed), + _AqiRange(max: 250, color: ColorsManager.severePink), + _AqiRange(max: 500, color: ColorsManager.hazardousPurple), ]; int _getActiveSegment(int value) { From 0bf34c66aa1a74c37cf0aef545a93864c6934bf4 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 27 May 2025 15:45:32 +0300 Subject: [PATCH 064/181] Animated `AqiSubValueWidget`. --- .../widgets/aqi_sub_value_widget.dart | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_sub_value_widget.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_sub_value_widget.dart index c4ed3b15..b23a32d8 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_sub_value_widget.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_sub_value_widget.dart @@ -3,27 +3,24 @@ import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; final class _AqiRange { - const _AqiRange({ - required this.max, - required this.color, - }); + const _AqiRange({required this.max, required this.color}); final int max; final Color color; } class AqiSubValueWidget extends StatelessWidget { - final String label; - final int value; - final String unit; - const AqiSubValueWidget({ - super.key, required this.label, required this.value, required this.unit, + super.key, }); + final String label; + final int value; + final String unit; + static const List<_AqiRange> _ranges = [ _AqiRange(max: 12, color: ColorsManager.goodGreen), _AqiRange(max: 35, color: ColorsManager.poorOrange), @@ -106,7 +103,9 @@ class AqiSubValueWidget extends StatelessWidget { alpha: isActive ? 1.0 : 0.25, ); return Expanded( - child: Container( + child: AnimatedContainer( + duration: const Duration(milliseconds: 150), + curve: Curves.linear, height: 5, decoration: BoxDecoration( color: color, From 34279cfdaed80fe1d136a131d227a0e2e29073b3 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 27 May 2025 16:10:49 +0300 Subject: [PATCH 065/181] added `location_pin.svg` icon. --- assets/icons/location_pin.svg | 11 +++++++++++ lib/utils/constants/assets.dart | 1 + 2 files changed, 12 insertions(+) create mode 100644 assets/icons/location_pin.svg diff --git a/assets/icons/location_pin.svg b/assets/icons/location_pin.svg new file mode 100644 index 00000000..e1ae063f --- /dev/null +++ b/assets/icons/location_pin.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/lib/utils/constants/assets.dart b/lib/utils/constants/assets.dart index 13d51ea5..9c9146a4 100644 --- a/lib/utils/constants/assets.dart +++ b/lib/utils/constants/assets.dart @@ -452,4 +452,5 @@ class Assets { 'assets/icons/refresh_status_icon.svg'; static const String energyConsumedIcon = 'assets/icons/energy_consumed_icon.svg'; + static const String locationPin = 'assets/icons/location_pin.svg'; } From fe716baba7957d730ac3f5d3204558aecd473570 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 27 May 2025 16:10:58 +0300 Subject: [PATCH 066/181] created `AqiLocation` widget. --- .../air_quality/widgets/aqi_location.dart | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 lib/pages/analytics/modules/air_quality/widgets/aqi_location.dart diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_location.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_location.dart new file mode 100644 index 00000000..3ad7e495 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_location.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:syncrow_web/utils/style.dart'; + +class AqiLocation extends StatelessWidget { + const AqiLocation({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + decoration: subSectionContainerDecoration.copyWith(boxShadow: const []), + padding: const EdgeInsetsDirectional.all(10), + child: Row( + spacing: 10, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + _buildLocationPin(), + Expanded( + child: Text( + 'Business Bay, Dubai - UAE', + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w400, + fontSize: 12, + ), + ), + ), + ], + ), + ); + } + + Widget _buildLocationPin() { + return SvgPicture.asset( + Assets.locationPin, + height: 12, + width: 12, + alignment: AlignmentDirectional.centerStart, + colorFilter: const ColorFilter.mode( + ColorsManager.vividBlue, + BlendMode.srcIn, + ), + ); + } +} From 1f444ccfcbf7be175d5d02fbfe2dc8cbe31188e5 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 27 May 2025 16:11:13 +0300 Subject: [PATCH 067/181] Created `AqiLocationInfoCell` widget. --- .../widgets/aqi_location_info_cell.dart | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 lib/pages/analytics/modules/air_quality/widgets/aqi_location_info_cell.dart diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info_cell.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info_cell.dart new file mode 100644 index 00000000..b5cc000b --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info_cell.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class AqiLocationInfoCell extends StatelessWidget { + const AqiLocationInfoCell({ + required this.label, + required this.value, + required this.svgPath, + super.key, + }); + + final String label; + final String value; + final String svgPath; + + @override + Widget build(BuildContext context) { + return Expanded( + child: Container( + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.circular(12), + ), + child: Stack( + children: [ + Align( + alignment: AlignmentDirectional.topStart, + child: Padding( + padding: const EdgeInsetsDirectional.all(10), + child: Text( + label, + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w400, + fontSize: 12, + ), + ), + ), + ), + Align( + alignment: AlignmentDirectional.bottomEnd, + child: Padding( + padding: const EdgeInsetsDirectional.all(10).add( + const EdgeInsetsDirectional.only(start: 32), + ), + child: FittedBox( + child: Text( + value, + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.vividBlue.withValues(alpha: 0.7), + fontWeight: FontWeight.w700, + fontSize: 40, + ), + ), + ), + ), + ), + Align( + alignment: AlignmentDirectional.bottomStart, + child: SizedBox.square( + dimension: 75, + child: FittedBox( + child: SvgPicture.asset( + svgPath, + colorFilter: ColorFilter.mode( + ColorsManager.textPrimaryColor.withValues(alpha: 0.2), + BlendMode.srcIn, + ), + fit: BoxFit.scaleDown, + ), + ), + ), + ), + ], + ), + ), + ); + } +} From 077c6e99d6e63b8205882f4d6f2bc476784c67e4 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 27 May 2025 16:55:17 +0300 Subject: [PATCH 068/181] added aqi informative icons. --- assets/icons/aqi_air_quality.svg | 5 +++++ assets/icons/aqi_humidity.svg | 7 +++++++ assets/icons/aqi_temperature.svg | 8 ++++++++ lib/utils/constants/assets.dart | 3 +++ 4 files changed, 23 insertions(+) create mode 100644 assets/icons/aqi_air_quality.svg create mode 100644 assets/icons/aqi_humidity.svg create mode 100644 assets/icons/aqi_temperature.svg diff --git a/assets/icons/aqi_air_quality.svg b/assets/icons/aqi_air_quality.svg new file mode 100644 index 00000000..cd2ed556 --- /dev/null +++ b/assets/icons/aqi_air_quality.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/aqi_humidity.svg b/assets/icons/aqi_humidity.svg new file mode 100644 index 00000000..dd8a1d7e --- /dev/null +++ b/assets/icons/aqi_humidity.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/aqi_temperature.svg b/assets/icons/aqi_temperature.svg new file mode 100644 index 00000000..09ab6d77 --- /dev/null +++ b/assets/icons/aqi_temperature.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/lib/utils/constants/assets.dart b/lib/utils/constants/assets.dart index 9c9146a4..64ecd0a9 100644 --- a/lib/utils/constants/assets.dart +++ b/lib/utils/constants/assets.dart @@ -453,4 +453,7 @@ class Assets { static const String energyConsumedIcon = 'assets/icons/energy_consumed_icon.svg'; static const String locationPin = 'assets/icons/location_pin.svg'; + static const String aqiTemperature = 'assets/icons/aqi_temperature.svg'; + static const String aqiHumidity = 'assets/icons/aqi_humidity.svg'; + static const String aqiAirQuality = 'assets/icons/aqi_air_quality.svg'; } From aded80fb9ab116dc78218d44318c0c09ee2f3e44 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 27 May 2025 16:55:43 +0300 Subject: [PATCH 069/181] modified sizing of `AirQualityView`. --- .../analytics/modules/air_quality/views/air_quality_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 746a5754..85028636 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 @@ -33,7 +33,7 @@ class AirQualityView extends StatelessWidget { return SingleChildScrollView( child: Container( padding: _padding, - height: height, + height: height * 0.9, child: const Column( children: [ Expanded( From 42319cc4f972808646e1fe5b5618deb98084b963 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 27 May 2025 16:56:01 +0300 Subject: [PATCH 070/181] added `unit` property to `AqiType`. --- .../air_quality/widgets/aqi_type_dropdown.dart | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) 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 ea85f075..196f02ce 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,16 +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'), - pm10('PM10'), - hcho('HCHO'), - tvoc('TVOC'), - co2('CO2'), - c6h6('C6H6'); + aqi('AQI', ''), + pm25('PM2.5', 'µg/m³'), + pm10('PM10', 'µg/m³'), + hcho('HCHO', 'mg/m³'), + tvoc('TVOC', 'µg/m³'), + co2('CO2', 'ppm'), + c6h6('C6H6', 'µg/m³'); + + const AqiType(this.value, this.unit); final String value; - const AqiType(this.value); + final String unit; } class AqiTypeDropdown extends StatefulWidget { From fc330d6e172678f5724bef3b3e283480257dfca4 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 28 May 2025 09:32:58 +0300 Subject: [PATCH 071/181] Making good progress towards finalizing the end side bar. --- .../air_quality/views/air_quality_view.dart | 2 +- .../air_quality_end_side_gauge_and_info.dart | 77 +++++++++++ .../air_quality_end_side_live_indicator.dart | 40 ++++++ .../widgets/air_quality_end_side_widget.dart | 128 +++++++++++++----- .../widgets/aqi_location_info_cell.dart | 40 +++--- .../widgets/aqi_sub_value_widget.dart | 30 ++-- 6 files changed, 248 insertions(+), 69 deletions(-) create mode 100644 lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_gauge_and_info.dart create mode 100644 lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_live_indicator.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 85028636..b4ae5f1b 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 @@ -33,7 +33,7 @@ class AirQualityView extends StatelessWidget { return SingleChildScrollView( child: Container( padding: _padding, - height: height * 0.9, + height: height * 1.1, child: const Column( children: [ Expanded( diff --git a/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_gauge_and_info.dart b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_gauge_and_info.dart new file mode 100644 index 00000000..086eda9e --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_gauge_and_info.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class AirQualityEndSideGaugeAndInfo extends StatelessWidget { + const AirQualityEndSideGaugeAndInfo({super.key}); + + @override + Widget build(BuildContext context) { + return Expanded( + flex: 3, + child: Row( + children: [ + const Expanded(flex: 2, child: Placeholder()), + const Spacer(), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Air Quality:', + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w400, + fontSize: 14, + ), + ), + Text( + 'Perfect', + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.green, + fontWeight: FontWeight.w400, + fontSize: 30, + ), + ), + DefaultTextStyle( + style: context.textTheme.bodySmall!.copyWith( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w400, + fontSize: 12, + ), + child: Row( + children: [ + SvgPicture.asset( + Assets.aqiTemperature, + height: 12, + width: 12, + colorFilter: const ColorFilter.mode( + ColorsManager.goodGreen, + BlendMode.srcIn, + ), + ), + const SizedBox(width: 4), + const Text('30C'), + const SizedBox(width: 10), + SvgPicture.asset( + Assets.aqiHumidity, + height: 12, + width: 12, + colorFilter: const ColorFilter.mode( + ColorsManager.textPrimaryColor, + BlendMode.srcIn, + ), + ), + const SizedBox(width: 4), + const Text('30%'), + ], + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_live_indicator.dart b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_live_indicator.dart new file mode 100644 index 00000000..da8dd86a --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_live_indicator.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class AirQualityEndSideLiveIndicator extends StatelessWidget { + const AirQualityEndSideLiveIndicator({super.key}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Entrance', + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w400, + fontSize: 14, + ), + ), + const Spacer(), + CircleAvatar( + backgroundColor: ColorsManager.green.withValues( + alpha: 0.5, + ), + radius: 2, + ), + const SizedBox(width: 4), + Text( + 'Live', + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.green.withValues(alpha: 0.5), + fontWeight: FontWeight.w400, + fontSize: 8, + ), + ), + ], + ); + } +} diff --git a/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart index 0a91fc49..03044e8d 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart @@ -1,10 +1,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/air_quality_end_side_gauge_and_info.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/air_quality_end_side_live_indicator.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_location.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_location_info_cell.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_sub_value_widget.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_devices/analytics_devices_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/fetch_energy_management_data_helper.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/analytics_device_dropdown.dart'; import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/style.dart'; @@ -41,43 +47,93 @@ class AirQualityEndSideWidget extends StatelessWidget { ), ), const Divider(), - Container( - decoration: secondarySection.copyWith(boxShadow: const []), - padding: const EdgeInsetsDirectional.all(20), - child: const Column( - spacing: 6, - children: [ - AqiSubValueWidget( - label: 'PM2.5', - value: 19, - unit: 'µg/m³', + Expanded( + flex: 15, + child: Container( + decoration: secondarySection.copyWith(boxShadow: const []), + padding: const EdgeInsetsDirectional.all(20), + child: Expanded( + child: Column( + spacing: 6, + children: [ + const AirQualityEndSideLiveIndicator(), + const AirQualityEndSideGaugeAndInfo(), + const SizedBox(height: 20), + // The spaces added to the labels are for alignment purposes, because FittedBox is used to align the text. + AqiSubValueWidget( + label: AqiType.pm25.value, + value: 19, + unit: AqiType.pm25.unit, + ), + AqiSubValueWidget( + label: AqiType.pm10.value, + value: 42, + unit: AqiType.pm10.unit, + ), + AqiSubValueWidget( + label: '${AqiType.co2.value} ', + value: 610, + unit: AqiType.co2.unit, + ), + AqiSubValueWidget( + label: AqiType.hcho.value, + value: 1, + unit: AqiType.hcho.unit, + ), + AqiSubValueWidget( + label: AqiType.tvoc.value, + value: 55, + unit: AqiType.tvoc.unit, + ), + AqiSubValueWidget( + label: AqiType.co2.value, + value: 18, + unit: AqiType.co2.unit, + ), + AqiSubValueWidget( + label: AqiType.c6h6.value, + value: 18, + unit: AqiType.c6h6.unit, + ), + ], ), - AqiSubValueWidget( - label: 'PM10', - value: 42, - unit: 'µg/m³', - ), - AqiSubValueWidget( - label: 'CO2', - value: 610, - unit: 'ppm', - ), - AqiSubValueWidget( - label: 'VOC', - value: 1, - unit: 'mg/m³', - ), - AqiSubValueWidget( - label: 'O3', - value: 55, - unit: 'µg/m³', - ), - AqiSubValueWidget( - label: 'NO2', - value: 18, - unit: 'µg/m³', - ), - ], + ), + ), + ), + const SizedBox(height: 20), + Expanded( + flex: 6, + child: Container( + decoration: secondarySection.copyWith(boxShadow: const []), + padding: const EdgeInsetsDirectional.all(20), + child: const Column( + spacing: 8, + children: [ + AqiLocation(), + Expanded( + child: Row( + spacing: 8, + children: [ + AqiLocationInfoCell( + label: 'Temperature', + value: '25°', + svgPath: Assets.aqiTemperature, + ), + AqiLocationInfoCell( + label: 'Humidity', + value: '25%', + svgPath: Assets.aqiHumidity, + ), + AqiLocationInfoCell( + label: 'Air Quality', + value: '120', + svgPath: Assets.aqiAirQuality, + ), + ], + ), + ), + ], + ), ), ), ], diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info_cell.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info_cell.dart index b5cc000b..07c27f04 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info_cell.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info_cell.dart @@ -17,6 +17,7 @@ class AqiLocationInfoCell extends StatelessWidget { @override Widget build(BuildContext context) { + final isMediumOrLess = MediaQuery.sizeOf(context).width <= 900; return Expanded( child: Container( decoration: BoxDecoration( @@ -27,14 +28,18 @@ class AqiLocationInfoCell extends StatelessWidget { children: [ Align( alignment: AlignmentDirectional.topStart, - child: Padding( - padding: const EdgeInsetsDirectional.all(10), - child: Text( - label, - style: context.textTheme.bodySmall?.copyWith( - color: ColorsManager.textPrimaryColor, - fontWeight: FontWeight.w400, - fontSize: 12, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: AlignmentDirectional.topStart, + child: Padding( + padding: const EdgeInsetsDirectional.all(10), + child: Text( + label, + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w400, + fontSize: 12, + ), ), ), ), @@ -46,12 +51,14 @@ class AqiLocationInfoCell extends StatelessWidget { const EdgeInsetsDirectional.only(start: 32), ), child: FittedBox( + fit: BoxFit.scaleDown, + alignment: AlignmentDirectional.bottomEnd, child: Text( value, style: context.textTheme.bodySmall?.copyWith( color: ColorsManager.vividBlue.withValues(alpha: 0.7), fontWeight: FontWeight.w700, - fontSize: 40, + fontSize: 24, ), ), ), @@ -60,16 +67,13 @@ class AqiLocationInfoCell extends StatelessWidget { Align( alignment: AlignmentDirectional.bottomStart, child: SizedBox.square( - dimension: 75, + dimension: isMediumOrLess + ? MediaQuery.sizeOf(context).width * 0.15 + : MediaQuery.sizeOf(context).width * 0.035, child: FittedBox( - child: SvgPicture.asset( - svgPath, - colorFilter: ColorFilter.mode( - ColorsManager.textPrimaryColor.withValues(alpha: 0.2), - BlendMode.srcIn, - ), - fit: BoxFit.scaleDown, - ), + fit: BoxFit.scaleDown, + alignment: AlignmentDirectional.bottomStart, + child: SvgPicture.asset(svgPath), ), ), ), diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_sub_value_widget.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_sub_value_widget.dart index b23a32d8..6596019a 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_sub_value_widget.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_sub_value_widget.dart @@ -40,20 +40,22 @@ class AqiSubValueWidget extends StatelessWidget { @override Widget build(BuildContext context) { final activeSegment = _getActiveSegment(value); - return Container( - padding: const EdgeInsetsDirectional.all(10), - decoration: BoxDecoration( - color: ColorsManager.whiteColors, - borderRadius: BorderRadius.circular(12), - ), - child: Row( - spacing: MediaQuery.sizeOf(context).width * 0.0075, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - _buildLabel(context), - _buildSegmentedBar(activeSegment), - _buildValueAndUnit(context), - ], + return Expanded( + child: Container( + padding: const EdgeInsetsDirectional.all(10), + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + spacing: MediaQuery.sizeOf(context).width * 0.0075, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildLabel(context), + _buildSegmentedBar(activeSegment), + _buildValueAndUnit(context), + ], + ), ), ); } From 595966d30631163cd02e2417b24d14f1d5104fa0 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 28 May 2025 14:22:35 +0300 Subject: [PATCH 072/181] implemented gauge. --- .../air_quality/views/air_quality_view.dart | 2 +- .../air_quality_end_side_gauge_and_info.dart | 267 ++++++++++++++---- pubspec.yaml | 1 + 3 files changed, 218 insertions(+), 52 deletions(-) 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 b4ae5f1b..c24922cf 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 @@ -51,7 +51,7 @@ class AirQualityView extends StatelessWidget { ), ), Expanded( - flex: 2, + flex: 3, child: AirQualityEndSideWidget(), ), ], diff --git a/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_gauge_and_info.dart b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_gauge_and_info.dart index 086eda9e..8e01a99a 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_gauge_and_info.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_gauge_and_info.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:gauge_indicator/gauge_indicator.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'; @@ -10,65 +11,229 @@ class AirQualityEndSideGaugeAndInfo extends StatelessWidget { @override Widget build(BuildContext context) { return Expanded( - flex: 3, + flex: 2, child: Row( children: [ - const Expanded(flex: 2, child: Placeholder()), + const Expanded( + flex: 3, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Expanded(flex: 1, child: AqiGauge(aqi: 200)), + ], + ), + ), const Spacer(), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Air Quality:', - style: context.textTheme.bodySmall?.copyWith( - color: ColorsManager.textPrimaryColor, - fontWeight: FontWeight.w400, - fontSize: 14, - ), - ), - Text( - 'Perfect', - style: context.textTheme.bodySmall?.copyWith( - color: ColorsManager.green, - fontWeight: FontWeight.w400, - fontSize: 30, - ), - ), - DefaultTextStyle( - style: context.textTheme.bodySmall!.copyWith( - color: ColorsManager.textPrimaryColor, - fontWeight: FontWeight.w400, - fontSize: 12, - ), - child: Row( - children: [ - SvgPicture.asset( - Assets.aqiTemperature, - height: 12, - width: 12, - colorFilter: const ColorFilter.mode( - ColorsManager.goodGreen, - BlendMode.srcIn, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 2, + child: FittedBox( + alignment: AlignmentDirectional.centerStart, + child: Text( + 'Air Quality:', + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w400, + fontSize: 14, ), ), - const SizedBox(width: 4), - const Text('30C'), - const SizedBox(width: 10), - SvgPicture.asset( - Assets.aqiHumidity, - height: 12, - width: 12, - colorFilter: const ColorFilter.mode( - ColorsManager.textPrimaryColor, - BlendMode.srcIn, + ), + ), + Expanded( + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: AlignmentDirectional.centerStart, + child: Text( + 'Perfect', + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.green, + fontWeight: FontWeight.w400, + fontSize: 30, ), ), - const SizedBox(width: 4), - const Text('30%'), - ], + ), ), - ), - ], + Expanded( + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: AlignmentDirectional.centerStart, + child: DefaultTextStyle( + style: context.textTheme.bodySmall!.copyWith( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w400, + fontSize: 12, + ), + child: Row( + children: [ + SvgPicture.asset( + Assets.aqiTemperature, + height: 12, + width: 12, + colorFilter: const ColorFilter.mode( + ColorsManager.goodGreen, + BlendMode.srcIn, + ), + ), + const SizedBox(width: 4), + const Text('30C'), + const SizedBox(width: 10), + SvgPicture.asset( + Assets.aqiHumidity, + height: 12, + width: 12, + colorFilter: const ColorFilter.mode( + ColorsManager.textPrimaryColor, + BlendMode.srcIn, + ), + ), + const SizedBox(width: 4), + const Text('30%'), + ], + ), + ), + ), + ), + ], + ), + ), + ], + ), + ); + } +} + +class AqiGauge extends StatelessWidget { + const AqiGauge({super.key, required this.aqi}); + + final double aqi; + + Color _getPointerColor(double value) { + if (value <= 50) { + return ColorsManager.goodGreen; + } else if (value <= 100) { + return ColorsManager.moderateYellow; + } else if (value <= 150) { + return ColorsManager.poorOrange; + } else { + if (value <= 225) { + final t = (value - 151) / (225 - 151); + return Color.lerp( + ColorsManager.unhealthyRed, + ColorsManager.severePink, + t, + )!; + } else { + final t = (value - 226) / (300 - 226); + return Color.lerp( + ColorsManager.severePink, + ColorsManager.hazardousPurple, + t, + )!; + } + } + } + + @override + Widget build(BuildContext context) { + return AnimatedRadialGauge( + value: aqi, + debug: false, + duration: const Duration(milliseconds: 200), + initialValue: 0, + alignment: Alignment.bottomCenter, + builder: (context, child, value) { + return Align( + alignment: AlignmentDirectional.bottomCenter, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: AlignmentDirectional.bottomCenter, + child: Text.rich( + TextSpan( + text: '${value.toInt()}', + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w700, + fontSize: 30, + ), + children: [ + const TextSpan( + text: 'AQI', + style: TextStyle( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w400, + fontSize: 12, + ), + ), + ], + ), + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + ), + ), + ); + }, + axis: GaugeAxis( + progressBar: const GaugeProgressBar.basic(color: Colors.transparent), + style: const GaugeAxisStyle( + cornerRadius: Radius.circular(16), + thickness: 10, + segmentSpacing: 4, + ), + min: 0, + max: 300, + pointer: GaugePointer.circle( + position: const GaugePointerPosition.surface(), + shadow: const BoxShadow( + color: ColorsManager.transparentColor, + blurRadius: 0, + offset: Offset(0, 0), + ), + radius: MediaQuery.sizeOf(context).width * 0.004, + color: ColorsManager.whiteColors, + border: GaugePointerBorder( + width: 4, + color: _getPointerColor(aqi), + ), + ), + transformer: const GaugeAxisTransformer.colorFadeIn( + background: Colors.transparent, + interval: Interval(0, 0), + ), + segments: [ + const GaugeSegment( + from: 0, + to: 50, + cornerRadius: Radius.circular(16), + color: ColorsManager.goodGreen, + ), + const GaugeSegment( + from: 51, + to: 100, + cornerRadius: Radius.circular(16), + color: ColorsManager.moderateYellow, + ), + const GaugeSegment( + from: 101, + to: 150, + cornerRadius: Radius.circular(16), + color: ColorsManager.poorOrange, + ), + const GaugeSegment( + from: 151, + to: 300, + cornerRadius: Radius.circular(16), + gradient: GaugeAxisGradient( + colorStops: [0.0, 0.5, 1.0], + colors: [ + ColorsManager.unhealthyRed, + ColorsManager.severePink, + ColorsManager.hazardousPurple, + ], + ), ), ], ), diff --git a/pubspec.yaml b/pubspec.yaml index 7decc506..2d09f0bb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -61,6 +61,7 @@ dependencies: firebase_crashlytics: ^4.3.2 firebase_database: ^11.3.2 bloc: ^9.0.0 + gauge_indicator: ^0.4.3 dev_dependencies: From 25db6ec687b7f8a7b37370a646d9897361ac7c1a Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 28 May 2025 14:24:03 +0300 Subject: [PATCH 073/181] Created `pull_request_template.md` . --- .github/pull_request_template.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..d18a89f3 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,30 @@ + + +## Jira Ticket + + +## Status + +**READY/IN DEVELOPMENT/HOLD** + +## Description + + + +## Type of Change + + + +- [ ] ✨ New feature (non-breaking change which adds functionality) +- [ ] 🛠️ Bug fix (non-breaking change which fixes an issue) +- [ ] ❌ Breaking change (fix or feature that would cause existing functionality to change) +- [ ] 🧹 Code refactor +- [ ] ✅ Build configuration change +- [ ] 📝 Documentation +- [ ] 🗑️ Chore \ No newline at end of file From a23370471ce9328c7892a7d6f0e400e36a7ad530 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 28 May 2025 14:39:41 +0300 Subject: [PATCH 074/181] improved sizing of `AqiLocationInfoCell`. --- .../modules/air_quality/widgets/aqi_location_info_cell.dart | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info_cell.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info_cell.dart index 07c27f04..e6ee71d7 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info_cell.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info_cell.dart @@ -17,7 +17,6 @@ class AqiLocationInfoCell extends StatelessWidget { @override Widget build(BuildContext context) { - final isMediumOrLess = MediaQuery.sizeOf(context).width <= 900; return Expanded( child: Container( decoration: BoxDecoration( @@ -67,9 +66,7 @@ class AqiLocationInfoCell extends StatelessWidget { Align( alignment: AlignmentDirectional.bottomStart, child: SizedBox.square( - dimension: isMediumOrLess - ? MediaQuery.sizeOf(context).width * 0.15 - : MediaQuery.sizeOf(context).width * 0.035, + dimension: MediaQuery.sizeOf(context).width * 0.45, child: FittedBox( fit: BoxFit.scaleDown, alignment: AlignmentDirectional.bottomStart, From 9a41e0c4f5875cd17e372304bb3c77ebdd751dc9 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 28 May 2025 14:50:46 +0300 Subject: [PATCH 075/181] moved `ApiGauge` to its own file. --- .../air_quality_end_side_gauge_and_info.dart | 166 ++---------------- .../widgets/air_quality_end_side_widget.dart | 2 +- .../air_quality/widgets/aqi_gauge.dart | 139 +++++++++++++++ 3 files changed, 155 insertions(+), 152 deletions(-) create mode 100644 lib/pages/analytics/modules/air_quality/widgets/aqi_gauge.dart diff --git a/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_gauge_and_info.dart b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_gauge_and_info.dart index 8e01a99a..8693a6d2 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_gauge_and_info.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_gauge_and_info.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:gauge_indicator/gauge_indicator.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_gauge.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'; @@ -26,12 +26,14 @@ class AirQualityEndSideGaugeAndInfo extends StatelessWidget { ), const Spacer(), Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - flex: 2, - child: FittedBox( + child: Padding( + padding: const EdgeInsetsDirectional.only(end: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Spacer(), + FittedBox( + fit: BoxFit.contain, alignment: AlignmentDirectional.centerStart, child: Text( 'Air Quality:', @@ -42,10 +44,8 @@ class AirQualityEndSideGaugeAndInfo extends StatelessWidget { ), ), ), - ), - Expanded( - child: FittedBox( - fit: BoxFit.scaleDown, + FittedBox( + fit: BoxFit.contain, alignment: AlignmentDirectional.centerStart, child: Text( 'Perfect', @@ -56,9 +56,8 @@ class AirQualityEndSideGaugeAndInfo extends StatelessWidget { ), ), ), - ), - Expanded( - child: FittedBox( + const Spacer(), + FittedBox( fit: BoxFit.scaleDown, alignment: AlignmentDirectional.centerStart, child: DefaultTextStyle( @@ -79,7 +78,7 @@ class AirQualityEndSideGaugeAndInfo extends StatelessWidget { ), ), const SizedBox(width: 4), - const Text('30C'), + const Text('30°C'), const SizedBox(width: 10), SvgPicture.asset( Assets.aqiHumidity, @@ -91,148 +90,13 @@ class AirQualityEndSideGaugeAndInfo extends StatelessWidget { ), ), const SizedBox(width: 4), - const Text('30%'), + const Text('30°C'), ], ), ), ), - ), - ], - ), - ), - ], - ), - ); - } -} - -class AqiGauge extends StatelessWidget { - const AqiGauge({super.key, required this.aqi}); - - final double aqi; - - Color _getPointerColor(double value) { - if (value <= 50) { - return ColorsManager.goodGreen; - } else if (value <= 100) { - return ColorsManager.moderateYellow; - } else if (value <= 150) { - return ColorsManager.poorOrange; - } else { - if (value <= 225) { - final t = (value - 151) / (225 - 151); - return Color.lerp( - ColorsManager.unhealthyRed, - ColorsManager.severePink, - t, - )!; - } else { - final t = (value - 226) / (300 - 226); - return Color.lerp( - ColorsManager.severePink, - ColorsManager.hazardousPurple, - t, - )!; - } - } - } - - @override - Widget build(BuildContext context) { - return AnimatedRadialGauge( - value: aqi, - debug: false, - duration: const Duration(milliseconds: 200), - initialValue: 0, - alignment: Alignment.bottomCenter, - builder: (context, child, value) { - return Align( - alignment: AlignmentDirectional.bottomCenter, - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: AlignmentDirectional.bottomCenter, - child: Text.rich( - TextSpan( - text: '${value.toInt()}', - style: context.textTheme.bodySmall?.copyWith( - color: ColorsManager.textPrimaryColor, - fontWeight: FontWeight.w700, - fontSize: 30, - ), - children: [ - const TextSpan( - text: 'AQI', - style: TextStyle( - color: ColorsManager.textPrimaryColor, - fontWeight: FontWeight.w400, - fontSize: 12, - ), - ), ], ), - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - ), - ), - ); - }, - axis: GaugeAxis( - progressBar: const GaugeProgressBar.basic(color: Colors.transparent), - style: const GaugeAxisStyle( - cornerRadius: Radius.circular(16), - thickness: 10, - segmentSpacing: 4, - ), - min: 0, - max: 300, - pointer: GaugePointer.circle( - position: const GaugePointerPosition.surface(), - shadow: const BoxShadow( - color: ColorsManager.transparentColor, - blurRadius: 0, - offset: Offset(0, 0), - ), - radius: MediaQuery.sizeOf(context).width * 0.004, - color: ColorsManager.whiteColors, - border: GaugePointerBorder( - width: 4, - color: _getPointerColor(aqi), - ), - ), - transformer: const GaugeAxisTransformer.colorFadeIn( - background: Colors.transparent, - interval: Interval(0, 0), - ), - segments: [ - const GaugeSegment( - from: 0, - to: 50, - cornerRadius: Radius.circular(16), - color: ColorsManager.goodGreen, - ), - const GaugeSegment( - from: 51, - to: 100, - cornerRadius: Radius.circular(16), - color: ColorsManager.moderateYellow, - ), - const GaugeSegment( - from: 101, - to: 150, - cornerRadius: Radius.circular(16), - color: ColorsManager.poorOrange, - ), - const GaugeSegment( - from: 151, - to: 300, - cornerRadius: Radius.circular(16), - gradient: GaugeAxisGradient( - colorStops: [0.0, 0.5, 1.0], - colors: [ - ColorsManager.unhealthyRed, - ColorsManager.severePink, - ColorsManager.hazardousPurple, - ], ), ), ], diff --git a/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart index 03044e8d..e636d97b 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart @@ -102,7 +102,7 @@ class AirQualityEndSideWidget extends StatelessWidget { ), const SizedBox(height: 20), Expanded( - flex: 6, + flex: 4, child: Container( decoration: secondarySection.copyWith(boxShadow: const []), padding: const EdgeInsetsDirectional.all(20), diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_gauge.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_gauge.dart new file mode 100644 index 00000000..d36f3e00 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_gauge.dart @@ -0,0 +1,139 @@ +import 'package:flutter/material.dart'; +import 'package:gauge_indicator/gauge_indicator.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class AqiGauge extends StatelessWidget { + const AqiGauge({super.key, required this.aqi}); + + final double aqi; + + Color _getPointerColor(double value) { + if (value <= 50) { + return ColorsManager.goodGreen; + } else if (value <= 100) { + return ColorsManager.moderateYellow; + } else if (value <= 150) { + return ColorsManager.poorOrange; + } else { + if (value <= 225) { + final t = (value - 151) / (225 - 151); + return Color.lerp( + ColorsManager.unhealthyRed, + ColorsManager.severePink, + t, + )!; + } else { + final t = (value - 226) / (300 - 226); + return Color.lerp( + ColorsManager.severePink, + ColorsManager.hazardousPurple, + t, + )!; + } + } + } + + @override + Widget build(BuildContext context) { + return AnimatedRadialGauge( + value: aqi, + debug: false, + duration: const Duration(milliseconds: 200), + initialValue: 0, + alignment: Alignment.bottomCenter, + builder: (context, child, value) { + return Align( + alignment: AlignmentDirectional.bottomCenter, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: AlignmentDirectional.bottomCenter, + child: Text.rich( + TextSpan( + text: '${value.toInt()}', + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w700, + fontSize: 30, + ), + children: [ + const TextSpan( + text: 'AQI', + style: TextStyle( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w400, + fontSize: 12, + ), + ), + ], + ), + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + ), + ), + ); + }, + axis: GaugeAxis( + progressBar: const GaugeProgressBar.basic(color: Colors.transparent), + style: const GaugeAxisStyle( + cornerRadius: Radius.circular(16), + thickness: 10, + segmentSpacing: 4, + ), + min: 0, + max: 300, + pointer: GaugePointer.circle( + position: const GaugePointerPosition.surface(), + shadow: const BoxShadow( + color: ColorsManager.transparentColor, + blurRadius: 0, + offset: Offset(0, 0), + ), + radius: MediaQuery.sizeOf(context).width * 0.004, + color: ColorsManager.whiteColors, + border: GaugePointerBorder( + width: 4, + color: _getPointerColor(aqi), + ), + ), + transformer: const GaugeAxisTransformer.colorFadeIn( + background: Colors.transparent, + interval: Interval(0, 0), + ), + segments: [ + const GaugeSegment( + from: 0, + to: 50, + cornerRadius: Radius.circular(16), + color: ColorsManager.goodGreen, + ), + const GaugeSegment( + from: 51, + to: 100, + cornerRadius: Radius.circular(16), + color: ColorsManager.moderateYellow, + ), + const GaugeSegment( + from: 101, + to: 150, + cornerRadius: Radius.circular(16), + color: ColorsManager.poorOrange, + ), + const GaugeSegment( + from: 151, + to: 300, + cornerRadius: Radius.circular(16), + gradient: GaugeAxisGradient( + colorStops: [0.0, 0.5, 1.0], + colors: [ + ColorsManager.unhealthyRed, + ColorsManager.severePink, + ColorsManager.hazardousPurple, + ], + ), + ), + ], + ), + ); + } +} From 16dc066440a466ff9fbecec51f98aeb201f32fe6 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 28 May 2025 14:56:14 +0300 Subject: [PATCH 076/181] removed unnecessary comment. --- .../air_quality/widgets/air_quality_end_side_widget.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart index e636d97b..23c81350 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart @@ -59,7 +59,6 @@ class AirQualityEndSideWidget extends StatelessWidget { const AirQualityEndSideLiveIndicator(), const AirQualityEndSideGaugeAndInfo(), const SizedBox(height: 20), - // The spaces added to the labels are for alignment purposes, because FittedBox is used to align the text. AqiSubValueWidget( label: AqiType.pm25.value, value: 19, @@ -71,7 +70,7 @@ class AirQualityEndSideWidget extends StatelessWidget { unit: AqiType.pm10.unit, ), AqiSubValueWidget( - label: '${AqiType.co2.value} ', + label: AqiType.co2.value, value: 610, unit: AqiType.co2.unit, ), From 7c69c7ddbd74ea8e6ab22c8c1735be10be08e1b6 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 28 May 2025 15:19:26 +0300 Subject: [PATCH 077/181] fixed responsiveness of end side bar. --- .../air_quality/widgets/air_quality_end_side_widget.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart index 23c81350..1c56b6d1 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart @@ -145,9 +145,9 @@ class AirQualityEndSideWidget extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ Expanded( - flex: 3, child: FittedBox( alignment: AlignmentDirectional.centerStart, + fit: BoxFit.scaleDown, child: SelectableText( 'AQI Sensor', style: context.textTheme.headlineSmall?.copyWith( @@ -160,9 +160,10 @@ class AirQualityEndSideWidget extends StatelessWidget { ), const Spacer(), Expanded( - flex: 4, + flex: 2, child: FittedBox( alignment: AlignmentDirectional.centerEnd, + fit: BoxFit.scaleDown, child: AnalyticsDeviceDropdown( onChanged: (value) { context.read().add( From a87b11d084435840e62a283bea5a1412546d6f29 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 28 May 2025 15:25:17 +0300 Subject: [PATCH 078/181] adjusted the size of `AqiGauge` and removed unnecessary code. --- .../widgets/air_quality_end_side_gauge_and_info.dart | 2 +- .../analytics/modules/air_quality/widgets/aqi_gauge.dart | 9 ++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_gauge_and_info.dart b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_gauge_and_info.dart index 8693a6d2..3f6006c3 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_gauge_and_info.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_gauge_and_info.dart @@ -15,7 +15,7 @@ class AirQualityEndSideGaugeAndInfo extends StatelessWidget { child: Row( children: [ const Expanded( - flex: 3, + flex: 2, child: Row( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start, diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_gauge.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_gauge.dart index d36f3e00..5d284afb 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_gauge.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_gauge.dart @@ -77,22 +77,17 @@ class AqiGauge extends StatelessWidget { progressBar: const GaugeProgressBar.basic(color: Colors.transparent), style: const GaugeAxisStyle( cornerRadius: Radius.circular(16), - thickness: 10, + thickness: 14, segmentSpacing: 4, ), min: 0, max: 300, pointer: GaugePointer.circle( position: const GaugePointerPosition.surface(), - shadow: const BoxShadow( - color: ColorsManager.transparentColor, - blurRadius: 0, - offset: Offset(0, 0), - ), radius: MediaQuery.sizeOf(context).width * 0.004, color: ColorsManager.whiteColors, border: GaugePointerBorder( - width: 4, + width: 6, color: _getPointerColor(aqi), ), ), From 5d3ef95cb7d13a74d78d914f5a02ca6c2ad55e92 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 28 May 2025 15:30:12 +0300 Subject: [PATCH 079/181] Refactor `AqiGauge` to use constants for range values, to allow for ease of change, and readability. --- .../air_quality/widgets/aqi_gauge.dart | 39 ++++++++++++------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_gauge.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_gauge.dart index 5d284afb..a287e8cd 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_gauge.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_gauge.dart @@ -7,24 +7,33 @@ class AqiGauge extends StatelessWidget { const AqiGauge({super.key, required this.aqi}); final double aqi; + static const _minRange = 0.0; + static const _goodRange = 50.0; + static const _moderateRange = 100.0; + static const _poorRange = 150.0; + static const _unhealthyToSevereRange = 225.0; + static const _maxRange = 300.0; Color _getPointerColor(double value) { - if (value <= 50) { + if (value <= _goodRange) { return ColorsManager.goodGreen; - } else if (value <= 100) { + } else if (value <= _moderateRange) { return ColorsManager.moderateYellow; - } else if (value <= 150) { + } else if (value <= _poorRange) { return ColorsManager.poorOrange; } else { - if (value <= 225) { - final t = (value - 151) / (225 - 151); + const unhealthyStart = _poorRange + 1; + if (value <= _unhealthyToSevereRange) { + final t = + (value - unhealthyStart) / (_unhealthyToSevereRange - unhealthyStart); return Color.lerp( ColorsManager.unhealthyRed, ColorsManager.severePink, t, )!; } else { - final t = (value - 226) / (300 - 226); + const severeStart = _unhealthyToSevereRange + 1; + final t = (value - severeStart) / (_maxRange - severeStart); return Color.lerp( ColorsManager.severePink, ColorsManager.hazardousPurple, @@ -81,7 +90,7 @@ class AqiGauge extends StatelessWidget { segmentSpacing: 4, ), min: 0, - max: 300, + max: _maxRange, pointer: GaugePointer.circle( position: const GaugePointerPosition.surface(), radius: MediaQuery.sizeOf(context).width * 0.004, @@ -97,26 +106,26 @@ class AqiGauge extends StatelessWidget { ), segments: [ const GaugeSegment( - from: 0, - to: 50, + from: _minRange, + to: _goodRange, cornerRadius: Radius.circular(16), color: ColorsManager.goodGreen, ), const GaugeSegment( - from: 51, - to: 100, + from: _goodRange + 1, + to: _moderateRange, cornerRadius: Radius.circular(16), color: ColorsManager.moderateYellow, ), const GaugeSegment( - from: 101, - to: 150, + from: _moderateRange + 1, + to: _poorRange, cornerRadius: Radius.circular(16), color: ColorsManager.poorOrange, ), const GaugeSegment( - from: 151, - to: 300, + from: _poorRange + 1, + to: _maxRange, cornerRadius: Radius.circular(16), gradient: GaugeAxisGradient( colorStops: [0.0, 0.5, 1.0], From 5b91ceb6394e9dda1ad27ce47116d90e363b5174 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 28 May 2025 15:33:28 +0300 Subject: [PATCH 080/181] enhanced animation of `AqiGague` --- .../analytics/modules/air_quality/widgets/aqi_gauge.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_gauge.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_gauge.dart index a287e8cd..70cd3be5 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_gauge.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_gauge.dart @@ -48,7 +48,8 @@ class AqiGauge extends StatelessWidget { return AnimatedRadialGauge( value: aqi, debug: false, - duration: const Duration(milliseconds: 200), + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOut, initialValue: 0, alignment: Alignment.bottomCenter, builder: (context, child, value) { @@ -89,7 +90,7 @@ class AqiGauge extends StatelessWidget { thickness: 14, segmentSpacing: 4, ), - min: 0, + min: _minRange, max: _maxRange, pointer: GaugePointer.circle( position: const GaugePointerPosition.surface(), From fd186a00fd9c9aa2d205d529f9d284ed2cea2287 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 28 May 2025 15:41:18 +0300 Subject: [PATCH 081/181] add shadow to pointer to match the design. --- .../analytics/modules/air_quality/widgets/aqi_gauge.dart | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_gauge.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_gauge.dart index 70cd3be5..0061f7e8 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_gauge.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_gauge.dart @@ -60,7 +60,7 @@ class AqiGauge extends StatelessWidget { alignment: AlignmentDirectional.bottomCenter, child: Text.rich( TextSpan( - text: '${value.toInt()}', + text: value.toStringAsFixed(0), style: context.textTheme.bodySmall?.copyWith( color: ColorsManager.textPrimaryColor, fontWeight: FontWeight.w700, @@ -100,9 +100,14 @@ class AqiGauge extends StatelessWidget { width: 6, color: _getPointerColor(aqi), ), + shadow: const BoxShadow( + color: ColorsManager.blackColor, + blurRadius: 6, + offset: Offset(0, 2), + ), ), transformer: const GaugeAxisTransformer.colorFadeIn( - background: Colors.transparent, + background: ColorsManager.transparentColor, interval: Interval(0, 0), ), segments: [ From 79c5fe1651b9bd5526f8b35465dcec470b51164a Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 28 May 2025 16:13:23 +0300 Subject: [PATCH 082/181] add icons for side bar info (humidity and tempreture). --- assets/icons/humidity.svg | 7 +++++++ assets/icons/thermometer.svg | 8 ++++++++ .../widgets/air_quality_end_side_gauge_and_info.dart | 6 +++--- .../air_quality/widgets/air_quality_end_side_widget.dart | 2 +- lib/utils/constants/assets.dart | 2 ++ 5 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 assets/icons/humidity.svg create mode 100644 assets/icons/thermometer.svg diff --git a/assets/icons/humidity.svg b/assets/icons/humidity.svg new file mode 100644 index 00000000..585ac31f --- /dev/null +++ b/assets/icons/humidity.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/thermometer.svg b/assets/icons/thermometer.svg new file mode 100644 index 00000000..94aa72eb --- /dev/null +++ b/assets/icons/thermometer.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_gauge_and_info.dart b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_gauge_and_info.dart index 3f6006c3..1e669b13 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_gauge_and_info.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_gauge_and_info.dart @@ -69,11 +69,11 @@ class AirQualityEndSideGaugeAndInfo extends StatelessWidget { child: Row( children: [ SvgPicture.asset( - Assets.aqiTemperature, + Assets.temperatureAqiSidebar, height: 12, width: 12, colorFilter: const ColorFilter.mode( - ColorsManager.goodGreen, + ColorsManager.textPrimaryColor, BlendMode.srcIn, ), ), @@ -81,7 +81,7 @@ class AirQualityEndSideGaugeAndInfo extends StatelessWidget { const Text('30°C'), const SizedBox(width: 10), SvgPicture.asset( - Assets.aqiHumidity, + Assets.humidityAqiSidebar, height: 12, width: 12, colorFilter: const ColorFilter.mode( diff --git a/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart index 1c56b6d1..89dd50c9 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart @@ -101,7 +101,7 @@ class AirQualityEndSideWidget extends StatelessWidget { ), const SizedBox(height: 20), Expanded( - flex: 4, + flex: 6, child: Container( decoration: secondarySection.copyWith(boxShadow: const []), padding: const EdgeInsetsDirectional.all(20), diff --git a/lib/utils/constants/assets.dart b/lib/utils/constants/assets.dart index 64ecd0a9..6dcf5f83 100644 --- a/lib/utils/constants/assets.dart +++ b/lib/utils/constants/assets.dart @@ -456,4 +456,6 @@ class Assets { static const String aqiTemperature = 'assets/icons/aqi_temperature.svg'; static const String aqiHumidity = 'assets/icons/aqi_humidity.svg'; static const String aqiAirQuality = 'assets/icons/aqi_air_quality.svg'; + static const String temperatureAqiSidebar = 'assets/icons/thermometer.svg'; + static const String humidityAqiSidebar = 'assets/icons/humidity.svg'; } From 7c65b874eb5df100c8439bd157db48bfc43f2be6 Mon Sep 17 00:00:00 2001 From: mohammad Date: Wed, 28 May 2025 16:40:44 +0300 Subject: [PATCH 083/181] Refactor table layout to accommodate dynamic table size --- lib/pages/common/custom_table.dart | 218 +++--- .../users_table/view/user_table.dart | 9 +- .../users_table/view/users_page.dart | 621 ++++++++++-------- 3 files changed, 473 insertions(+), 375 deletions(-) diff --git a/lib/pages/common/custom_table.dart b/lib/pages/common/custom_table.dart index 04334393..62760a16 100644 --- a/lib/pages/common/custom_table.dart +++ b/lib/pages/common/custom_table.dart @@ -21,6 +21,7 @@ class DynamicTable extends StatefulWidget { final List? initialSelectedIds; final int uuidIndex; final Function(dynamic selectedRows)? onSelectionChanged; + final Function(int rowIndex)? onSettingsPressed; const DynamicTable({ super.key, required this.headers, @@ -37,6 +38,7 @@ class DynamicTable extends StatefulWidget { this.initialSelectedIds, required this.uuidIndex, this.onSelectionChanged, + this.onSettingsPressed, }); @override @@ -48,11 +50,20 @@ class _DynamicTableState extends State { bool _selectAll = false; final ScrollController _verticalScrollController = ScrollController(); final ScrollController _horizontalScrollController = ScrollController(); - + late ScrollController _horizontalHeaderScrollController; + late ScrollController _horizontalBodyScrollController; @override void initState() { super.initState(); _initializeSelection(); + _horizontalHeaderScrollController = ScrollController(); + _horizontalBodyScrollController = ScrollController(); + + // Synchronize horizontal scrolling + _horizontalBodyScrollController.addListener(() { + _horizontalHeaderScrollController + .jumpTo(_horizontalBodyScrollController.offset); + }); } @override @@ -102,101 +113,87 @@ class _DynamicTableState extends State { context.read().add(UpdateSelection(_selectedRows)); } + @override + void dispose() { + _horizontalHeaderScrollController.dispose(); + _horizontalBodyScrollController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Container( decoration: widget.cellDecoration, - child: Scrollbar( - controller: _verticalScrollController, - thumbVisibility: true, - trackVisibility: true, - child: Scrollbar( - controller: _horizontalScrollController, - thumbVisibility: true, - trackVisibility: true, - notificationPredicate: (notif) => notif.depth == 1, - child: SingleChildScrollView( - controller: _verticalScrollController, + child: Column( + children: [ + Container( + decoration: widget.headerDecoration ?? + const BoxDecoration(color: ColorsManager.boxColor), child: SingleChildScrollView( - controller: _horizontalScrollController, scrollDirection: Axis.horizontal, + physics: const NeverScrollableScrollPhysics(), + controller: _horizontalHeaderScrollController, child: SizedBox( width: widget.size.width, - child: Column( + child: Row( children: [ - Container( - decoration: widget.headerDecoration ?? - const BoxDecoration( - color: ColorsManager.boxColor, - ), - child: Row( - children: [ - if (widget.withCheckBox) _buildSelectAllCheckbox(), - ...List.generate(widget.headers.length, (index) { - return _buildTableHeaderCell( - widget.headers[index], index); - }) - //...widget.headers.map((header) => _buildTableHeaderCell(header)), - ], - ), - ), - widget.isEmpty - ? SizedBox( - height: widget.size.height * 0.5, - width: widget.size.width, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Column( - children: [ - SvgPicture.asset(Assets.emptyTable), - const SizedBox( - height: 15, - ), - Text( - widget.tableName == 'AccessManagement' - ? 'No Password ' - : 'No Devices', - style: Theme.of(context) - .textTheme - .bodySmall! - .copyWith( - color: - ColorsManager.grayColor), - ) - ], - ), - ], - ), - ], - ), - ) - : Column( - children: - List.generate(widget.data.length, (index) { - final row = widget.data[index]; - return Row( - children: [ - if (widget.withCheckBox) - _buildRowCheckbox( - index, widget.size.height * 0.08), - ...row.map((cell) => _buildTableCell( - cell.toString(), - widget.size.height * 0.08)), - ], - ); - }), - ), + if (widget.withCheckBox) _buildSelectAllCheckbox(), + ...List.generate(widget.headers.length, (index) { + return _buildTableHeaderCell( + widget.headers[index], index); + }), ], ), ), ), ), - ), + Expanded( + child: Scrollbar( + controller: _verticalScrollController, + thumbVisibility: true, + trackVisibility: true, + child: SingleChildScrollView( + controller: _verticalScrollController, + child: Scrollbar( + controller: _horizontalBodyScrollController, + thumbVisibility: false, + trackVisibility: false, + notificationPredicate: (notif) => notif.depth == 1, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + controller: _horizontalBodyScrollController, + child: SizedBox( + width: widget.size.width, + child: widget.isEmpty + ? _buildEmptyState() + : Column( + children: + List.generate(widget.data.length, (rowIndex) { + final row = widget.data[rowIndex]; + return Row( + children: [ + if (widget.withCheckBox) + _buildRowCheckbox( + rowIndex, widget.size.height * 0.08), + ...row.asMap().entries.map((entry) { + return _buildTableCell( + entry.value.toString(), + widget.size.height * 0.08, + rowIndex: rowIndex, + columnIndex: entry.key, + ); + }).toList(), + ], + ); + }), + ), + ), + ), + ), + ), + ), + ), + ], ), ); } @@ -218,6 +215,32 @@ class _DynamicTableState extends State { ); } + Widget _buildEmptyState() => Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + children: [ + SvgPicture.asset(Assets.emptyTable), + const SizedBox(height: 15), + Text( + widget.tableName == 'AccessManagement' + ? 'No Password ' + : 'No Devices', + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(color: ColorsManager.grayColor), + ) + ], + ), + ], + ), + ], + ); Widget _buildRowCheckbox(int index, double size) { return Container( width: 50, @@ -272,13 +295,23 @@ class _DynamicTableState extends State { ); } - Widget _buildTableCell(String content, double size) { + Widget _buildTableCell( + String content, + double size, { + required int rowIndex, + required int columnIndex, + }) { bool isBatteryLevel = content.endsWith('%'); double? batteryLevel; if (isBatteryLevel) { batteryLevel = double.tryParse(content.replaceAll('%', '').trim()); } + bool isSettingsColumn = widget.headers[columnIndex] == 'Settings'; + + if (isSettingsColumn) { + return _buildSettingsIcon(rowIndex, size); + } Color? statusColor; switch (content) { @@ -330,4 +363,23 @@ class _DynamicTableState extends State { ), ); } + + Widget _buildSettingsIcon(int rowIndex, double size) { + return Container( + height: size, + width: 120, + padding: const EdgeInsets.all(5.0), + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide(color: ColorsManager.boxDivider, width: 1.0), + ), + color: Colors.white, + ), + alignment: Alignment.center, + child: IconButton( + icon: SvgPicture.asset(Assets.settings), + onPressed: () => widget.onSettingsPressed?.call(rowIndex), + ), + ); + } } diff --git a/lib/pages/roles_and_permission/users_page/users_table/view/user_table.dart b/lib/pages/roles_and_permission/users_page/users_table/view/user_table.dart index b26c09c4..9b10b5d4 100644 --- a/lib/pages/roles_and_permission/users_page/users_table/view/user_table.dart +++ b/lib/pages/roles_and_permission/users_page/users_table/view/user_table.dart @@ -95,7 +95,7 @@ class _TableRow extends StatelessWidget { ], ), if (!isLast) - Divider( + const Divider( height: 1, thickness: 1, color: ColorsManager.boxDivider, @@ -110,12 +110,14 @@ class DynamicTableScreen extends StatefulWidget { final List titles; final List> rows; final void Function(int columnIndex)? onFilter; + final double tableSize; const DynamicTableScreen({ required this.titles, required this.rows, required this.onFilter, Key? key, + required this.tableSize, }) : super(key: key); @override @@ -205,7 +207,8 @@ class _DynamicTableScreenState extends State { bottomRight: Radius.circular(15), ), ), - child: Column( + child: ListView( + shrinkWrap: true, children: [ for (int rowIndex = 0; rowIndex < widget.rows.length; rowIndex++) _TableRow( @@ -253,7 +256,7 @@ class _DynamicTableScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildHeader(), - _buildBody(), + Container(height: widget.tableSize - 37, child: _buildBody()), ], ), ), diff --git a/lib/pages/roles_and_permission/users_page/users_table/view/users_page.dart b/lib/pages/roles_and_permission/users_page/users_table/view/users_page.dart index b5455646..26c41201 100644 --- a/lib/pages/roles_and_permission/users_page/users_table/view/users_page.dart +++ b/lib/pages/roles_and_permission/users_page/users_table/view/users_page.dart @@ -27,7 +27,8 @@ class UsersPage extends StatelessWidget { Widget build(BuildContext context) { final TextEditingController searchController = TextEditingController(); - Widget actionButton({bool isActive = false, required String title, Function()? onTap}) { + Widget actionButton( + {bool isActive = false, required String title, Function()? onTap}) { return InkWell( onTap: onTap, child: Padding( @@ -60,7 +61,8 @@ class UsersPage extends StatelessWidget { : ColorsManager.disabledPink.withOpacity(0.5), ), child: Padding( - padding: const EdgeInsets.only(left: 10, right: 10, bottom: 5, top: 5), + padding: + const EdgeInsets.only(left: 10, right: 10, bottom: 5, top: 5), child: Row( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, @@ -84,12 +86,15 @@ class UsersPage extends StatelessWidget { } Widget changeIconStatus( - {required String userId, required String status, required Function()? onTap}) { + {required String userId, + required String status, + required Function()? onTap}) { return Center( child: InkWell( onTap: onTap, child: Padding( - padding: const EdgeInsets.only(left: 5, right: 5, bottom: 5, top: 5), + padding: + const EdgeInsets.only(left: 5, right: 5, bottom: 5, top: 5), child: SvgPicture.asset( status == "invited" ? Assets.invitedIcon @@ -114,8 +119,7 @@ class UsersPage extends StatelessWidget { padding: const EdgeInsets.all(20), child: Align( alignment: Alignment.topCenter, - child: ListView( - shrinkWrap: true, + child: Column( children: [ Row( children: [ @@ -188,292 +192,325 @@ class UsersPage extends StatelessWidget { ), ], ), - const SizedBox(height: 25), - DynamicTableScreen( - onFilter: (columnIndex) { - if (columnIndex == 0) { - showNameMenu( - context: context, - isSelected: _blocRole.currentSortOrder, - aToZTap: () { - context.read().add(const SortUsersByNameAsc()); - }, - zToaTap: () { - context.read().add(const SortUsersByNameDesc()); - }, - ); - } - if (columnIndex == 2) { - final Map checkboxStates = { - for (var item in _blocRole.jobTitle) - item: _blocRole.selectedJobTitles.contains(item), - }; - final RenderBox overlay = - Overlay.of(context).context.findRenderObject() as RenderBox; + const SizedBox(height: 20), + Container( + height: screenSize.height * 0.65, + child: DynamicTableScreen( + tableSize: screenSize.height * 0.65, + onFilter: (columnIndex) { + if (columnIndex == 0) { + showNameMenu( + context: context, + isSelected: _blocRole.currentSortOrder, + aToZTap: () { + context + .read() + .add(const SortUsersByNameAsc()); + }, + zToaTap: () { + context + .read() + .add(const SortUsersByNameDesc()); + }, + ); + } + if (columnIndex == 2) { + final Map checkboxStates = { + for (var item in _blocRole.jobTitle) + item: _blocRole.selectedJobTitles.contains(item), + }; + final RenderBox overlay = Overlay.of(context) + .context + .findRenderObject() as RenderBox; - showPopUpFilterMenu( - position: RelativeRect.fromLTRB( - overlay.size.width / 5.3, - 240, - overlay.size.width / 4, - 0, - ), - list: _blocRole.jobTitle, - context: context, - checkboxStates: checkboxStates, - isSelected: _blocRole.currentSortJopTitle, - onOkPressed: () { - searchController.clear(); - _blocRole.add(FilterClearEvent()); - final selectedItems = checkboxStates.entries - .where((entry) => entry.value) - .map((entry) => entry.key) - .toList(); - Navigator.of(context).pop(); - _blocRole.add(FilterUsersByJobEvent( - selectedJob: selectedItems, - sortOrder: _blocRole.currentSortJopTitle, - )); - }, - onSortAtoZ: (v) { - _blocRole.currentSortJopTitle = v; - }, - onSortZtoA: (v) { - _blocRole.currentSortJopTitle = v; - }, - ); - } + showPopUpFilterMenu( + position: RelativeRect.fromLTRB( + overlay.size.width / 5.3, + 240, + overlay.size.width / 4, + 0, + ), + list: _blocRole.jobTitle, + context: context, + checkboxStates: checkboxStates, + isSelected: _blocRole.currentSortJopTitle, + onOkPressed: () { + searchController.clear(); + _blocRole.add(FilterClearEvent()); + final selectedItems = checkboxStates.entries + .where((entry) => entry.value) + .map((entry) => entry.key) + .toList(); + Navigator.of(context).pop(); + _blocRole.add(FilterUsersByJobEvent( + selectedJob: selectedItems, + sortOrder: _blocRole.currentSortJopTitle, + )); + }, + onSortAtoZ: (v) { + _blocRole.currentSortJopTitle = v; + }, + onSortZtoA: (v) { + _blocRole.currentSortJopTitle = v; + }, + ); + } - if (columnIndex == 3) { - final Map checkboxStates = { - for (var item in _blocRole.roleTypes) - item: _blocRole.selectedRoles.contains(item), - }; - final RenderBox overlay = - Overlay.of(context).context.findRenderObject() as RenderBox; - showPopUpFilterMenu( - position: RelativeRect.fromLTRB( - overlay.size.width / 4, - 240, - overlay.size.width / 4, - 0, - ), - list: _blocRole.roleTypes, - context: context, - checkboxStates: checkboxStates, - isSelected: _blocRole.currentSortRole, - onOkPressed: () { - searchController.clear(); - _blocRole.add(FilterClearEvent()); - final selectedItems = checkboxStates.entries - .where((entry) => entry.value) - .map((entry) => entry.key) - .toList(); - Navigator.of(context).pop(); - context.read().add(FilterUsersByRoleEvent( - selectedRoles: selectedItems, - sortOrder: _blocRole.currentSortRole)); - }, - onSortAtoZ: (v) { - _blocRole.currentSortRole = v; - }, - onSortZtoA: (v) { - _blocRole.currentSortRole = v; - }, - ); - } - if (columnIndex == 4) { - showDateFilterMenu( - context: context, - isSelected: _blocRole.currentSortOrder, - aToZTap: () { - context.read().add(const DateNewestToOldestEvent()); - }, - zToaTap: () { - context.read().add(const DateOldestToNewestEvent()); - }, - ); - } - if (columnIndex == 6) { - final Map checkboxStates = { - for (var item in _blocRole.createdBy) - item: _blocRole.selectedCreatedBy.contains(item), - }; - final RenderBox overlay = - Overlay.of(context).context.findRenderObject() as RenderBox; - showPopUpFilterMenu( - position: RelativeRect.fromLTRB( - overlay.size.width / 1, - 240, - overlay.size.width / 4, - 0, - ), - list: _blocRole.createdBy, - context: context, - checkboxStates: checkboxStates, - isSelected: _blocRole.currentSortCreatedBy, - onOkPressed: () { - searchController.clear(); - _blocRole.add(FilterClearEvent()); - final selectedItems = checkboxStates.entries - .where((entry) => entry.value) - .map((entry) => entry.key) - .toList(); - Navigator.of(context).pop(); - _blocRole.add(FilterUsersByCreatedEvent( - selectedCreatedBy: selectedItems, - sortOrder: _blocRole.currentSortCreatedBy)); - }, - onSortAtoZ: (v) { - _blocRole.currentSortCreatedBy = v; - }, - onSortZtoA: (v) { - _blocRole.currentSortCreatedBy = v; - }, - ); - } - if (columnIndex == 7) { - final Map checkboxStates = { - for (var item in _blocRole.status) - item: _blocRole.selectedStatuses.contains(item), - }; + if (columnIndex == 3) { + final Map checkboxStates = { + for (var item in _blocRole.roleTypes) + item: _blocRole.selectedRoles.contains(item), + }; + final RenderBox overlay = Overlay.of(context) + .context + .findRenderObject() as RenderBox; + showPopUpFilterMenu( + position: RelativeRect.fromLTRB( + overlay.size.width / 4, + 240, + overlay.size.width / 4, + 0, + ), + list: _blocRole.roleTypes, + context: context, + checkboxStates: checkboxStates, + isSelected: _blocRole.currentSortRole, + onOkPressed: () { + searchController.clear(); + _blocRole.add(FilterClearEvent()); + final selectedItems = checkboxStates.entries + .where((entry) => entry.value) + .map((entry) => entry.key) + .toList(); + Navigator.of(context).pop(); + context.read().add( + FilterUsersByRoleEvent( + selectedRoles: selectedItems, + sortOrder: _blocRole.currentSortRole)); + }, + onSortAtoZ: (v) { + _blocRole.currentSortRole = v; + }, + onSortZtoA: (v) { + _blocRole.currentSortRole = v; + }, + ); + } + if (columnIndex == 4) { + showDateFilterMenu( + context: context, + isSelected: _blocRole.currentSortOrder, + aToZTap: () { + context + .read() + .add(const DateNewestToOldestEvent()); + }, + zToaTap: () { + context + .read() + .add(const DateOldestToNewestEvent()); + }, + ); + } + if (columnIndex == 6) { + final Map checkboxStates = { + for (var item in _blocRole.createdBy) + item: _blocRole.selectedCreatedBy.contains(item), + }; + final RenderBox overlay = Overlay.of(context) + .context + .findRenderObject() as RenderBox; + showPopUpFilterMenu( + position: RelativeRect.fromLTRB( + overlay.size.width / 1, + 240, + overlay.size.width / 4, + 0, + ), + list: _blocRole.createdBy, + context: context, + checkboxStates: checkboxStates, + isSelected: _blocRole.currentSortCreatedBy, + onOkPressed: () { + searchController.clear(); + _blocRole.add(FilterClearEvent()); + final selectedItems = checkboxStates.entries + .where((entry) => entry.value) + .map((entry) => entry.key) + .toList(); + Navigator.of(context).pop(); + _blocRole.add(FilterUsersByCreatedEvent( + selectedCreatedBy: selectedItems, + sortOrder: _blocRole.currentSortCreatedBy)); + }, + onSortAtoZ: (v) { + _blocRole.currentSortCreatedBy = v; + }, + onSortZtoA: (v) { + _blocRole.currentSortCreatedBy = v; + }, + ); + } + if (columnIndex == 7) { + final Map checkboxStates = { + for (var item in _blocRole.status) + item: _blocRole.selectedStatuses.contains(item), + }; - final RenderBox overlay = - Overlay.of(context).context.findRenderObject() as RenderBox; - showPopUpFilterMenu( - position: RelativeRect.fromLTRB( - overlay.size.width / 0, - 240, - overlay.size.width / 5, - 0, + final RenderBox overlay = Overlay.of(context) + .context + .findRenderObject() as RenderBox; + showPopUpFilterMenu( + position: RelativeRect.fromLTRB( + overlay.size.width / 0, + 240, + overlay.size.width / 5, + 0, + ), + list: _blocRole.status, + context: context, + checkboxStates: checkboxStates, + isSelected: _blocRole.currentSortStatus, + onOkPressed: () { + searchController.clear(); + _blocRole.add(FilterClearEvent()); + final selectedItems = checkboxStates.entries + .where((entry) => entry.value) + .map((entry) => entry.key) + .toList(); + Navigator.of(context).pop(); + _blocRole.add(FilterUsersByDeActevateEvent( + selectedActivate: selectedItems, + sortOrder: _blocRole.currentSortStatus)); + }, + onSortAtoZ: (v) { + _blocRole.currentSortStatus = v; + }, + onSortZtoA: (v) { + _blocRole.currentSortStatus = v; + }, + ); + } + if (columnIndex == 8) { + showDeActivateFilterMenu( + context: context, + isSelected: _blocRole.currentSortOrderDate, + aToZTap: () { + context + .read() + .add(const DateNewestToOldestEvent()); + }, + zToaTap: () { + context + .read() + .add(const DateOldestToNewestEvent()); + }, + ); + } + }, + titles: const [ + "Full Name", + "Email Address", + "Job Title", + "Role", + "Creation Date", + "Creation Time", + "Created By", + "Status", + "De/Activate", + "Action" + ], + rows: state.users.map((user) { + return [ + Text('${user.firstName} ${user.lastName}'), + Text(user.email), + Text(user.jobTitle), + Text(user.roleType ?? ''), + Text(user.createdDate ?? ''), + Text(user.createdTime ?? ''), + Text(user.invitedBy), + status( + status: user.isEnabled == false + ? 'disabled' + : user.status, ), - list: _blocRole.status, - context: context, - checkboxStates: checkboxStates, - isSelected: _blocRole.currentSortStatus, - onOkPressed: () { - searchController.clear(); - _blocRole.add(FilterClearEvent()); - final selectedItems = checkboxStates.entries - .where((entry) => entry.value) - .map((entry) => entry.key) - .toList(); - Navigator.of(context).pop(); - _blocRole.add(FilterUsersByDeActevateEvent( - selectedActivate: selectedItems, - sortOrder: _blocRole.currentSortStatus)); - }, - onSortAtoZ: (v) { - _blocRole.currentSortStatus = v; - }, - onSortZtoA: (v) { - _blocRole.currentSortStatus = v; - }, - ); - } - if (columnIndex == 8) { - showDeActivateFilterMenu( - context: context, - isSelected: _blocRole.currentSortOrderDate, - aToZTap: () { - context.read().add(const DateNewestToOldestEvent()); - }, - zToaTap: () { - context.read().add(const DateOldestToNewestEvent()); - }, - ); - } - }, - titles: const [ - "Full Name", - "Email Address", - "Job Title", - "Role", - "Creation Date", - "Creation Time", - "Created By", - "Status", - "De/Activate", - "Action" - ], - rows: state.users.map((user) { - return [ - Text('${user.firstName} ${user.lastName}'), - Text(user.email), - Text(user.jobTitle), - Text(user.roleType ?? ''), - Text(user.createdDate ?? ''), - Text(user.createdTime ?? ''), - Text(user.invitedBy), - status( - status: user.isEnabled == false ? 'disabled' : user.status, - ), - changeIconStatus( - status: user.isEnabled == false ? 'disabled' : user.status, - userId: user.uuid, - onTap: user.status != "invited" - ? () { - context.read().add(ChangeUserStatus( - userId: user.uuid, - newStatus: - user.isEnabled == false ? 'disabled' : user.status)); - } - : null, - ), - Row( - children: [ - user.isEnabled != false - ? actionButton( - isActive: true, - title: "Edit", - onTap: () { - context.read().add(ClearCachedData()); - showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext context) { - return EditUserDialog(userId: user.uuid); - }, - ).then((v) { - if (v != null) { + changeIconStatus( + status: user.isEnabled == false + ? 'disabled' + : user.status, + userId: user.uuid, + onTap: user.status != "invited" + ? () { + context.read().add( + ChangeUserStatus( + userId: user.uuid, + newStatus: user.isEnabled == false + ? 'disabled' + : user.status)); + } + : null, + ), + Row( + children: [ + user.isEnabled != false + ? actionButton( + isActive: true, + title: "Edit", + onTap: () { + context + .read() + .add(ClearCachedData()); + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return EditUserDialog( + userId: user.uuid); + }, + ).then((v) { if (v != null) { - _blocRole.add(const GetUsers()); + if (v != null) { + _blocRole.add(const GetUsers()); + } } + }); + }, + ) + : actionButton( + title: "Edit", + ), + actionButton( + title: "Delete", + onTap: () { + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return DeleteUserDialog( + onTapDelete: () async { + try { + _blocRole.add(DeleteUserEvent( + user.uuid, context)); + await Future.delayed( + const Duration(seconds: 2)); + return true; + } catch (e) { + return false; } }); }, - ) - : actionButton( - title: "Edit", - ), - actionButton( - title: "Delete", - onTap: () { - showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext context) { - return DeleteUserDialog(onTapDelete: () async { - try { - _blocRole.add(DeleteUserEvent(user.uuid, context)); - await Future.delayed(const Duration(seconds: 2)); - return true; - } catch (e) { - return false; - } - }); - }, - ).then((v) { - if (v != null) { - _blocRole.add(const GetUsers()); - } - }); - }, - ), - ], - ), - ]; - }).toList(), + ).then((v) { + if (v != null) { + _blocRole.add(const GetUsers()); + } + }); + }, + ), + ], + ), + ]; + }).toList(), + ), ), Padding( padding: const EdgeInsets.all(8.0), @@ -486,14 +523,20 @@ class UsersPage extends StatelessWidget { visiblePagesCount: 4, buttonRadius: 10, selectedButtonColor: ColorsManager.secondaryColor, - buttonUnSelectedBorderColor: ColorsManager.grayBorder, - lastPageIcon: const Icon(Icons.keyboard_double_arrow_right), - firstPageIcon: const Icon(Icons.keyboard_double_arrow_left), - totalPages: - (_blocRole.totalUsersCount.length / _blocRole.itemsPerPage).ceil(), + buttonUnSelectedBorderColor: + ColorsManager.grayBorder, + lastPageIcon: + const Icon(Icons.keyboard_double_arrow_right), + firstPageIcon: + const Icon(Icons.keyboard_double_arrow_left), + totalPages: (_blocRole.totalUsersCount.length / + _blocRole.itemsPerPage) + .ceil(), currentPage: _blocRole.currentPage, onPageChanged: (int pageNumber) { - context.read().add(ChangePage(pageNumber)); + context + .read() + .add(ChangePage(pageNumber)); }, ), ), From cc5f107ccb00bcc510d186ae49269b20a34dc770 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 28 May 2025 16:37:34 +0300 Subject: [PATCH 084/181] Extracted `AqiHumidityAndTemperature` into its own widget and file. --- .../air_quality/views/air_quality_view.dart | 7 +-- .../air_quality_end_side_gauge_and_info.dart | 46 ++--------------- .../widgets/aqi_humidity_and_temperature.dart | 51 +++++++++++++++++++ 3 files changed, 56 insertions(+), 48 deletions(-) create mode 100644 lib/pages/analytics/modules/air_quality/widgets/aqi_humidity_and_temperature.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 c24922cf..a7f1893f 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 @@ -41,7 +41,7 @@ class AirQualityView extends StatelessWidget { spacing: 32, children: [ Expanded( - flex: 5, + flex: 10, child: Column( spacing: 20, children: [ @@ -50,10 +50,7 @@ class AirQualityView extends StatelessWidget { ], ), ), - Expanded( - flex: 3, - child: AirQualityEndSideWidget(), - ), + Expanded(flex: 5, child: AirQualityEndSideWidget()), ], ), ), diff --git a/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_gauge_and_info.dart b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_gauge_and_info.dart index 1e669b13..06a33c26 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_gauge_and_info.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_gauge_and_info.dart @@ -1,8 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_gauge.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_humidity_and_temperature.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'; class AirQualityEndSideGaugeAndInfo extends StatelessWidget { @@ -19,9 +18,7 @@ class AirQualityEndSideGaugeAndInfo extends StatelessWidget { child: Row( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start, - children: [ - Expanded(flex: 1, child: AqiGauge(aqi: 200)), - ], + children: [Expanded(child: AqiGauge(aqi: 200))], ), ), const Spacer(), @@ -57,44 +54,7 @@ class AirQualityEndSideGaugeAndInfo extends StatelessWidget { ), ), const Spacer(), - FittedBox( - fit: BoxFit.scaleDown, - alignment: AlignmentDirectional.centerStart, - child: DefaultTextStyle( - style: context.textTheme.bodySmall!.copyWith( - color: ColorsManager.textPrimaryColor, - fontWeight: FontWeight.w400, - fontSize: 12, - ), - child: Row( - children: [ - SvgPicture.asset( - Assets.temperatureAqiSidebar, - height: 12, - width: 12, - colorFilter: const ColorFilter.mode( - ColorsManager.textPrimaryColor, - BlendMode.srcIn, - ), - ), - const SizedBox(width: 4), - const Text('30°C'), - const SizedBox(width: 10), - SvgPicture.asset( - Assets.humidityAqiSidebar, - height: 12, - width: 12, - colorFilter: const ColorFilter.mode( - ColorsManager.textPrimaryColor, - BlendMode.srcIn, - ), - ), - const SizedBox(width: 4), - const Text('30°C'), - ], - ), - ), - ), + const AqiHumidityAndTemperature(), ], ), ), diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_humidity_and_temperature.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_humidity_and_temperature.dart new file mode 100644 index 00000000..e8a3f7b4 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_humidity_and_temperature.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class AqiHumidityAndTemperature extends StatelessWidget { + const AqiHumidityAndTemperature({super.key}); + + static const iconSize = 12.0; + static const colorFilter = ColorFilter.mode( + ColorsManager.textPrimaryColor, + BlendMode.srcIn, + ); + + @override + Widget build(BuildContext context) { + return FittedBox( + fit: BoxFit.scaleDown, + alignment: AlignmentDirectional.centerStart, + child: DefaultTextStyle( + style: context.textTheme.bodySmall!.copyWith( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w400, + fontSize: 12, + ), + child: Row( + children: [ + SvgPicture.asset( + Assets.temperatureAqiSidebar, + height: iconSize, + width: iconSize, + colorFilter: colorFilter, + ), + const SizedBox(width: 4), + const Text('30°C'), + const SizedBox(width: 10), + SvgPicture.asset( + Assets.humidityAqiSidebar, + height: iconSize, + width: iconSize, + colorFilter: colorFilter, + ), + const SizedBox(width: 4), + const Text('30°C'), + ], + ), + ), + ); + } +} From fccf395c38de2677910ff31c30ee3e052e126bcc Mon Sep 17 00:00:00 2001 From: mohammad Date: Wed, 28 May 2025 16:56:51 +0300 Subject: [PATCH 085/181] Update function names to follow consistent naming convention in name_filter.dart and users_page.dart --- .../users_page/users_table/view/name_filter.dart | 4 ++-- .../users_page/users_table/view/users_page.dart | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pages/roles_and_permission/users_page/users_table/view/name_filter.dart b/lib/pages/roles_and_permission/users_page/users_table/view/name_filter.dart index 5ff10b20..f551cf3c 100644 --- a/lib/pages/roles_and_permission/users_page/users_table/view/name_filter.dart +++ b/lib/pages/roles_and_permission/users_page/users_table/view/name_filter.dart @@ -5,7 +5,7 @@ import 'package:syncrow_web/utils/constants/assets.dart'; Future showNameMenu({ required BuildContext context, Function()? aToZTap, - Function()? zToaTap, + Function()? zToATap, String? isSelected, }) async { final RenderBox overlay = @@ -46,7 +46,7 @@ Future showNameMenu({ ), ), PopupMenuItem( - onTap: zToaTap, + onTap: zToATap, child: ListTile( leading: Image.asset( Assets.ZtoAIcon, diff --git a/lib/pages/roles_and_permission/users_page/users_table/view/users_page.dart b/lib/pages/roles_and_permission/users_page/users_table/view/users_page.dart index 26c41201..735ce839 100644 --- a/lib/pages/roles_and_permission/users_page/users_table/view/users_page.dart +++ b/lib/pages/roles_and_permission/users_page/users_table/view/users_page.dart @@ -207,7 +207,7 @@ class UsersPage extends StatelessWidget { .read() .add(const SortUsersByNameAsc()); }, - zToaTap: () { + zToATap: () { context .read() .add(const SortUsersByNameDesc()); From a75e6a89a98eab2390c3a3d3aef573e8e760e1e7 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 29 May 2025 09:24:29 +0300 Subject: [PATCH 086/181] Enhanced responsiveness of `AqiLocationInfoCell`. --- .../air_quality/views/air_quality_view.dart | 2 +- .../widgets/air_quality_end_side_widget.dart | 4 +- .../widgets/aqi_location_info_cell.dart | 51 ++++++++++--------- 3 files changed, 31 insertions(+), 26 deletions(-) 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 a7f1893f..17ecbc22 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 @@ -50,7 +50,7 @@ class AirQualityView extends StatelessWidget { ], ), ), - Expanded(flex: 5, child: AirQualityEndSideWidget()), + Expanded(flex: 6, child: AirQualityEndSideWidget()), ], ), ), diff --git a/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart index 89dd50c9..30dc9b89 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart @@ -115,7 +115,7 @@ class AirQualityEndSideWidget extends StatelessWidget { children: [ AqiLocationInfoCell( label: 'Temperature', - value: '25°', + value: ' 25°', svgPath: Assets.aqiTemperature, ), AqiLocationInfoCell( @@ -125,7 +125,7 @@ class AirQualityEndSideWidget extends StatelessWidget { ), AqiLocationInfoCell( label: 'Air Quality', - value: '120', + value: ' 120', svgPath: Assets.aqiAirQuality, ), ], diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info_cell.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info_cell.dart index e6ee71d7..fa0216a1 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info_cell.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info_cell.dart @@ -27,17 +27,20 @@ class AqiLocationInfoCell extends StatelessWidget { children: [ Align( alignment: AlignmentDirectional.topStart, - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: AlignmentDirectional.topStart, - child: Padding( - padding: const EdgeInsetsDirectional.all(10), - child: Text( - label, - style: context.textTheme.bodySmall?.copyWith( - color: ColorsManager.textPrimaryColor, - fontWeight: FontWeight.w400, - fontSize: 12, + child: Padding( + padding: const EdgeInsetsDirectional.all(10), + child: SizedBox( + height: 24, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: AlignmentDirectional.topStart, + child: Text( + label, + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w400, + fontSize: 12, + ), ), ), ), @@ -46,18 +49,20 @@ class AqiLocationInfoCell extends StatelessWidget { Align( alignment: AlignmentDirectional.bottomEnd, child: Padding( - padding: const EdgeInsetsDirectional.all(10).add( - const EdgeInsetsDirectional.only(start: 32), - ), - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: AlignmentDirectional.bottomEnd, - child: Text( - value, - style: context.textTheme.bodySmall?.copyWith( - color: ColorsManager.vividBlue.withValues(alpha: 0.7), - fontWeight: FontWeight.w700, - fontSize: 24, + padding: const EdgeInsetsDirectional.all(10), + child: SizedBox( + height: 40, + width: 120, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: AlignmentDirectional.bottomEnd, + child: Text( + value, + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.vividBlue.withValues(alpha: 0.7), + fontWeight: FontWeight.w700, + fontSize: 24, + ), ), ), ), From 3d133581ffcf52384f6de7074bc5c956311917a1 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 29 May 2025 09:59:27 +0300 Subject: [PATCH 087/181] Implemented and used a reusable widget for analytics sidebars headers. --- .../widgets/air_quality_end_side_widget.dart | 69 +------------- .../widgets/analytics_device_dropdown.dart | 26 +++--- .../power_clamp_energy_data_widget.dart | 80 +++-------------- .../widgets/occupancy_end_side_bar.dart | 67 +------------- .../widgets/analytics_sidebar_header.dart | 90 +++++++++++++++++++ 5 files changed, 122 insertions(+), 210 deletions(-) create mode 100644 lib/pages/analytics/widgets/analytics_sidebar_header.dart diff --git a/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart index 30dc9b89..808c3ff8 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart @@ -1,17 +1,12 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/air_quality_end_side_gauge_and_info.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/air_quality_end_side_live_indicator.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_location.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_location_info_cell.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_sub_value_widget.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_devices/analytics_devices_bloc.dart'; -import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/fetch_energy_management_data_helper.dart'; -import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/analytics_device_dropdown.dart'; -import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/pages/analytics/widgets/analytics_sidebar_header.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; -import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/style.dart'; class AirQualityEndSideWidget extends StatelessWidget { @@ -27,26 +22,7 @@ class AirQualityEndSideWidget extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildHeader(context), - Text( - 'Device ID:', - style: context.textTheme.bodySmall?.copyWith( - color: ColorsManager.textPrimaryColor, - fontWeight: FontWeight.w400, - fontSize: 12, - ), - ), - const SizedBox(height: 6), - SelectableText( - context.watch().state.selectedDevice?.uuid ?? - 'N/A', - style: context.textTheme.bodySmall?.copyWith( - color: ColorsManager.blackColor, - fontWeight: FontWeight.w400, - fontSize: 12, - ), - ), - const Divider(), + const AnalyticsSidebarHeader(title: 'AQI Sensor'), Expanded( flex: 15, child: Container( @@ -139,45 +115,4 @@ class AirQualityEndSideWidget extends StatelessWidget { ), ); } - - Widget _buildHeader(BuildContext context) { - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded( - child: FittedBox( - alignment: AlignmentDirectional.centerStart, - fit: BoxFit.scaleDown, - child: SelectableText( - 'AQI Sensor', - style: context.textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.w700, - color: ColorsManager.vividBlue.withValues(alpha: 0.6), - fontSize: 18, - ), - ), - ), - ), - const Spacer(), - Expanded( - flex: 2, - child: FittedBox( - alignment: AlignmentDirectional.centerEnd, - fit: BoxFit.scaleDown, - child: AnalyticsDeviceDropdown( - onChanged: (value) { - context.read().add( - SelectAnalyticsDeviceEvent(value), - ); - FetchEnergyManagementDataHelper.loadRealtimeDeviceChanges( - context, - deviceUuid: value.uuid, - ); - }, - ), - ), - ), - ], - ); - } } diff --git a/lib/pages/analytics/modules/energy_management/widgets/analytics_device_dropdown.dart b/lib/pages/analytics/modules/energy_management/widgets/analytics_device_dropdown.dart index 65157aa4..f7b33309 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/analytics_device_dropdown.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/analytics_device_dropdown.dart @@ -6,9 +6,14 @@ import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; class AnalyticsDeviceDropdown extends StatelessWidget { - const AnalyticsDeviceDropdown({required this.onChanged, super.key}); + const AnalyticsDeviceDropdown({ + required this.onChanged, + this.showSpaceUuid = false, + super.key, + }); final ValueChanged onChanged; + final bool showSpaceUuid; @override Widget build(BuildContext context) { @@ -72,17 +77,18 @@ class AnalyticsDeviceDropdown extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ Text(e.name), - if (spaceUuid != null) - FittedBox( - fit: BoxFit.scaleDown, - alignment: AlignmentDirectional.centerStart, - child: Text( - spaceUuid, - style: _getTextStyle(context)?.copyWith( - fontSize: 10, + if (showSpaceUuid) + if (spaceUuid != null) + FittedBox( + fit: BoxFit.scaleDown, + alignment: AlignmentDirectional.centerStart, + child: Text( + spaceUuid, + style: _getTextStyle(context)?.copyWith( + fontSize: 10, + ), ), ), - ), ], ), ); 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 e8f802cd..4d04a36b 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 @@ -2,19 +2,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/analytics/models/power_clamp_energy_status.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/power_clamp_info/power_clamp_info_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/modules/energy_management/helpers/fetch_energy_management_data_helper.dart'; -import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/analytics_device_dropdown.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/energy_consumption_by_phases_chart_box.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/power_clamp_energy_status_widget.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/power_clamp_phases_data_widget.dart'; import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart'; +import 'package:syncrow_web/pages/analytics/widgets/analytics_sidebar_header.dart'; import 'package:syncrow_web/pages/device_managment/power_clamp/models/power_clamp_model.dart'; -import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; -import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/style.dart'; class PowerClampEnergyDataWidget extends StatelessWidget { @@ -42,26 +39,18 @@ class PowerClampEnergyDataWidget extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ AnalyticsErrorWidget(state.errorMessage), - _buildHeader(context), - Text( - 'Device ID:', - style: context.textTheme.bodySmall?.copyWith( - color: ColorsManager.textPrimaryColor, - fontWeight: FontWeight.w400, - fontSize: 12, - ), + AnalyticsSidebarHeader( + title: 'Smart Power Clamp', + showSpaceUuid: true, + onChanged: (device) { + FetchEnergyManagementDataHelper.loadEnergyConsumptionByPhases( + context, + powerClampUuid: device.uuid, + selectedDate: + context.read().state.monthlyDate, + ); + }, ), - const SizedBox(height: 6), - SelectableText( - context.watch().state.selectedDevice?.uuid ?? - 'N/A', - style: context.textTheme.bodySmall?.copyWith( - color: ColorsManager.blackColor, - fontWeight: FontWeight.w400, - fontSize: 12, - ), - ), - const Divider(), Expanded( flex: 2, child: PowerClampEnergyStatusWidget( @@ -111,51 +100,6 @@ class PowerClampEnergyDataWidget extends StatelessWidget { ); } - Widget _buildHeader(BuildContext context) { - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded( - flex: 3, - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: AlignmentDirectional.centerStart, - child: SelectableText( - 'Smart Power Clamp', - style: context.textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.w700, - color: ColorsManager.vividBlue.withValues(alpha: 0.6), - fontSize: 18, - ), - ), - ), - ), - const Spacer(), - Expanded( - flex: 2, - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: AlignmentDirectional.centerEnd, - child: AnalyticsDeviceDropdown( - onChanged: (value) { - FetchEnergyManagementDataHelper.loadEnergyConsumptionByPhases( - context, - powerClampUuid: value.uuid, - selectedDate: - context.read().state.monthlyDate, - ); - FetchEnergyManagementDataHelper.loadRealtimeDeviceChanges( - context, - deviceUuid: value.uuid, - ); - }, - ), - ), - ), - ], - ); - } - String _valueFromCode(String code, List points) { return points .firstWhere((e) => e.code == code, orElse: () => DataPoint(value: '--')) diff --git a/lib/pages/analytics/modules/occupancy/widgets/occupancy_end_side_bar.dart b/lib/pages/analytics/modules/occupancy/widgets/occupancy_end_side_bar.dart index 9f096789..b3f162fa 100644 --- a/lib/pages/analytics/modules/occupancy/widgets/occupancy_end_side_bar.dart +++ b/lib/pages/analytics/modules/occupancy/widgets/occupancy_end_side_bar.dart @@ -1,15 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/analytics/models/power_clamp_energy_status.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/modules/energy_management/helpers/fetch_energy_management_data_helper.dart'; -import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/analytics_device_dropdown.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/power_clamp_energy_status_widget.dart'; +import 'package:syncrow_web/pages/analytics/widgets/analytics_sidebar_header.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; -import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; -import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/style.dart'; class OccupancyEndSideBar extends StatelessWidget { @@ -27,28 +23,7 @@ class OccupancyEndSideBar extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildHeader(context), - Text( - 'Device ID:', - style: context.textTheme.bodySmall?.copyWith( - color: ColorsManager.textPrimaryColor, - fontWeight: FontWeight.w400, - fontSize: 12, - ), - ), - const SizedBox(height: 6), - SelectableText( - context.watch().state.selectedDevice?.uuid ?? - 'N/A', - style: context.textTheme.bodySmall?.copyWith( - color: ColorsManager.blackColor, - fontWeight: FontWeight.w400, - fontSize: 12, - ), - ), - const SizedBox(height: 10), - const Divider(height: 1, color: ColorsManager.greyColor), - const SizedBox(height: 50), + const AnalyticsSidebarHeader(title: 'Presnce Sensor'), SizedBox( height: MediaQuery.sizeOf(context).height * 0.2, child: PowerClampEnergyStatusWidget( @@ -101,42 +76,4 @@ class OccupancyEndSideBar extends StatelessWidget { .toString(); return value == 'null' ? defaultValue ?? '--' : value; } - - Widget _buildHeader(BuildContext context) { - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded( - flex: 3, - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: AlignmentDirectional.centerStart, - child: SelectableText( - 'Presnce Sensor', - style: context.textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.w700, - color: ColorsManager.vividBlue.withValues(alpha: 0.6), - fontSize: 18, - ), - ), - ), - ), - const Spacer(), - Expanded( - flex: 2, - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: AlignmentDirectional.centerEnd, - child: AnalyticsDeviceDropdown( - onChanged: (value) => - FetchEnergyManagementDataHelper.loadRealtimeDeviceChanges( - context, - deviceUuid: value.uuid, - ), - ), - ), - ), - ], - ); - } } diff --git a/lib/pages/analytics/widgets/analytics_sidebar_header.dart b/lib/pages/analytics/widgets/analytics_sidebar_header.dart new file mode 100644 index 00000000..5e454ea4 --- /dev/null +++ b/lib/pages/analytics/widgets/analytics_sidebar_header.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/analytics/models/analytics_device.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/helpers/fetch_energy_management_data_helper.dart'; +import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/analytics_device_dropdown.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class AnalyticsSidebarHeader extends StatelessWidget { + const AnalyticsSidebarHeader({ + required this.title, + this.showSpaceUuid = false, + this.onChanged, + super.key, + }); + + final String title; + final bool showSpaceUuid; + final void Function(AnalyticsDevice device)? onChanged; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + flex: 2, + child: FittedBox( + alignment: AlignmentDirectional.centerStart, + fit: BoxFit.scaleDown, + child: SelectableText( + title, + style: context.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w700, + color: ColorsManager.vividBlue.withValues(alpha: 0.6), + fontSize: 18, + ), + ), + ), + ), + const Spacer(), + Expanded( + flex: 2, + child: FittedBox( + alignment: AlignmentDirectional.centerEnd, + fit: BoxFit.scaleDown, + child: AnalyticsDeviceDropdown( + onChanged: (value) { + context.read().add( + SelectAnalyticsDeviceEvent(value), + ); + FetchEnergyManagementDataHelper.loadRealtimeDeviceChanges( + context, + deviceUuid: value.uuid, + ); + onChanged?.call(value); + }, + ), + ), + ), + ], + ), + Text( + 'Device ID:', + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w400, + fontSize: 12, + ), + ), + const SizedBox(height: 6), + SelectableText( + context.watch().state.selectedDevice?.uuid ?? 'N/A', + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.blackColor, + fontWeight: FontWeight.w400, + fontSize: 12, + ), + ), + const SizedBox(height: 10), + const Divider(height: 1, color: ColorsManager.greyColor), + const SizedBox(height: 24), + ], + ); + } +} From 0a9d53e5bd46a5be00dad9faa246d5a69e5dafd6 Mon Sep 17 00:00:00 2001 From: mohammad Date: Thu, 29 May 2025 10:48:12 +0300 Subject: [PATCH 088/181] 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 94b4aa7c46037f14a8379f7987277d0dd3426bc1 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 29 May 2025 11:25:27 +0300 Subject: [PATCH 089/181] Extracted big widgets into smaller ones, and integrated aqi device info with `RealtimeChangesBloc`. --- .../air_quality_end_side_gauge_and_info.dart | 14 ++- .../widgets/air_quality_end_side_widget.dart | 104 ++---------------- .../air_quality/widgets/aqi_device_info.dart | 100 +++++++++++++++++ .../widgets/aqi_humidity_and_temperature.dart | 13 ++- .../widgets/aqi_location_info.dart | 45 ++++++++ .../widgets/aqi_sub_value_widget.dart | 36 ++++-- 6 files changed, 200 insertions(+), 112 deletions(-) create mode 100644 lib/pages/analytics/modules/air_quality/widgets/aqi_device_info.dart create mode 100644 lib/pages/analytics/modules/air_quality/widgets/aqi_location_info.dart diff --git a/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_gauge_and_info.dart b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_gauge_and_info.dart index 06a33c26..8722ec68 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_gauge_and_info.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_gauge_and_info.dart @@ -5,7 +5,14 @@ import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; class AirQualityEndSideGaugeAndInfo extends StatelessWidget { - const AirQualityEndSideGaugeAndInfo({super.key}); + const AirQualityEndSideGaugeAndInfo({ + super.key, + required this.temperature, + required this.humidity, + }); + + final int temperature; + final int humidity; @override Widget build(BuildContext context) { @@ -54,7 +61,10 @@ class AirQualityEndSideGaugeAndInfo extends StatelessWidget { ), ), const Spacer(), - const AqiHumidityAndTemperature(), + AqiHumidityAndTemperature( + temperature: temperature, + humidity: humidity, + ), ], ), ), diff --git a/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart index 808c3ff8..6e182e18 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart @@ -1,12 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/air_quality_end_side_gauge_and_info.dart'; -import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/air_quality_end_side_live_indicator.dart'; -import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_location.dart'; -import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_location_info_cell.dart'; -import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_sub_value_widget.dart'; -import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_device_info.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_location_info.dart'; import 'package:syncrow_web/pages/analytics/widgets/analytics_sidebar_header.dart'; -import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/style.dart'; class AirQualityEndSideWidget extends StatelessWidget { @@ -19,98 +14,13 @@ class AirQualityEndSideWidget extends StatelessWidget { borderRadius: BorderRadius.circular(30), ), padding: const EdgeInsetsDirectional.all(32), - child: Column( + child: const Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const AnalyticsSidebarHeader(title: 'AQI Sensor'), - Expanded( - flex: 15, - child: Container( - decoration: secondarySection.copyWith(boxShadow: const []), - padding: const EdgeInsetsDirectional.all(20), - child: Expanded( - child: Column( - spacing: 6, - children: [ - const AirQualityEndSideLiveIndicator(), - const AirQualityEndSideGaugeAndInfo(), - const SizedBox(height: 20), - AqiSubValueWidget( - label: AqiType.pm25.value, - value: 19, - unit: AqiType.pm25.unit, - ), - AqiSubValueWidget( - label: AqiType.pm10.value, - value: 42, - unit: AqiType.pm10.unit, - ), - AqiSubValueWidget( - label: AqiType.co2.value, - value: 610, - unit: AqiType.co2.unit, - ), - AqiSubValueWidget( - label: AqiType.hcho.value, - value: 1, - unit: AqiType.hcho.unit, - ), - AqiSubValueWidget( - label: AqiType.tvoc.value, - value: 55, - unit: AqiType.tvoc.unit, - ), - AqiSubValueWidget( - label: AqiType.co2.value, - value: 18, - unit: AqiType.co2.unit, - ), - AqiSubValueWidget( - label: AqiType.c6h6.value, - value: 18, - unit: AqiType.c6h6.unit, - ), - ], - ), - ), - ), - ), - const SizedBox(height: 20), - Expanded( - flex: 6, - child: Container( - decoration: secondarySection.copyWith(boxShadow: const []), - padding: const EdgeInsetsDirectional.all(20), - child: const Column( - spacing: 8, - children: [ - AqiLocation(), - Expanded( - child: Row( - spacing: 8, - children: [ - AqiLocationInfoCell( - label: 'Temperature', - value: ' 25°', - svgPath: Assets.aqiTemperature, - ), - AqiLocationInfoCell( - label: 'Humidity', - value: '25%', - svgPath: Assets.aqiHumidity, - ), - AqiLocationInfoCell( - label: 'Air Quality', - value: ' 120', - svgPath: Assets.aqiAirQuality, - ), - ], - ), - ), - ], - ), - ), - ), + AnalyticsSidebarHeader(title: 'AQI Sensor'), + Expanded(flex: 15, child: AqiDeviceInfo()), + SizedBox(height: 20), + Expanded(flex: 6, child: AqiLocationInfo()), ], ), ); diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_device_info.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_device_info.dart new file mode 100644 index 00000000..451350ac --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_device_info.dart @@ -0,0 +1,100 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/air_quality_end_side_gauge_and_info.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/air_quality_end_side_live_indicator.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_sub_value_widget.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart'; +import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; +import 'package:syncrow_web/utils/style.dart'; + +class AqiDeviceInfo extends StatelessWidget { + const AqiDeviceInfo({super.key}); + + double _getValueForStatus( + List deviceStatusList, + String code, { + double defaultValue = 0, + }) { + try { + final foundStatus = deviceStatusList.firstWhere((e) => e.code == code); + return double.parse(foundStatus.value.toString()); + } catch (_) { + return defaultValue; + } + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final status = state.deviceStatusList; + final humidityValue = _getValueForStatus(status, 'humidity_value'); + final tempValue = _getValueForStatus(status, 'temp_current'); + final pm25Value = _getValueForStatus(status, 'pm25_value'); + final pm10Value = _getValueForStatus(status, 'pm10'); + final co2Value = _getValueForStatus(status, 'co2_value'); + final ch2oValue = _getValueForStatus(status, 'ch2o_value'); + final tvocValue = _getValueForStatus(status, 'tvoc_value'); + + return Container( + decoration: secondarySection.copyWith(boxShadow: const []), + padding: const EdgeInsetsDirectional.all(20), + child: Expanded( + child: Column( + spacing: 6, + children: [ + const AirQualityEndSideLiveIndicator(), + AirQualityEndSideGaugeAndInfo( + temperature: humidityValue.toInt(), + humidity: tempValue.toInt(), + ), + const SizedBox(height: 20), + AqiSubValueWidget( + range: (0, 999), + label: AqiType.pm25.value, + value: pm25Value < 100 + ? double.parse(pm25Value.toStringAsFixed(1).padLeft(4, '0')) + : pm25Value, + unit: AqiType.pm25.unit, + ), + AqiSubValueWidget( + range: (0, 999), + label: AqiType.pm10.value, + value: pm10Value < 100 + ? double.parse(pm10Value.toStringAsFixed(1).padLeft(4, '0')) + : pm10Value, + unit: AqiType.pm10.unit, + ), + AqiSubValueWidget( + range: (0, 5), + label: AqiType.hcho.value, + value: double.parse(ch2oValue.toStringAsFixed(2)), + unit: AqiType.hcho.unit, + ), + AqiSubValueWidget( + range: (0, 9.99), + label: AqiType.tvoc.value, + value: tvocValue, + unit: AqiType.tvoc.unit, + ), + AqiSubValueWidget( + range: (0, 5000), + label: AqiType.co2.value, + value: co2Value, + unit: AqiType.co2.unit, + ), + AqiSubValueWidget( + range: (0, 100), + label: AqiType.c6h6.value, + value: 18, + unit: AqiType.c6h6.unit, + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_humidity_and_temperature.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_humidity_and_temperature.dart index e8a3f7b4..6bea1db9 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_humidity_and_temperature.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_humidity_and_temperature.dart @@ -5,7 +5,13 @@ import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; class AqiHumidityAndTemperature extends StatelessWidget { - const AqiHumidityAndTemperature({super.key}); + const AqiHumidityAndTemperature({ + super.key, + required this.temperature, + required this.humidity, + }); + final int temperature; + final int humidity; static const iconSize = 12.0; static const colorFilter = ColorFilter.mode( @@ -13,6 +19,7 @@ class AqiHumidityAndTemperature extends StatelessWidget { BlendMode.srcIn, ); + @override Widget build(BuildContext context) { return FittedBox( @@ -33,7 +40,7 @@ class AqiHumidityAndTemperature extends StatelessWidget { colorFilter: colorFilter, ), const SizedBox(width: 4), - const Text('30°C'), + Text('$temperature°C'), const SizedBox(width: 10), SvgPicture.asset( Assets.humidityAqiSidebar, @@ -42,7 +49,7 @@ class AqiHumidityAndTemperature extends StatelessWidget { colorFilter: colorFilter, ), const SizedBox(width: 4), - const Text('30°C'), + Text('$humidity%'), ], ), ), diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info.dart new file mode 100644 index 00000000..f8e087b8 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_location.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_location_info_cell.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/style.dart'; + +class AqiLocationInfo extends StatelessWidget { + const AqiLocationInfo({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + decoration: secondarySection.copyWith(boxShadow: const []), + padding: const EdgeInsetsDirectional.all(20), + child: const Column( + spacing: 8, + children: [ + AqiLocation(), + Expanded( + child: Row( + spacing: 8, + children: [ + AqiLocationInfoCell( + label: 'Temperature', + value: ' 25°', + svgPath: Assets.aqiTemperature, + ), + AqiLocationInfoCell( + label: 'Humidity', + value: '25%', + svgPath: Assets.aqiHumidity, + ), + AqiLocationInfoCell( + label: 'Air Quality', + value: ' 120', + svgPath: Assets.aqiAirQuality, + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_sub_value_widget.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_sub_value_widget.dart index 6596019a..214a4d61 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_sub_value_widget.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_sub_value_widget.dart @@ -5,7 +5,7 @@ import 'package:syncrow_web/utils/extension/build_context_x.dart'; final class _AqiRange { const _AqiRange({required this.max, required this.color}); - final int max; + final double max; final Color color; } @@ -14,12 +14,35 @@ class AqiSubValueWidget extends StatelessWidget { required this.label, required this.value, required this.unit, + required this.range, super.key, }); + static List<_AqiRange> _getRangesForValue((double min, double max) range) { + final (double min, double max) = range; + final rangeSize = (max - min) / 6; + return [ + _AqiRange(max: range.$1 + rangeSize, color: ColorsManager.goodGreen), + _AqiRange(max: range.$1 + (rangeSize * 2), color: ColorsManager.poorOrange), + _AqiRange(max: range.$1 + (rangeSize * 3), color: ColorsManager.poorOrange), + _AqiRange(max: range.$1 + (rangeSize * 4), color: ColorsManager.unhealthyRed), + _AqiRange(max: range.$1 + (rangeSize * 5), color: ColorsManager.severePink), + _AqiRange(max: range.$2, color: ColorsManager.hazardousPurple), + ]; + } + + int _getActiveSegmentByRange(double value, (double min, double max) range) { + final ranges = _getRangesForValue(range); + for (int i = 0; i < ranges.length; i++) { + if (value <= ranges[i].max) return i; + } + return ranges.length - 1; + } + final String label; - final int value; + final double value; final String unit; + final (double min, double max) range; static const List<_AqiRange> _ranges = [ _AqiRange(max: 12, color: ColorsManager.goodGreen), @@ -30,16 +53,9 @@ class AqiSubValueWidget extends StatelessWidget { _AqiRange(max: 500, color: ColorsManager.hazardousPurple), ]; - int _getActiveSegment(int value) { - for (int i = 0; i < _ranges.length; i++) { - if (value <= _ranges[i].max) return i; - } - return _ranges.length - 1; - } - @override Widget build(BuildContext context) { - final activeSegment = _getActiveSegment(value); + final activeSegment = _getActiveSegmentByRange(value, range); return Expanded( child: Container( padding: const EdgeInsetsDirectional.all(10), From 7bfd08238e507152278d43b21bd0bb339e72d1b3 Mon Sep 17 00:00:00 2001 From: mohammad Date: Thu, 29 May 2025 12:19:04 +0300 Subject: [PATCH 090/181] Refactor event handling in GarageDoorBloc to use local variable for deviceId --- .../device_managment/garage_door/bloc/garage_door_bloc.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/device_managment/garage_door/bloc/garage_door_bloc.dart b/lib/pages/device_managment/garage_door/bloc/garage_door_bloc.dart index 9083ffbe..593fdeab 100644 --- a/lib/pages/device_managment/garage_door/bloc/garage_door_bloc.dart +++ b/lib/pages/device_managment/garage_door/bloc/garage_door_bloc.dart @@ -360,7 +360,7 @@ class GarageDoorBloc extends Bloc { delay: deviceStatus.delay + Duration(minutes: 10)); emit(GarageDoorLoadedState(status: deviceStatus)); add(GarageDoorControlEvent( - deviceId: event.deviceId, + deviceId: deviceId, value: deviceStatus.delay.inSeconds, code: 'countdown_1')); } catch (e) { From 43cb985e74c3057b93bc639ccb7d76b22271f616 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 29 May 2025 13:05:49 +0300 Subject: [PATCH 091/181] finished integrating realtime data. --- .../air_quality/views/air_quality_view.dart | 21 ++++- .../air_quality/widgets/aqi_device_info.dart | 76 ++++++++++++------- .../widgets/aqi_sub_value_widget.dart | 36 ++++----- .../widgets/aqi_type_dropdown.dart | 3 +- 4 files changed, 87 insertions(+), 49 deletions(-) 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..3a236a58 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,12 +1,27 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.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/range_of_aqi_chart_box.dart'; +import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart'; -class AirQualityView extends StatelessWidget { +class AirQualityView extends StatefulWidget { const AirQualityView({super.key}); static const _padding = EdgeInsetsDirectional.all(32); + @override + State createState() => _AirQualityViewState(); +} + +class _AirQualityViewState extends State { + @override + void initState() { + context.read().add( + const RealtimeDeviceChangesStarted('078e4ed4-8f85-47b8-8c78-5a766549135d'), + ); + super.initState(); + } + @override Widget build(BuildContext context) { return LayoutBuilder( @@ -15,7 +30,7 @@ class AirQualityView extends StatelessWidget { final height = MediaQuery.sizeOf(context).height; if (isMediumOrLess) { return SingleChildScrollView( - padding: _padding, + padding: AirQualityView._padding, child: Column( spacing: 32, children: [ @@ -32,7 +47,7 @@ class AirQualityView extends StatelessWidget { return SingleChildScrollView( child: Container( - padding: _padding, + padding: AirQualityView._padding, height: height * 1.1, child: const Column( children: [ diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_device_info.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_device_info.dart index 451350ac..d9a874bb 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_device_info.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_device_info.dart @@ -11,16 +11,19 @@ import 'package:syncrow_web/utils/style.dart'; class AqiDeviceInfo extends StatelessWidget { const AqiDeviceInfo({super.key}); - double _getValueForStatus( + String _getValueForStatus( List deviceStatusList, String code, { double defaultValue = 0, + String Function(int value)? formatter, }) { try { final foundStatus = deviceStatusList.firstWhere((e) => e.code == code); - return double.parse(foundStatus.value.toString()); - } catch (_) { - return defaultValue; + final value = foundStatus.value.toString(); + final intValue = int.parse(value); + return formatter != null ? formatter(intValue) : intValue.toString(); + } catch (e) { + return defaultValue.toString(); } } @@ -29,13 +32,42 @@ class AqiDeviceInfo extends StatelessWidget { return BlocBuilder( builder: (context, state) { final status = state.deviceStatusList; - final humidityValue = _getValueForStatus(status, 'humidity_value'); - final tempValue = _getValueForStatus(status, 'temp_current'); - final pm25Value = _getValueForStatus(status, 'pm25_value'); - final pm10Value = _getValueForStatus(status, 'pm10'); - final co2Value = _getValueForStatus(status, 'co2_value'); - final ch2oValue = _getValueForStatus(status, 'ch2o_value'); - final tvocValue = _getValueForStatus(status, 'tvoc_value'); + final humidityValue = _getValueForStatus( + status, + 'humidity_value', + formatter: (value) => value.toStringAsFixed(0), + ); + + final tempValue = _getValueForStatus( + status, + 'temp_current', + formatter: (value) => value.toStringAsFixed(0), + ); + final pm25Value = _getValueForStatus( + status, + 'pm25_value', + formatter: (value) => value.toString().padLeft(3, '0'), + ); + final pm10Value = _getValueForStatus( + status, + 'pm10', + formatter: (value) => value.toString().padLeft(3, '0'), + ); + final co2Value = _getValueForStatus( + status, + 'co2_value', + formatter: (value) => value.toString().padLeft(4, '0'), + ); + final ch2oValue = _getValueForStatus( + status, + 'ch2o_value', + formatter: (value) => (value / 100).toStringAsFixed(2), + ); + final tvocValue = _getValueForStatus( + status, + 'tvoc_value', + formatter: (value) => (value / 100).toStringAsFixed(2), + ); return Container( decoration: secondarySection.copyWith(boxShadow: const []), @@ -46,34 +78,30 @@ class AqiDeviceInfo extends StatelessWidget { children: [ const AirQualityEndSideLiveIndicator(), AirQualityEndSideGaugeAndInfo( - temperature: humidityValue.toInt(), - humidity: tempValue.toInt(), + temperature: int.parse(tempValue), + humidity: int.parse(humidityValue), ), const SizedBox(height: 20), AqiSubValueWidget( range: (0, 999), label: AqiType.pm25.value, - value: pm25Value < 100 - ? double.parse(pm25Value.toStringAsFixed(1).padLeft(4, '0')) - : pm25Value, + value: pm25Value, unit: AqiType.pm25.unit, ), AqiSubValueWidget( range: (0, 999), label: AqiType.pm10.value, - value: pm10Value < 100 - ? double.parse(pm10Value.toStringAsFixed(1).padLeft(4, '0')) - : pm10Value, + value: pm10Value, unit: AqiType.pm10.unit, ), AqiSubValueWidget( range: (0, 5), label: AqiType.hcho.value, - value: double.parse(ch2oValue.toStringAsFixed(2)), + value: ch2oValue, unit: AqiType.hcho.unit, ), AqiSubValueWidget( - range: (0, 9.99), + range: (0, 999), label: AqiType.tvoc.value, value: tvocValue, unit: AqiType.tvoc.unit, @@ -84,12 +112,6 @@ class AqiDeviceInfo extends StatelessWidget { value: co2Value, unit: AqiType.co2.unit, ), - AqiSubValueWidget( - range: (0, 100), - label: AqiType.c6h6.value, - value: 18, - unit: AqiType.c6h6.unit, - ), ], ), ), diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_sub_value_widget.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_sub_value_widget.dart index 214a4d61..29a597a4 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_sub_value_widget.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_sub_value_widget.dart @@ -17,6 +17,23 @@ class AqiSubValueWidget extends StatelessWidget { required this.range, super.key, }); + + final String label; + final String value; + final String unit; + final (double min, double max) range; + + double get _parsedValue => double.parse(value); + + static const List<_AqiRange> _ranges = [ + _AqiRange(max: 12, color: ColorsManager.goodGreen), + _AqiRange(max: 35, color: ColorsManager.poorOrange), + _AqiRange(max: 55, color: ColorsManager.poorOrange), + _AqiRange(max: 150, color: ColorsManager.unhealthyRed), + _AqiRange(max: 250, color: ColorsManager.severePink), + _AqiRange(max: 500, color: ColorsManager.hazardousPurple), + ]; + static List<_AqiRange> _getRangesForValue((double min, double max) range) { final (double min, double max) = range; final rangeSize = (max - min) / 6; @@ -38,24 +55,9 @@ class AqiSubValueWidget extends StatelessWidget { return ranges.length - 1; } - - final String label; - final double value; - final String unit; - final (double min, double max) range; - - static const List<_AqiRange> _ranges = [ - _AqiRange(max: 12, color: ColorsManager.goodGreen), - _AqiRange(max: 35, color: ColorsManager.poorOrange), - _AqiRange(max: 55, color: ColorsManager.poorOrange), - _AqiRange(max: 150, color: ColorsManager.unhealthyRed), - _AqiRange(max: 250, color: ColorsManager.severePink), - _AqiRange(max: 500, color: ColorsManager.hazardousPurple), - ]; - @override Widget build(BuildContext context) { - final activeSegment = _getActiveSegmentByRange(value, range); + final activeSegment = _getActiveSegmentByRange(_parsedValue, range); return Expanded( child: Container( padding: const EdgeInsetsDirectional.all(10), @@ -86,7 +88,7 @@ class AqiSubValueWidget extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( - value.toString(), + value, style: context.textTheme.titleMedium?.copyWith( color: ColorsManager.blackColor, fontWeight: FontWeight.w400, 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 196f02ce..c725d1fa 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 @@ -8,8 +8,7 @@ enum AqiType { pm10('PM10', 'µg/m³'), hcho('HCHO', 'mg/m³'), tvoc('TVOC', 'µg/m³'), - co2('CO2', 'ppm'), - c6h6('C6H6', 'µg/m³'); + co2('CO2', 'ppm'); const AqiType(this.value, this.unit); From 36ddebb5ae963ba295ce1e2adbbbff1d39d047f8 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 29 May 2025 13:28:44 +0300 Subject: [PATCH 092/181] Implemented new gauge design. --- .../air_quality_end_side_gauge_and_info.dart | 45 +++------ .../air_quality/widgets/aqi_device_info.dart | 9 +- .../air_quality/widgets/aqi_gauge.dart | 97 +++++++------------ .../widgets/aqi_humidity_and_temperature.dart | 41 ++++---- 4 files changed, 78 insertions(+), 114 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_gauge_and_info.dart b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_gauge_and_info.dart index 8722ec68..fb0cac9e 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_gauge_and_info.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_gauge_and_info.dart @@ -1,18 +1,25 @@ import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_gauge.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_humidity_and_temperature.dart'; -import 'package:syncrow_web/utils/color_manager.dart'; -import 'package:syncrow_web/utils/extension/build_context_x.dart'; class AirQualityEndSideGaugeAndInfo extends StatelessWidget { const AirQualityEndSideGaugeAndInfo({ super.key, required this.temperature, required this.humidity, + required this.aqiLevel, }); final int temperature; final int humidity; + final String aqiLevel; + + double get aqi => switch (aqiLevel) { + 'level_1' => 25.0, + 'level_2' => 75.0, + 'level_3' => 125.0, + _ => 0.0, + }; @override Widget build(BuildContext context) { @@ -20,12 +27,12 @@ class AirQualityEndSideGaugeAndInfo extends StatelessWidget { flex: 2, child: Row( children: [ - const Expanded( + Expanded( flex: 2, child: Row( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start, - children: [Expanded(child: AqiGauge(aqi: 200))], + children: [Expanded(child: AqiGauge(aqi: aqi))], ), ), const Spacer(), @@ -33,34 +40,10 @@ class AirQualityEndSideGaugeAndInfo extends StatelessWidget { child: Padding( padding: const EdgeInsetsDirectional.only(end: 20), child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, + // crossAxisAlignment: CrossAxisAlignment.stretch, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, children: [ - const Spacer(), - FittedBox( - fit: BoxFit.contain, - alignment: AlignmentDirectional.centerStart, - child: Text( - 'Air Quality:', - style: context.textTheme.bodySmall?.copyWith( - color: ColorsManager.textPrimaryColor, - fontWeight: FontWeight.w400, - fontSize: 14, - ), - ), - ), - FittedBox( - fit: BoxFit.contain, - alignment: AlignmentDirectional.centerStart, - child: Text( - 'Perfect', - style: context.textTheme.bodySmall?.copyWith( - color: ColorsManager.green, - fontWeight: FontWeight.w400, - fontSize: 30, - ), - ), - ), - const Spacer(), AqiHumidityAndTemperature( temperature: temperature, humidity: humidity, diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_device_info.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_device_info.dart index d9a874bb..f3773c29 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_device_info.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_device_info.dart @@ -41,7 +41,7 @@ class AqiDeviceInfo extends StatelessWidget { final tempValue = _getValueForStatus( status, 'temp_current', - formatter: (value) => value.toStringAsFixed(0), + formatter: (value) => (value / 10).toStringAsFixed(0), ); final pm25Value = _getValueForStatus( status, @@ -78,6 +78,13 @@ class AqiDeviceInfo extends StatelessWidget { children: [ const AirQualityEndSideLiveIndicator(), AirQualityEndSideGaugeAndInfo( + aqiLevel: status + .firstWhere( + (e) => e.code == 'air_quality_index', + orElse: () => Status(code: 'air_quality_index', value: ''), + ) + .value + .toString(), temperature: int.parse(tempValue), humidity: int.parse(humidityValue), ), diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_gauge.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_gauge.dart index 0061f7e8..a94d8cd4 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_gauge.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_gauge.dart @@ -10,41 +10,32 @@ class AqiGauge extends StatelessWidget { static const _minRange = 0.0; static const _goodRange = 50.0; static const _moderateRange = 100.0; - static const _poorRange = 150.0; - static const _unhealthyToSevereRange = 225.0; - static const _maxRange = 300.0; + static const _maxRange = 150.0; - Color _getPointerColor(double value) { - if (value <= _goodRange) { - return ColorsManager.goodGreen; - } else if (value <= _moderateRange) { - return ColorsManager.moderateYellow; - } else if (value <= _poorRange) { - return ColorsManager.poorOrange; - } else { - const unhealthyStart = _poorRange + 1; - if (value <= _unhealthyToSevereRange) { - final t = - (value - unhealthyStart) / (_unhealthyToSevereRange - unhealthyStart); - return Color.lerp( - ColorsManager.unhealthyRed, - ColorsManager.severePink, - t, - )!; - } else { - const severeStart = _unhealthyToSevereRange + 1; - final t = (value - severeStart) / (_maxRange - severeStart); - return Color.lerp( - ColorsManager.severePink, - ColorsManager.hazardousPurple, - t, - )!; - } - } + String _getStatusText(double value) { + return switch (value) { + <= _goodRange => 'Good', + <= _moderateRange => 'Moderate', + _ => 'Poor', + }; } + Color _darkenColor(Color color) => Color.lerp( + color, + Colors.black.withValues(alpha: 0.8), + 0.4, + )!; + + Color _getStatusColor(double value) => switch (value) { + <= _goodRange => ColorsManager.goodGreen, + <= _moderateRange => ColorsManager.moderateYellow, + _ => ColorsManager.poorOrange, + }; + @override Widget build(BuildContext context) { + final status = _getStatusText(aqi); + final statusColor = _getStatusColor(aqi); return AnimatedRadialGauge( value: aqi, debug: false, @@ -60,24 +51,23 @@ class AqiGauge extends StatelessWidget { alignment: AlignmentDirectional.bottomCenter, child: Text.rich( TextSpan( - text: value.toStringAsFixed(0), + text: 'Air Quality\n', style: context.textTheme.bodySmall?.copyWith( color: ColorsManager.textPrimaryColor, - fontWeight: FontWeight.w700, - fontSize: 30, + fontWeight: FontWeight.w400, + fontSize: 12, ), children: [ - const TextSpan( - text: 'AQI', - style: TextStyle( - color: ColorsManager.textPrimaryColor, + TextSpan( + text: status, + style: context.textTheme.bodySmall?.copyWith( + color: _darkenColor(statusColor), fontWeight: FontWeight.w400, - fontSize: 12, + fontSize: 30, ), ), ], ), - overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, ), ), @@ -98,7 +88,7 @@ class AqiGauge extends StatelessWidget { color: ColorsManager.whiteColors, border: GaugePointerBorder( width: 6, - color: _getPointerColor(aqi), + color: statusColor, ), shadow: const BoxShadow( color: ColorsManager.blackColor, @@ -106,41 +96,24 @@ class AqiGauge extends StatelessWidget { offset: Offset(0, 2), ), ), - transformer: const GaugeAxisTransformer.colorFadeIn( - background: ColorsManager.transparentColor, - interval: Interval(0, 0), - ), - segments: [ - const GaugeSegment( + segments: const [ + GaugeSegment( from: _minRange, to: _goodRange, cornerRadius: Radius.circular(16), color: ColorsManager.goodGreen, ), - const GaugeSegment( + GaugeSegment( from: _goodRange + 1, to: _moderateRange, cornerRadius: Radius.circular(16), color: ColorsManager.moderateYellow, ), - const GaugeSegment( + GaugeSegment( from: _moderateRange + 1, - to: _poorRange, - cornerRadius: Radius.circular(16), - color: ColorsManager.poorOrange, - ), - const GaugeSegment( - from: _poorRange + 1, to: _maxRange, cornerRadius: Radius.circular(16), - gradient: GaugeAxisGradient( - colorStops: [0.0, 0.5, 1.0], - colors: [ - ColorsManager.unhealthyRed, - ColorsManager.severePink, - ColorsManager.hazardousPurple, - ], - ), + color: ColorsManager.poorOrange, ), ], ), diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_humidity_and_temperature.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_humidity_and_temperature.dart index 6bea1db9..0d6f4ebf 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_humidity_and_temperature.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_humidity_and_temperature.dart @@ -19,7 +19,6 @@ class AqiHumidityAndTemperature extends StatelessWidget { BlendMode.srcIn, ); - @override Widget build(BuildContext context) { return FittedBox( @@ -29,30 +28,32 @@ class AqiHumidityAndTemperature extends StatelessWidget { style: context.textTheme.bodySmall!.copyWith( color: ColorsManager.textPrimaryColor, fontWeight: FontWeight.w400, - fontSize: 12, + fontSize: 16, ), - child: Row( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - SvgPicture.asset( - Assets.temperatureAqiSidebar, - height: iconSize, - width: iconSize, - colorFilter: colorFilter, - ), - const SizedBox(width: 4), - Text('$temperature°C'), - const SizedBox(width: 10), - SvgPicture.asset( - Assets.humidityAqiSidebar, - height: iconSize, - width: iconSize, - colorFilter: colorFilter, - ), - const SizedBox(width: 4), - Text('$humidity%'), + _buildIconAndValue(Assets.temperatureAqiSidebar, '$temperature°C'), + const SizedBox(height: 10), + _buildIconAndValue(Assets.humidityAqiSidebar, '$humidity%'), ], ), ), ); } + + Widget _buildIconAndValue(String icon, String value) { + return Row( + children: [ + SvgPicture.asset( + icon, + height: iconSize, + width: iconSize, + colorFilter: colorFilter, + ), + const SizedBox(width: 4), + Text(value), + ], + ); + } } From 7bd0c061d4d277e02dfab3154032751e9613ae20 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 29 May 2025 13:29:52 +0300 Subject: [PATCH 093/181] enhanced design of `AqiLocation`. --- .../analytics/modules/air_quality/widgets/aqi_location.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_location.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_location.dart index 3ad7e495..3f1d1f09 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_location.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_location.dart @@ -11,7 +11,10 @@ class AqiLocation extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - decoration: subSectionContainerDecoration.copyWith(boxShadow: const []), + decoration: subSectionContainerDecoration.copyWith( + boxShadow: const [], + borderRadius: BorderRadius.circular(10), + ), padding: const EdgeInsetsDirectional.all(10), child: Row( spacing: 10, From a44d4231f1bd391dc2c6568541cda162f50a2569 Mon Sep 17 00:00:00 2001 From: mohammad Date: Thu, 29 May 2025 14:26:24 +0300 Subject: [PATCH 094/181] 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 bc289a0ddfe1092fa648a12a2457197e4e436c03 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 29 May 2025 14:45:03 +0300 Subject: [PATCH 095/181] removed testing code. --- .../air_quality/views/air_quality_view.dart | 21 +++---------------- 1 file changed, 3 insertions(+), 18 deletions(-) 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 3a236a58..17ecbc22 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,27 +1,12 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.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/range_of_aqi_chart_box.dart'; -import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart'; -class AirQualityView extends StatefulWidget { +class AirQualityView extends StatelessWidget { const AirQualityView({super.key}); static const _padding = EdgeInsetsDirectional.all(32); - @override - State createState() => _AirQualityViewState(); -} - -class _AirQualityViewState extends State { - @override - void initState() { - context.read().add( - const RealtimeDeviceChangesStarted('078e4ed4-8f85-47b8-8c78-5a766549135d'), - ); - super.initState(); - } - @override Widget build(BuildContext context) { return LayoutBuilder( @@ -30,7 +15,7 @@ class _AirQualityViewState extends State { final height = MediaQuery.sizeOf(context).height; if (isMediumOrLess) { return SingleChildScrollView( - padding: AirQualityView._padding, + padding: _padding, child: Column( spacing: 32, children: [ @@ -47,7 +32,7 @@ class _AirQualityViewState extends State { return SingleChildScrollView( child: Container( - padding: AirQualityView._padding, + padding: _padding, height: height * 1.1, child: const Column( children: [ From b95f4063d9d540ee3671cf8d0155e9cc3f3a917b Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 29 May 2025 14:54:53 +0300 Subject: [PATCH 096/181] removed unused widget. --- .../air_quality_end_side_gauge_and_info.dart | 27 +++++++------------ 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_gauge_and_info.dart b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_gauge_and_info.dart index fb0cac9e..93904604 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_gauge_and_info.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_gauge_and_info.dart @@ -14,13 +14,6 @@ class AirQualityEndSideGaugeAndInfo extends StatelessWidget { final int humidity; final String aqiLevel; - double get aqi => switch (aqiLevel) { - 'level_1' => 25.0, - 'level_2' => 75.0, - 'level_3' => 125.0, - _ => 0.0, - }; - @override Widget build(BuildContext context) { return Expanded( @@ -39,16 +32,9 @@ class AirQualityEndSideGaugeAndInfo extends StatelessWidget { Expanded( child: Padding( padding: const EdgeInsetsDirectional.only(end: 20), - child: Column( - // crossAxisAlignment: CrossAxisAlignment.stretch, - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - AqiHumidityAndTemperature( - temperature: temperature, - humidity: humidity, - ), - ], + child: AqiHumidityAndTemperature( + temperature: temperature, + humidity: humidity, ), ), ), @@ -56,4 +42,11 @@ class AirQualityEndSideGaugeAndInfo extends StatelessWidget { ), ); } + + double get aqi => switch (aqiLevel) { + 'level_1' => 25.0, + 'level_2' => 75.0, + 'level_3' => 125.0, + _ => 0.0, + }; } From 3d4c17214c7863e0459856dbaca918ce0a1542ac Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 29 May 2025 14:56:56 +0300 Subject: [PATCH 097/181] Refactored `AqiGauge` to consolidate status text and color logic into a single method, improving code readability and maintainability. --- .../air_quality/widgets/aqi_gauge.dart | 37 ++++++++----------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_gauge.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_gauge.dart index a94d8cd4..fc7f923a 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_gauge.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_gauge.dart @@ -7,35 +7,15 @@ class AqiGauge extends StatelessWidget { const AqiGauge({super.key, required this.aqi}); final double aqi; + static const _minRange = 0.0; static const _goodRange = 50.0; static const _moderateRange = 100.0; static const _maxRange = 150.0; - String _getStatusText(double value) { - return switch (value) { - <= _goodRange => 'Good', - <= _moderateRange => 'Moderate', - _ => 'Poor', - }; - } - - Color _darkenColor(Color color) => Color.lerp( - color, - Colors.black.withValues(alpha: 0.8), - 0.4, - )!; - - Color _getStatusColor(double value) => switch (value) { - <= _goodRange => ColorsManager.goodGreen, - <= _moderateRange => ColorsManager.moderateYellow, - _ => ColorsManager.poorOrange, - }; - @override Widget build(BuildContext context) { - final status = _getStatusText(aqi); - final statusColor = _getStatusColor(aqi); + final (status, statusColor) = _getStatusData(aqi); return AnimatedRadialGauge( value: aqi, debug: false, @@ -119,4 +99,17 @@ class AqiGauge extends StatelessWidget { ), ); } + + (String status, Color color) _getStatusData(double value) { + return switch (value) { + <= _goodRange => ('Good', ColorsManager.goodGreen), + <= _moderateRange => ('Moderate', ColorsManager.moderateYellow), + _ => ('Poor', ColorsManager.poorOrange), + }; + } + + Color _darkenColor(Color color) { + final black = Colors.black.withValues(alpha: 0.8); + return Color.lerp(color, black, 0.4)!; + } } From 5636fbe6c9bc2fd048ae8613f71a2e819d61b413 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 29 May 2025 14:57:26 +0300 Subject: [PATCH 098/181] sorted constructor dependencies. --- .../air_quality/widgets/aqi_humidity_and_temperature.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_humidity_and_temperature.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_humidity_and_temperature.dart index 0d6f4ebf..7726ff32 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_humidity_and_temperature.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_humidity_and_temperature.dart @@ -6,10 +6,11 @@ import 'package:syncrow_web/utils/extension/build_context_x.dart'; class AqiHumidityAndTemperature extends StatelessWidget { const AqiHumidityAndTemperature({ - super.key, required this.temperature, required this.humidity, + super.key, }); + final int temperature; final int humidity; From 283a0dd536c820aee80c0bf1ff6fc94df7fec999 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 29 May 2025 14:59:03 +0300 Subject: [PATCH 099/181] Updated `AqiSubValueWidget` to use minimum value for range calculations, improving accuracy in AQI range display. --- .../air_quality/widgets/aqi_sub_value_widget.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_sub_value_widget.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_sub_value_widget.dart index 29a597a4..5a8e6e6c 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_sub_value_widget.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_sub_value_widget.dart @@ -38,12 +38,12 @@ class AqiSubValueWidget extends StatelessWidget { final (double min, double max) = range; final rangeSize = (max - min) / 6; return [ - _AqiRange(max: range.$1 + rangeSize, color: ColorsManager.goodGreen), - _AqiRange(max: range.$1 + (rangeSize * 2), color: ColorsManager.poorOrange), - _AqiRange(max: range.$1 + (rangeSize * 3), color: ColorsManager.poorOrange), - _AqiRange(max: range.$1 + (rangeSize * 4), color: ColorsManager.unhealthyRed), - _AqiRange(max: range.$1 + (rangeSize * 5), color: ColorsManager.severePink), - _AqiRange(max: range.$2, color: ColorsManager.hazardousPurple), + _AqiRange(max: min + rangeSize, color: ColorsManager.goodGreen), + _AqiRange(max: min + (rangeSize * 2), color: ColorsManager.poorOrange), + _AqiRange(max: min + (rangeSize * 3), color: ColorsManager.poorOrange), + _AqiRange(max: min + (rangeSize * 4), color: ColorsManager.unhealthyRed), + _AqiRange(max: min + (rangeSize * 5), color: ColorsManager.severePink), + _AqiRange(max: min, color: ColorsManager.hazardousPurple), ]; } From 511acc186fd500c304e0d86ed27f5dc2c4bba5c7 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 29 May 2025 15:39:44 +0300 Subject: [PATCH 100/181] Created a param class for loading device location data. --- .../params/get_device_location_data_param.dart | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 lib/pages/analytics/params/get_device_location_data_param.dart diff --git a/lib/pages/analytics/params/get_device_location_data_param.dart b/lib/pages/analytics/params/get_device_location_data_param.dart new file mode 100644 index 00000000..c66af4d8 --- /dev/null +++ b/lib/pages/analytics/params/get_device_location_data_param.dart @@ -0,0 +1,16 @@ +class GetDeviceLocationDataParam { + const GetDeviceLocationDataParam({ + required this.latitude, + required this.longitude, + }); + + final double latitude; + final double longitude; + + Map toJson() { + return { + 'latitude': latitude, + 'longitude': longitude, + }; + } +} From e7476a084d866fb546c3ebe52b92a575c663bbae Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 29 May 2025 15:39:51 +0300 Subject: [PATCH 101/181] Created a model class for loading device location data. --- .../models/device_location_info.dart | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 lib/pages/analytics/models/device_location_info.dart diff --git a/lib/pages/analytics/models/device_location_info.dart b/lib/pages/analytics/models/device_location_info.dart new file mode 100644 index 00000000..9b0095f7 --- /dev/null +++ b/lib/pages/analytics/models/device_location_info.dart @@ -0,0 +1,58 @@ +import 'package:equatable/equatable.dart'; + +class DeviceLocationInfo extends Equatable { + const DeviceLocationInfo({ + this.airQuality, + this.humidity, + this.city, + this.country, + this.address, + this.temperature, + }); + + final double? airQuality; + final double? humidity; + final String? city; + final String? country; + final String? address; + final double? temperature; + + factory DeviceLocationInfo.fromJson(Map json) { + return DeviceLocationInfo( + airQuality: json['airQuality'] as double?, + humidity: json['humidity'] as double?, + city: json['city'] as String?, + country: json['country'] as String?, + address: json['address'] as String?, + temperature: json['temperature'] as double?, + ); + } + + DeviceLocationInfo copyWith({ + double? airQuality, + double? humidity, + String? city, + String? country, + String? address, + double? temperature, + }) { + return DeviceLocationInfo( + airQuality: airQuality ?? this.airQuality, + humidity: humidity ?? this.humidity, + city: city ?? this.city, + country: country ?? this.country, + address: address ?? this.address, + temperature: temperature ?? this.temperature, + ); + } + + @override + List get props => [ + airQuality, + humidity, + city, + country, + address, + temperature, + ]; +} From 6ffb677c33edf1897395577d4073f16732e92843 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 29 May 2025 15:40:15 +0300 Subject: [PATCH 102/181] Created an interface and its fake implementation for loading device location data. --- .../device_location_service.dart | 6 +++++ .../fake_device_location_service.dart | 22 +++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 lib/pages/analytics/services/device_location/device_location_service.dart create mode 100644 lib/pages/analytics/services/device_location/fake_device_location_service.dart diff --git a/lib/pages/analytics/services/device_location/device_location_service.dart b/lib/pages/analytics/services/device_location/device_location_service.dart new file mode 100644 index 00000000..d28b4a7b --- /dev/null +++ b/lib/pages/analytics/services/device_location/device_location_service.dart @@ -0,0 +1,6 @@ +import 'package:syncrow_web/pages/analytics/models/device_location_info.dart'; +import 'package:syncrow_web/pages/analytics/params/get_device_location_data_param.dart'; + +abstract interface class DeviceLocationService { + Future get(GetDeviceLocationDataParam param); +} diff --git a/lib/pages/analytics/services/device_location/fake_device_location_service.dart b/lib/pages/analytics/services/device_location/fake_device_location_service.dart new file mode 100644 index 00000000..c1a4e82f --- /dev/null +++ b/lib/pages/analytics/services/device_location/fake_device_location_service.dart @@ -0,0 +1,22 @@ +import 'package:syncrow_web/pages/analytics/models/device_location_info.dart'; +import 'package:syncrow_web/pages/analytics/params/get_device_location_data_param.dart'; +import 'package:syncrow_web/pages/analytics/services/device_location/device_location_service.dart'; + +class FakeDeviceLocationService implements DeviceLocationService { + const FakeDeviceLocationService(); + + @override + Future get(GetDeviceLocationDataParam param) async { + return await Future.delayed( + const Duration(milliseconds: 500), + () => const DeviceLocationInfo( + airQuality: 45.0, + humidity: 65.0, + city: 'Dubai', + country: 'UAE', + address: 'Business Bay', + temperature: 22.5, + ), + ); + } +} From d92b699a2b01d2a7e305e7e04352ad7ea4a9ce94 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 29 May 2025 15:40:44 +0300 Subject: [PATCH 103/181] Created a bloc for loading and managing the state of device location data. --- .../device_location/device_location_bloc.dart | 50 +++++++++++++++++++ .../device_location_event.dart | 21 ++++++++ .../device_location_state.dart | 18 +++++++ 3 files changed, 89 insertions(+) create mode 100644 lib/pages/analytics/modules/air_quality/blocs/device_location/device_location_bloc.dart create mode 100644 lib/pages/analytics/modules/air_quality/blocs/device_location/device_location_event.dart create mode 100644 lib/pages/analytics/modules/air_quality/blocs/device_location/device_location_state.dart diff --git a/lib/pages/analytics/modules/air_quality/blocs/device_location/device_location_bloc.dart b/lib/pages/analytics/modules/air_quality/blocs/device_location/device_location_bloc.dart new file mode 100644 index 00000000..4f41eb0c --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/blocs/device_location/device_location_bloc.dart @@ -0,0 +1,50 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/analytics/models/device_location_info.dart'; +import 'package:syncrow_web/pages/analytics/params/get_device_location_data_param.dart'; +import 'package:syncrow_web/pages/analytics/services/device_location/device_location_service.dart'; + +part 'device_location_event.dart'; +part 'device_location_state.dart'; + +class DeviceLocationBloc extends Bloc { + DeviceLocationBloc( + this._deviceLocationService, + ) : super(const DeviceLocationState()) { + on(_onLoadDeviceLocation); + on(_onClearDeviceLocation); + } + + final DeviceLocationService _deviceLocationService; + + Future _onLoadDeviceLocation( + LoadDeviceLocationEvent event, + Emitter emit, + ) async { + emit(const DeviceLocationState(status: DeviceLocationStatus.loading)); + + try { + final locationInfo = await _deviceLocationService.get(event.param); + emit( + DeviceLocationState( + status: DeviceLocationStatus.success, + locationInfo: locationInfo, + ), + ); + } catch (e) { + emit( + DeviceLocationState( + status: DeviceLocationStatus.failure, + errorMessage: e.toString(), + ), + ); + } + } + + void _onClearDeviceLocation( + ClearDeviceLocationEvent event, + Emitter emit, + ) { + emit(const DeviceLocationState()); + } +} diff --git a/lib/pages/analytics/modules/air_quality/blocs/device_location/device_location_event.dart b/lib/pages/analytics/modules/air_quality/blocs/device_location/device_location_event.dart new file mode 100644 index 00000000..376d055b --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/blocs/device_location/device_location_event.dart @@ -0,0 +1,21 @@ +part of 'device_location_bloc.dart'; + +sealed class DeviceLocationEvent extends Equatable { + const DeviceLocationEvent(); + + @override + List get props => []; +} + +class LoadDeviceLocationEvent extends DeviceLocationEvent { + const LoadDeviceLocationEvent(this.param); + + final GetDeviceLocationDataParam param; + + @override + List get props => [param]; +} + +class ClearDeviceLocationEvent extends DeviceLocationEvent { + const ClearDeviceLocationEvent(); +} diff --git a/lib/pages/analytics/modules/air_quality/blocs/device_location/device_location_state.dart b/lib/pages/analytics/modules/air_quality/blocs/device_location/device_location_state.dart new file mode 100644 index 00000000..15c681b6 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/blocs/device_location/device_location_state.dart @@ -0,0 +1,18 @@ +part of 'device_location_bloc.dart'; + +enum DeviceLocationStatus { initial, loading, success, failure } + +class DeviceLocationState extends Equatable { + const DeviceLocationState({ + this.status = DeviceLocationStatus.initial, + this.locationInfo, + this.errorMessage, + }); + + final DeviceLocationStatus status; + final DeviceLocationInfo? locationInfo; + final String? errorMessage; + + @override + List get props => [status, locationInfo, errorMessage]; +} From 8ad048e18d95330165b06dfaf136096c82e00817 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 29 May 2025 15:47:24 +0300 Subject: [PATCH 104/181] Added `geocoding: ^4.0.0` package. --- pubspec.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/pubspec.yaml b/pubspec.yaml index 7decc506..612477fc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -61,6 +61,7 @@ dependencies: firebase_crashlytics: ^4.3.2 firebase_database: ^11.3.2 bloc: ^9.0.0 + geocoding: ^4.0.0 dev_dependencies: From b6879035f069467efd24b7ab7069e9e12c9abdc9 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 29 May 2025 15:47:34 +0300 Subject: [PATCH 105/181] Implemented geocoding functionality to retrieve and manage device location data using the newly added `geocoding` package. --- ...ode_device_location_service_decorator.dart | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 lib/pages/analytics/services/device_location/reverse_geocode_device_location_service_decorator.dart diff --git a/lib/pages/analytics/services/device_location/reverse_geocode_device_location_service_decorator.dart b/lib/pages/analytics/services/device_location/reverse_geocode_device_location_service_decorator.dart new file mode 100644 index 00000000..a3ac1e55 --- /dev/null +++ b/lib/pages/analytics/services/device_location/reverse_geocode_device_location_service_decorator.dart @@ -0,0 +1,40 @@ +import 'package:geocoding/geocoding.dart'; +import 'package:syncrow_web/pages/analytics/models/device_location_info.dart'; +import 'package:syncrow_web/pages/analytics/params/get_device_location_data_param.dart'; +import 'package:syncrow_web/pages/analytics/services/device_location/device_location_service.dart'; + +class ReverseGeocodeDeviceLocationServiceDecorator implements DeviceLocationService { + const ReverseGeocodeDeviceLocationServiceDecorator(this._decoratee); + + final DeviceLocationService _decoratee; + + @override + Future get(GetDeviceLocationDataParam param) async { + try { + final deviceLocationInfo = await _decoratee.get(param); + + final placemarks = await placemarkFromCoordinates( + param.latitude, + param.longitude, + ); + + if (placemarks.isNotEmpty) { + final place = placemarks.first; + + final city = place.locality; + final country = place.country; + final address = place.street; + + return deviceLocationInfo.copyWith( + city: city, + country: country, + address: address, + ); + } + + return deviceLocationInfo; + } catch (e) { + throw Exception('Failed to reverse load device location info'); + } + } +} From 5654d66b600555798dc6d7986a80bee4b6096b1d Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 1 Jun 2025 09:51:01 +0300 Subject: [PATCH 106/181] Created a remote implementation for `DeviceLocationService`. --- .env.development | 3 +- .env.production | 3 +- .env.staging | 3 +- .../remote_device_location_service.dart | 88 +++++++++++++++++++ 4 files changed, 94 insertions(+), 3 deletions(-) create mode 100644 lib/pages/analytics/services/device_location/remote_device_location_service.dart diff --git a/.env.development b/.env.development index e77609dc..8b8c7587 100644 --- a/.env.development +++ b/.env.development @@ -1,2 +1,3 @@ ENV_NAME=development -BASE_URL=https://syncrow-dev.azurewebsites.net \ No newline at end of file +BASE_URL=https://syncrow-dev.azurewebsites.net +OPEN_WEATHER_API_KEY=5253339f3f994603cd406b0817823d02 diff --git a/.env.production b/.env.production index 4e9dcb81..73a13524 100644 --- a/.env.production +++ b/.env.production @@ -1,2 +1,3 @@ ENV_NAME=production -BASE_URL=https://syncrow-staging.azurewebsites.net \ No newline at end of file +BASE_URL=https://syncrow-staging.azurewebsites.net +OPEN_WEATHER_API_KEY=5253339f3f994603cd406b0817823d02 \ No newline at end of file diff --git a/.env.staging b/.env.staging index 9565b426..8ab31d93 100644 --- a/.env.staging +++ b/.env.staging @@ -1,2 +1,3 @@ ENV_NAME=staging -BASE_URL=https://syncrow-staging.azurewebsites.net \ No newline at end of file +BASE_URL=https://syncrow-staging.azurewebsites.net +OPEN_WEATHER_API_KEY=5253339f3f994603cd406b0817823d02 \ No newline at end of file diff --git a/lib/pages/analytics/services/device_location/remote_device_location_service.dart b/lib/pages/analytics/services/device_location/remote_device_location_service.dart new file mode 100644 index 00000000..b78be6cc --- /dev/null +++ b/lib/pages/analytics/services/device_location/remote_device_location_service.dart @@ -0,0 +1,88 @@ +import 'package:dio/dio.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:syncrow_web/pages/analytics/models/device_location_info.dart'; +import 'package:syncrow_web/pages/analytics/params/get_device_location_data_param.dart'; +import 'package:syncrow_web/pages/analytics/services/device_location/device_location_service.dart'; + +class RemoteDeviceLocationService implements DeviceLocationService { + const RemoteDeviceLocationService(this._dio); + + final Dio _dio; + static final _openWeatherApiKey = dotenv.env['OPEN_WEATHER_API_KEY']!; + @override + Future get(GetDeviceLocationDataParam param) async { + try { + final results = await Future.wait([ + _getAirQualityData(param), + _getWeatherData(param), + ]); + + final airQuality = results[0] as double?; + final weatherData = results[1] as _WeatherData?; + + return DeviceLocationInfo( + airQuality: airQuality, + temperature: weatherData?.temperature, + humidity: weatherData?.humidity, + ); + } catch (e) { + throw Exception('Failed to fetch location data: $e'); + } + } + + Future _getAirQualityData(GetDeviceLocationDataParam param) async { + final response = await _dio.get( + 'https://api.openweathermap.org/data/2.5/air_pollution', + queryParameters: { + 'lat': param.latitude, + 'lon': param.longitude, + 'appid': _openWeatherApiKey, + }, + ); + + final data = response.data as Map; + final list = data['list'] as List; + if (list.isEmpty) return null; + + final main = list[0]['main'] as Map; + return (main['aqi'] as num).toDouble(); + } + + Future<_WeatherData?> _getWeatherData(GetDeviceLocationDataParam param) async { + final now = DateTime.now(); + final start = DateTime(now.year, now.month, now.day); + final end = DateTime(now.year, now.month, now.day, 23, 59, 59); + try { + final response = await _dio.get( + 'https://api.openweathermap.org/data/2.5/weather', + queryParameters: { + 'lat': param.latitude, + 'lon': param.longitude, + 'start': start.millisecondsSinceEpoch, + 'end': end.millisecondsSinceEpoch, + 'appid': _openWeatherApiKey, + }, + ); + + final data = response.data as Map; + final main = data['main'] as Map; + + return _WeatherData( + temperature: (main['temp'] as num).toDouble(), + humidity: (main['humidity'] as num).toDouble(), + ); + } catch (e) { + return null; + } + } +} + +class _WeatherData { + const _WeatherData({ + required this.temperature, + required this.humidity, + }); + + final double temperature; + final double humidity; +} From 2c4da63266ff35855c490e5a0850c584ca6aa1d5 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 1 Jun 2025 10:50:51 +0300 Subject: [PATCH 107/181] Injected `DeviceLocationBloc` into `AnalyticsPage`. --- .../analytics/views/analytics_page.dart | 10 ++++++++++ .../remote_device_location_service.dart | 19 ++++++++++++++----- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/lib/pages/analytics/modules/analytics/views/analytics_page.dart b/lib/pages/analytics/modules/analytics/views/analytics_page.dart index 68a531c8..ca07c389 100644 --- a/lib/pages/analytics/modules/analytics/views/analytics_page.dart +++ b/lib/pages/analytics/modules/analytics/views/analytics_page.dart @@ -1,5 +1,7 @@ +import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/device_location/device_location_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/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'; @@ -16,6 +18,7 @@ import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy_he 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/device_location/remote_device_location_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/occupacy/remote_occupancy_service.dart'; @@ -101,6 +104,13 @@ class _AnalyticsPageState extends State { FakeRangeOfAqiService(), ), ), + BlocProvider( + create: (context) => DeviceLocationBloc( + RemoteDeviceLocationService( + Dio(BaseOptions(baseUrl: 'https://api.openweathermap.org/data/2.5')), + ), + ), + ), ], child: const AnalyticsPageForm(), ); diff --git a/lib/pages/analytics/services/device_location/remote_device_location_service.dart b/lib/pages/analytics/services/device_location/remote_device_location_service.dart index b78be6cc..707d6c61 100644 --- a/lib/pages/analytics/services/device_location/remote_device_location_service.dart +++ b/lib/pages/analytics/services/device_location/remote_device_location_service.dart @@ -9,6 +9,7 @@ class RemoteDeviceLocationService implements DeviceLocationService { final Dio _dio; static final _openWeatherApiKey = dotenv.env['OPEN_WEATHER_API_KEY']!; + @override Future get(GetDeviceLocationDataParam param) async { try { @@ -31,8 +32,12 @@ class RemoteDeviceLocationService implements DeviceLocationService { } Future _getAirQualityData(GetDeviceLocationDataParam param) async { - final response = await _dio.get( - 'https://api.openweathermap.org/data/2.5/air_pollution', + final response = await _dio.get>( + '/air_pollution/history', + options: Options( + method: 'GET', + responseType: ResponseType.json, + ), queryParameters: { 'lat': param.latitude, 'lon': param.longitude, @@ -40,7 +45,7 @@ class RemoteDeviceLocationService implements DeviceLocationService { }, ); - final data = response.data as Map; + final data = response.data ?? {}; final list = data['list'] as List; if (list.isEmpty) return null; @@ -53,8 +58,12 @@ class RemoteDeviceLocationService implements DeviceLocationService { final start = DateTime(now.year, now.month, now.day); final end = DateTime(now.year, now.month, now.day, 23, 59, 59); try { - final response = await _dio.get( - 'https://api.openweathermap.org/data/2.5/weather', + final response = await _dio.get>( + '/weather', + options: Options( + method: 'GET', + responseType: ResponseType.json, + ), queryParameters: { 'lat': param.latitude, 'lon': param.longitude, From 3d183528c56c55daa12bf506caea1fd4ffa5e00b Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 1 Jun 2025 10:57:49 +0300 Subject: [PATCH 108/181] Fixed thrown exceptions because of `Expanded` widgets. --- .../air_quality/widgets/aqi_device_info.dart | 97 +++++++++---------- .../widgets/aqi_location_info.dart | 36 +++---- 2 files changed, 66 insertions(+), 67 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_device_info.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_device_info.dart index f3773c29..ebe88614 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_device_info.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_device_info.dart @@ -72,55 +72,54 @@ class AqiDeviceInfo extends StatelessWidget { return Container( decoration: secondarySection.copyWith(boxShadow: const []), padding: const EdgeInsetsDirectional.all(20), - child: Expanded( - child: Column( - spacing: 6, - children: [ - const AirQualityEndSideLiveIndicator(), - AirQualityEndSideGaugeAndInfo( - aqiLevel: status - .firstWhere( - (e) => e.code == 'air_quality_index', - orElse: () => Status(code: 'air_quality_index', value: ''), - ) - .value - .toString(), - temperature: int.parse(tempValue), - humidity: int.parse(humidityValue), - ), - const SizedBox(height: 20), - AqiSubValueWidget( - range: (0, 999), - label: AqiType.pm25.value, - value: pm25Value, - unit: AqiType.pm25.unit, - ), - AqiSubValueWidget( - range: (0, 999), - label: AqiType.pm10.value, - value: pm10Value, - unit: AqiType.pm10.unit, - ), - AqiSubValueWidget( - range: (0, 5), - label: AqiType.hcho.value, - value: ch2oValue, - unit: AqiType.hcho.unit, - ), - AqiSubValueWidget( - range: (0, 999), - label: AqiType.tvoc.value, - value: tvocValue, - unit: AqiType.tvoc.unit, - ), - AqiSubValueWidget( - range: (0, 5000), - label: AqiType.co2.value, - value: co2Value, - unit: AqiType.co2.unit, - ), - ], - ), + child: Column( + spacing: 6, + mainAxisSize: MainAxisSize.max, + children: [ + const AirQualityEndSideLiveIndicator(), + AirQualityEndSideGaugeAndInfo( + aqiLevel: status + .firstWhere( + (e) => e.code == 'air_quality_index', + orElse: () => Status(code: 'air_quality_index', value: ''), + ) + .value + .toString(), + temperature: int.parse(tempValue), + humidity: int.parse(humidityValue), + ), + const SizedBox(height: 20), + AqiSubValueWidget( + range: (0, 999), + label: AqiType.pm25.value, + value: pm25Value, + unit: AqiType.pm25.unit, + ), + AqiSubValueWidget( + range: (0, 999), + label: AqiType.pm10.value, + value: pm10Value, + unit: AqiType.pm10.unit, + ), + AqiSubValueWidget( + range: (0, 5), + label: AqiType.hcho.value, + value: ch2oValue, + unit: AqiType.hcho.unit, + ), + AqiSubValueWidget( + range: (0, 999), + label: AqiType.tvoc.value, + value: tvocValue, + unit: AqiType.tvoc.unit, + ), + AqiSubValueWidget( + range: (0, 5000), + label: AqiType.co2.value, + value: co2Value, + unit: AqiType.co2.unit, + ), + ], ), ); }, diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info.dart index f8e087b8..8426328e 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info.dart @@ -18,24 +18,24 @@ class AqiLocationInfo extends StatelessWidget { AqiLocation(), Expanded( child: Row( - spacing: 8, - children: [ - AqiLocationInfoCell( - label: 'Temperature', - value: ' 25°', - svgPath: Assets.aqiTemperature, - ), - AqiLocationInfoCell( - label: 'Humidity', - value: '25%', - svgPath: Assets.aqiHumidity, - ), - AqiLocationInfoCell( - label: 'Air Quality', - value: ' 120', - svgPath: Assets.aqiAirQuality, - ), - ], + spacing: 8, + children: [ + AqiLocationInfoCell( + label: 'Temperature', + value: ' 25°', + svgPath: Assets.aqiTemperature, + ), + AqiLocationInfoCell( + label: 'Humidity', + value: '25%', + svgPath: Assets.aqiHumidity, + ), + AqiLocationInfoCell( + label: 'Air Quality', + value: ' 120', + svgPath: Assets.aqiAirQuality, + ), + ], ), ), ], From 305d695358bfa707bdf7a8bc53885555b761c05a Mon Sep 17 00:00:00 2001 From: mohammad Date: Sun, 1 Jun 2025 13:12:58 +0300 Subject: [PATCH 109/181] 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 110/181] 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 111/181] 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 112/181] 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 113/181] 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 114/181] 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 115/181] 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 116/181] 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 117/181] 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 118/181] 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 119/181] 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 120/181] 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 121/181] 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 122/181] . --- .../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 123/181] 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 124/181] 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 125/181] 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 126/181] 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 127/181] 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 128/181] 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 b0ed84489370ecaaaf7000d8c4e5fb0641e4e2e2 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 1 Jun 2025 14:37:22 +0300 Subject: [PATCH 129/181] made events and state class `final`s, to better document that they shouldn't be extended. --- .../blocs/device_location/device_location_event.dart | 4 ++-- .../blocs/device_location/device_location_state.dart | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/blocs/device_location/device_location_event.dart b/lib/pages/analytics/modules/air_quality/blocs/device_location/device_location_event.dart index 376d055b..37137e4a 100644 --- a/lib/pages/analytics/modules/air_quality/blocs/device_location/device_location_event.dart +++ b/lib/pages/analytics/modules/air_quality/blocs/device_location/device_location_event.dart @@ -7,7 +7,7 @@ sealed class DeviceLocationEvent extends Equatable { List get props => []; } -class LoadDeviceLocationEvent extends DeviceLocationEvent { +final class LoadDeviceLocationEvent extends DeviceLocationEvent { const LoadDeviceLocationEvent(this.param); final GetDeviceLocationDataParam param; @@ -16,6 +16,6 @@ class LoadDeviceLocationEvent extends DeviceLocationEvent { List get props => [param]; } -class ClearDeviceLocationEvent extends DeviceLocationEvent { +final class ClearDeviceLocationEvent extends DeviceLocationEvent { const ClearDeviceLocationEvent(); } diff --git a/lib/pages/analytics/modules/air_quality/blocs/device_location/device_location_state.dart b/lib/pages/analytics/modules/air_quality/blocs/device_location/device_location_state.dart index 15c681b6..8f66ad28 100644 --- a/lib/pages/analytics/modules/air_quality/blocs/device_location/device_location_state.dart +++ b/lib/pages/analytics/modules/air_quality/blocs/device_location/device_location_state.dart @@ -2,7 +2,7 @@ part of 'device_location_bloc.dart'; enum DeviceLocationStatus { initial, loading, success, failure } -class DeviceLocationState extends Equatable { +final class DeviceLocationState extends Equatable { const DeviceLocationState({ this.status = DeviceLocationStatus.initial, this.locationInfo, From 78f42dacf61ed0e5adf209d05d2fe43ae936d8e7 Mon Sep 17 00:00:00 2001 From: mohammad Date: Sun, 1 Jun 2025 14:37:42 +0300 Subject: [PATCH 130/181] 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 131/181] 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 132/181] 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 133/181] 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 134/181] 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 135/181] 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 136/181] 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 137/181] 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 138/181] 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 139/181] 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 140/181] 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 141/181] 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 142/181] 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 143/181] 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 144/181] 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 145/181] 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 146/181] 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 147/181] 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 148/181] 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 149/181] 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 150/181] 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 151/181] 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 152/181] 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 153/181] 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 154/181] 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 155/181] 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 156/181] 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 157/181] 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 158/181] 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 159/181] 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 160/181] 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 161/181] 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 162/181] 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 163/181] 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 164/181] 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 165/181] 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]; } - From bcb6e49a015a695db5411b4593286aca68e3e461 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 4 Jun 2025 09:17:38 +0300 Subject: [PATCH 166/181] Deleted `FakeDeviceLocationService` class, since it is no longer needed. --- .../fake_device_location_service.dart | 22 ------------------- 1 file changed, 22 deletions(-) delete mode 100644 lib/pages/analytics/services/device_location/fake_device_location_service.dart diff --git a/lib/pages/analytics/services/device_location/fake_device_location_service.dart b/lib/pages/analytics/services/device_location/fake_device_location_service.dart deleted file mode 100644 index c1a4e82f..00000000 --- a/lib/pages/analytics/services/device_location/fake_device_location_service.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:syncrow_web/pages/analytics/models/device_location_info.dart'; -import 'package:syncrow_web/pages/analytics/params/get_device_location_data_param.dart'; -import 'package:syncrow_web/pages/analytics/services/device_location/device_location_service.dart'; - -class FakeDeviceLocationService implements DeviceLocationService { - const FakeDeviceLocationService(); - - @override - Future get(GetDeviceLocationDataParam param) async { - return await Future.delayed( - const Duration(milliseconds: 500), - () => const DeviceLocationInfo( - airQuality: 45.0, - humidity: 65.0, - city: 'Dubai', - country: 'UAE', - address: 'Business Bay', - temperature: 22.5, - ), - ); - } -} From 8d999f118c458878ae37d398c414106b289a94e0 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 4 Jun 2025 09:18:28 +0300 Subject: [PATCH 167/181] Connected `RemoteDeviceLocationService` to the new BE API, instead of directly fetching the data from OpenWeather Api's. --- .../remote_device_location_service.dart | 97 +++---------------- 1 file changed, 16 insertions(+), 81 deletions(-) diff --git a/lib/pages/analytics/services/device_location/remote_device_location_service.dart b/lib/pages/analytics/services/device_location/remote_device_location_service.dart index 707d6c61..dce547a2 100644 --- a/lib/pages/analytics/services/device_location/remote_device_location_service.dart +++ b/lib/pages/analytics/services/device_location/remote_device_location_service.dart @@ -1,97 +1,32 @@ import 'package:dio/dio.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:syncrow_web/pages/analytics/models/device_location_info.dart'; import 'package:syncrow_web/pages/analytics/params/get_device_location_data_param.dart'; import 'package:syncrow_web/pages/analytics/services/device_location/device_location_service.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; class RemoteDeviceLocationService implements DeviceLocationService { - const RemoteDeviceLocationService(this._dio); + const RemoteDeviceLocationService(this._httpService); - final Dio _dio; - static final _openWeatherApiKey = dotenv.env['OPEN_WEATHER_API_KEY']!; + final HTTPService _httpService; + + static const _defaultErrorMessage = 'Failed to load device location'; @override Future get(GetDeviceLocationDataParam param) async { try { - final results = await Future.wait([ - _getAirQualityData(param), - _getWeatherData(param), - ]); - - final airQuality = results[0] as double?; - final weatherData = results[1] as _WeatherData?; - - return DeviceLocationInfo( - airQuality: airQuality, - temperature: weatherData?.temperature, - humidity: weatherData?.humidity, + final response = await _httpService.get( + path: '/weather', + queryParameters: param.toJson(), + expectedResponseModel: (data) => DeviceLocationInfo.fromJson(data), ); + return response; + } on DioException catch (e) { + final message = e.response?.data as Map?; + final error = message?['error'] as Map?; + final errorMessage = error?['error'] as String? ?? ''; + throw Exception(errorMessage); } catch (e) { - throw Exception('Failed to fetch location data: $e'); - } - } - - Future _getAirQualityData(GetDeviceLocationDataParam param) async { - final response = await _dio.get>( - '/air_pollution/history', - options: Options( - method: 'GET', - responseType: ResponseType.json, - ), - queryParameters: { - 'lat': param.latitude, - 'lon': param.longitude, - 'appid': _openWeatherApiKey, - }, - ); - - final data = response.data ?? {}; - final list = data['list'] as List; - if (list.isEmpty) return null; - - final main = list[0]['main'] as Map; - return (main['aqi'] as num).toDouble(); - } - - Future<_WeatherData?> _getWeatherData(GetDeviceLocationDataParam param) async { - final now = DateTime.now(); - final start = DateTime(now.year, now.month, now.day); - final end = DateTime(now.year, now.month, now.day, 23, 59, 59); - try { - final response = await _dio.get>( - '/weather', - options: Options( - method: 'GET', - responseType: ResponseType.json, - ), - queryParameters: { - 'lat': param.latitude, - 'lon': param.longitude, - 'start': start.millisecondsSinceEpoch, - 'end': end.millisecondsSinceEpoch, - 'appid': _openWeatherApiKey, - }, - ); - - final data = response.data as Map; - final main = data['main'] as Map; - - return _WeatherData( - temperature: (main['temp'] as num).toDouble(), - humidity: (main['humidity'] as num).toDouble(), - ); - } catch (e) { - return null; + throw Exception('$_defaultErrorMessage: $e'); } } } - -class _WeatherData { - const _WeatherData({ - required this.temperature, - required this.humidity, - }); - - final double temperature; - final double humidity; -} From 8e8fdf0fc69a1ae5e7468fcf5895447d68746c0f Mon Sep 17 00:00:00 2001 From: mohammad Date: Wed, 4 Jun 2025 09:26:56 +0300 Subject: [PATCH 168/181] Rename dialog buttons for clarity: 'Cancel' to 'Back' and 'Confirm' to 'Save' --- lib/pages/routines/helper/save_routine_helper.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pages/routines/helper/save_routine_helper.dart b/lib/pages/routines/helper/save_routine_helper.dart index e2066374..f8b52dab 100644 --- a/lib/pages/routines/helper/save_routine_helper.dart +++ b/lib/pages/routines/helper/save_routine_helper.dart @@ -109,12 +109,12 @@ class SaveRoutineHelper { spacing: 16, mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - DialogFooterButton( - text: 'Cancel', + DialogFooterButton( + text: 'Back', onTap: () => Navigator.pop(context), ), DialogFooterButton( - text: 'Confirm', + text: 'Save', onTap: () { if (state.isAutomation) { if (state.isUpdate ?? false) { From e48fc8b82c7bf130f49899e54d23506b7056bf37 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 4 Jun 2025 09:27:21 +0300 Subject: [PATCH 169/181] loads and clears `DeviceLocationBloc`. --- .../helpers/fetch_air_quality_data_helper.dart | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart b/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart index 65e62365..aaffc3fd 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,11 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/device_location/device_location_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart'; import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart'; import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart'; +import 'package:syncrow_web/pages/analytics/params/get_device_location_data_param.dart'; import 'package:syncrow_web/pages/analytics/params/get_range_of_aqi_param.dart'; abstract final class FetchAirQualityDataHelper { @@ -39,6 +41,8 @@ abstract final class FetchAirQualityDataHelper { ); context.read().add(const ClearRangeOfAqiEvent()); + + context.read().add(const ClearDeviceLocationEvent()); } static void loadAnalyticsDevices( @@ -58,6 +62,15 @@ abstract final class FetchAirQualityDataHelper { context.read() ..add(const RealtimeDeviceChangesClosed()) ..add(RealtimeDeviceChangesStarted(device.uuid)); + + context.read().add( + const LoadDeviceLocationEvent( + GetDeviceLocationDataParam( + latitude: 35.6895, + longitude: 139.6917, + ), + ), + ); }, ), ); From 25a55ad82033ff6c1787bfe4c83c88478a1213b4 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 4 Jun 2025 09:27:46 +0300 Subject: [PATCH 170/181] made `GetDeviceLocationDataParam.toJson` method have the correct keys for the API. --- .../analytics/params/get_device_location_data_param.dart | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/lib/pages/analytics/params/get_device_location_data_param.dart b/lib/pages/analytics/params/get_device_location_data_param.dart index c66af4d8..29427d10 100644 --- a/lib/pages/analytics/params/get_device_location_data_param.dart +++ b/lib/pages/analytics/params/get_device_location_data_param.dart @@ -7,10 +7,5 @@ class GetDeviceLocationDataParam { final double latitude; final double longitude; - Map toJson() { - return { - 'latitude': latitude, - 'longitude': longitude, - }; - } + Map toJson() => {'lat': latitude, 'lon': longitude}; } From 1edeb664aa95249021a61f8a13c7725db904467a Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 4 Jun 2025 09:28:16 +0300 Subject: [PATCH 171/181] Connected data coming from `DeviceLocationBloc` into the respective widgets. --- .../air_quality/widgets/aqi_location.dart | 31 +++++++++- .../widgets/aqi_location_info.dart | 61 +++++++++++-------- 2 files changed, 65 insertions(+), 27 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_location.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_location.dart index 3f1d1f09..2503874f 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_location.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_location.dart @@ -6,7 +6,34 @@ import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/style.dart'; class AqiLocation extends StatelessWidget { - const AqiLocation({super.key}); + const AqiLocation({ + required this.city, + required this.country, + required this.address, + super.key, + }); + + final String? city; + final String? country; + final String? address; + + String _getFormattedLocation() { + if (city == null && country == null && address == null) { + return 'N/A'; + } + + final parts = []; + + if (city != null) parts.add(city!); + if (address != null) parts.add(address!); + final locationPart = parts.join(', '); + + if (country != null) { + return locationPart.isEmpty ? country! : '$locationPart - $country'; + } + + return locationPart; + } @override Widget build(BuildContext context) { @@ -24,7 +51,7 @@ class AqiLocation extends StatelessWidget { _buildLocationPin(), Expanded( child: Text( - 'Business Bay, Dubai - UAE', + _getFormattedLocation(), style: context.textTheme.bodySmall?.copyWith( color: ColorsManager.textPrimaryColor, fontWeight: FontWeight.w400, diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info.dart index 8426328e..983f76b2 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info.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/device_location/device_location_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_location.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_location_info_cell.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; @@ -9,37 +11,46 @@ class AqiLocationInfo extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - decoration: secondarySection.copyWith(boxShadow: const []), - padding: const EdgeInsetsDirectional.all(20), - child: const Column( - spacing: 8, - children: [ - AqiLocation(), - Expanded( - child: Row( + return BlocBuilder( + builder: (context, state) { + final info = state.locationInfo; + return Container( + decoration: secondarySection.copyWith(boxShadow: const []), + padding: const EdgeInsetsDirectional.all(20), + child: Column( spacing: 8, children: [ - AqiLocationInfoCell( - label: 'Temperature', - value: ' 25°', - svgPath: Assets.aqiTemperature, + AqiLocation( + city: info?.city, + country: info?.country, + address: info?.address, ), - AqiLocationInfoCell( - label: 'Humidity', - value: '25%', - svgPath: Assets.aqiHumidity, - ), - AqiLocationInfoCell( - label: 'Air Quality', - value: ' 120', - svgPath: Assets.aqiAirQuality, + Expanded( + child: Row( + spacing: 8, + children: [ + AqiLocationInfoCell( + label: 'Temperature', + value: ' ${info?.temperature?.roundToDouble() ?? '--'}°', + svgPath: Assets.aqiTemperature, + ), + AqiLocationInfoCell( + label: 'Humidity', + value: '${info?.humidity?.roundToDouble() ?? '--'}%', + svgPath: Assets.aqiHumidity, + ), + AqiLocationInfoCell( + label: 'Air Quality', + value: ' ${info?.airQuality?.roundToDouble() ?? '--'}', + svgPath: Assets.aqiAirQuality, + ), + ], + ), ), ], - ), ), - ], - ), + ); + }, ); } } From e2c44ba85fdeabbd62018ca716937d43bcf6a1dc Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 4 Jun 2025 09:28:50 +0300 Subject: [PATCH 172/181] injected the remote and reverse geocoder dependenies into `DeviceLocationBloc`. --- .../analytics/modules/analytics/views/analytics_page.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pages/analytics/modules/analytics/views/analytics_page.dart b/lib/pages/analytics/modules/analytics/views/analytics_page.dart index ca07c389..0a45ba8d 100644 --- a/lib/pages/analytics/modules/analytics/views/analytics_page.dart +++ b/lib/pages/analytics/modules/analytics/views/analytics_page.dart @@ -1,4 +1,3 @@ -import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/device_location/device_location_bloc.dart'; @@ -19,6 +18,7 @@ import 'package:syncrow_web/pages/analytics/services/analytics_devices/analytics 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/device_location/remote_device_location_service.dart'; +import 'package:syncrow_web/pages/analytics/services/device_location/reverse_geocode_device_location_service_decorator.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/occupacy/remote_occupancy_service.dart'; @@ -106,8 +106,8 @@ class _AnalyticsPageState extends State { ), BlocProvider( create: (context) => DeviceLocationBloc( - RemoteDeviceLocationService( - Dio(BaseOptions(baseUrl: 'https://api.openweathermap.org/data/2.5')), + ReverseGeocodeDeviceLocationServiceDecorator( + RemoteDeviceLocationService(_httpService), ), ), ), From 651ac6785eb0afdf3f054c1bf0943a1d819bc971 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 4 Jun 2025 09:32:56 +0300 Subject: [PATCH 173/181] removed open weather api keys from `.env` files. --- .env.development | 3 +-- .env.production | 3 +-- .env.staging | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.env.development b/.env.development index 8b8c7587..e77609dc 100644 --- a/.env.development +++ b/.env.development @@ -1,3 +1,2 @@ ENV_NAME=development -BASE_URL=https://syncrow-dev.azurewebsites.net -OPEN_WEATHER_API_KEY=5253339f3f994603cd406b0817823d02 +BASE_URL=https://syncrow-dev.azurewebsites.net \ No newline at end of file diff --git a/.env.production b/.env.production index 73a13524..4e9dcb81 100644 --- a/.env.production +++ b/.env.production @@ -1,3 +1,2 @@ ENV_NAME=production -BASE_URL=https://syncrow-staging.azurewebsites.net -OPEN_WEATHER_API_KEY=5253339f3f994603cd406b0817823d02 \ No newline at end of file +BASE_URL=https://syncrow-staging.azurewebsites.net \ No newline at end of file diff --git a/.env.staging b/.env.staging index 8ab31d93..9565b426 100644 --- a/.env.staging +++ b/.env.staging @@ -1,3 +1,2 @@ ENV_NAME=staging -BASE_URL=https://syncrow-staging.azurewebsites.net -OPEN_WEATHER_API_KEY=5253339f3f994603cd406b0817823d02 \ No newline at end of file +BASE_URL=https://syncrow-staging.azurewebsites.net \ No newline at end of file From 79b974ee6cbe953096d11852d775a8f2f9696fb9 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 4 Jun 2025 09:36:41 +0300 Subject: [PATCH 174/181] re-injected `AirQualityDistributionBloc` into `AnalyticsPage`. --- .../analytics/modules/analytics/views/analytics_page.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/pages/analytics/modules/analytics/views/analytics_page.dart b/lib/pages/analytics/modules/analytics/views/analytics_page.dart index 0a45ba8d..01dc8ef5 100644 --- a/lib/pages/analytics/modules/analytics/views/analytics_page.dart +++ b/lib/pages/analytics/modules/analytics/views/analytics_page.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/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'; 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'; @@ -14,6 +15,7 @@ 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'; @@ -104,6 +106,11 @@ class _AnalyticsPageState extends State { FakeRangeOfAqiService(), ), ), + BlocProvider( + create: (context) => AirQualityDistributionBloc( + FakeAirQualityDistributionService(), + ), + ), BlocProvider( create: (context) => DeviceLocationBloc( ReverseGeocodeDeviceLocationServiceDecorator( From 5f8eb9de0652223f130c14cd901a1e0eb82d415e Mon Sep 17 00:00:00 2001 From: mohammad Date: Wed, 4 Jun 2025 10:32:31 +0300 Subject: [PATCH 175/181] Add settings button SVG and refactor settings icon implementation in dynamic table --- assets/icons/settings_button.svg | 24 +++++ lib/pages/common/custom_table.dart | 135 +++++++++++++++++++---------- lib/utils/constants/assets.dart | 108 +++++++++++++++-------- 3 files changed, 186 insertions(+), 81 deletions(-) create mode 100644 assets/icons/settings_button.svg diff --git a/assets/icons/settings_button.svg b/assets/icons/settings_button.svg new file mode 100644 index 00000000..43cad368 --- /dev/null +++ b/assets/icons/settings_button.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/pages/common/custom_table.dart b/lib/pages/common/custom_table.dart index 0abe075b..f23daa45 100644 --- a/lib/pages/common/custom_table.dart +++ b/lib/pages/common/custom_table.dart @@ -162,31 +162,34 @@ class _DynamicTableState extends State { child: SingleChildScrollView( scrollDirection: Axis.horizontal, controller: _horizontalBodyScrollController, - child: SizedBox( - width: widget.size.width, - child: widget.isEmpty - ? _buildEmptyState() - : Column( - children: - List.generate(widget.data.length, (rowIndex) { - final row = widget.data[rowIndex]; - return Row( - children: [ - if (widget.withCheckBox) - _buildRowCheckbox( - rowIndex, widget.size.height * 0.08), - ...row.asMap().entries.map((entry) { - return _buildTableCell( - entry.value.toString(), - widget.size.height * 0.08, - rowIndex: rowIndex, - columnIndex: entry.key, - ); - }).toList(), - ], - ); - }), - ), + child: Container( + color: ColorsManager.whiteColors, + child: SizedBox( + width: widget.size.width, + child: widget.isEmpty + ? _buildEmptyState() + : Column( + children: List.generate(widget.data.length, + (rowIndex) { + final row = widget.data[rowIndex]; + return Row( + children: [ + if (widget.withCheckBox) + _buildRowCheckbox(rowIndex, + widget.size.height * 0.08), + ...row.asMap().entries.map((entry) { + return _buildTableCell( + entry.value.toString(), + widget.size.height * 0.08, + rowIndex: rowIndex, + columnIndex: entry.key, + ); + }).toList(), + ], + ); + }), + ), + ), ), ), ), @@ -211,7 +214,6 @@ class _DynamicTableState extends State { onChanged: widget.withSelectAll && widget.data.isNotEmpty ? _toggleSelectAll : null, - ), ); } @@ -282,7 +284,6 @@ 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( @@ -303,7 +304,6 @@ class _DynamicTableState extends State { required int rowIndex, required int columnIndex, }) { - bool isBatteryLevel = content.endsWith('%'); double? batteryLevel; @@ -313,9 +313,13 @@ class _DynamicTableState extends State { bool isSettingsColumn = widget.headers[columnIndex] == 'Settings'; if (isSettingsColumn) { - return _buildSettingsIcon(rowIndex, size); + return buildSettingsIcon( + width: 120, + height: 60, + iconSize: 40, + onTap: () => widget.onSettingsPressed?.call(rowIndex), + ); } - Color? statusColor; switch (content) { @@ -368,22 +372,63 @@ class _DynamicTableState extends State { ); } - Widget _buildSettingsIcon(int rowIndex, double size) { - return Container( - height: size, - width: 120, - padding: const EdgeInsets.all(5.0), - decoration: const BoxDecoration( - border: Border( - bottom: BorderSide(color: ColorsManager.boxDivider, width: 1.0), + Widget buildSettingsIcon( + {double width = 120, + double height = 60, + double iconSize = 40, + VoidCallback? onTap}) { + return Column( + children: [ + Container( + padding: const EdgeInsets.only(top: 10, bottom: 15, left: 10), + margin: const EdgeInsets.only(right: 15), + decoration: const BoxDecoration( + color: ColorsManager.whiteColors, + border: Border( + bottom: BorderSide( + color: ColorsManager.boxDivider, + width: 1.0, + ), + ), + ), + width: width, + child: Padding( + padding: const EdgeInsets.only( + right: 16.0, + left: 17.0, + ), + child: Container( + width: 50, + decoration: BoxDecoration( + color: const Color(0xFFF7F8FA), + borderRadius: BorderRadius.circular(height / 2), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.17), + blurRadius: 14, + offset: const Offset(0, 4), + ), + ], + ), + child: InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Center( + child: SvgPicture.asset( + Assets.settings, // ضع المسار الصحيح هنا + width: 40, + height: 22, + color: ColorsManager + .primaryColor, // نفس لون الأيقونة في الصورة + ), + ), + ), + ), + ), + ), ), - color: Colors.white, - ), - alignment: Alignment.center, - child: IconButton( - icon: SvgPicture.asset(Assets.settings), - onPressed: () => widget.onSettingsPressed?.call(rowIndex), - ), + ], ); } } diff --git a/lib/utils/constants/assets.dart b/lib/utils/constants/assets.dart index 5eb0eb05..dfc0b394 100644 --- a/lib/utils/constants/assets.dart +++ b/lib/utils/constants/assets.dart @@ -14,7 +14,8 @@ class Assets { static const String rightLine = "assets/images/right_line.png"; static const String google = "assets/images/google.svg"; static const String facebook = "assets/images/facebook.svg"; - static const String invisiblePassword = "assets/images/Password_invisible.svg"; + static const String invisiblePassword = + "assets/images/Password_invisible.svg"; static const String visiblePassword = "assets/images/password_visible.svg"; static const String accessIcon = "assets/images/access_icon.svg"; static const String spaseManagementIcon = @@ -33,7 +34,8 @@ class Assets { static const String emptyTable = "assets/images/empty_table.svg"; // General assets - static const String motionlessDetection = "assets/icons/motionless_detection.svg"; + static const String motionlessDetection = + "assets/icons/motionless_detection.svg"; static const String acHeating = "assets/icons/ac_heating.svg"; static const String acPowerOff = "assets/icons/ac_power_off.svg"; static const String acFanMiddle = "assets/icons/ac_fan_middle.svg"; @@ -70,19 +72,22 @@ class Assets { "assets/icons/automation_functions/temp_password_unlock.svg"; static const String doorlockNormalOpen = "assets/icons/automation_functions/doorlock_normal_open.svg"; - static const String doorbell = "assets/icons/automation_functions/doorbell.svg"; + static const String doorbell = + "assets/icons/automation_functions/doorbell.svg"; static const String remoteUnlockViaApp = "assets/icons/automation_functions/remote_unlock_via_app.svg"; static const String doubleLock = "assets/icons/automation_functions/double_lock.svg"; static const String selfTestResult = "assets/icons/automation_functions/self_test_result.svg"; - static const String lockAlarm = "assets/icons/automation_functions/lock_alarm.svg"; + static const String lockAlarm = + "assets/icons/automation_functions/lock_alarm.svg"; static const String presenceState = "assets/icons/automation_functions/presence_state.svg"; static const String currentTemp = "assets/icons/automation_functions/current_temp.svg"; - static const String presence = "assets/icons/automation_functions/presence.svg"; + static const String presence = + "assets/icons/automation_functions/presence.svg"; static const String residualElectricity = "assets/icons/automation_functions/residual_electricity.svg"; static const String hijackAlarm = @@ -99,12 +104,15 @@ class Assets { // Presence Sensor Assets static const String sensorMotionIcon = "assets/icons/sensor_motion_ic.svg"; - static const String sensorPresenceIcon = "assets/icons/sensor_presence_ic.svg"; + static const String sensorPresenceIcon = + "assets/icons/sensor_presence_ic.svg"; static const String sensorVacantIcon = "assets/icons/sensor_vacant_ic.svg"; static const String illuminanceRecordIcon = "assets/icons/illuminance_record_ic.svg"; - static const String presenceRecordIcon = "assets/icons/presence_record_ic.svg"; - static const String helpDescriptionIcon = "assets/icons/help_description_ic.svg"; + static const String presenceRecordIcon = + "assets/icons/presence_record_ic.svg"; + static const String helpDescriptionIcon = + "assets/icons/help_description_ic.svg"; static const String lightPulp = "assets/icons/light_pulb.svg"; static const String acDevice = "assets/icons/ac_device.svg"; @@ -154,10 +162,12 @@ class Assets { static const String unit = 'assets/icons/unit_icon.svg'; static const String villa = 'assets/icons/villa_icon.svg'; static const String iconEdit = 'assets/icons/icon_edit_icon.svg'; - static const String textFieldSearch = 'assets/icons/textfield_search_icon.svg'; + static const String textFieldSearch = + 'assets/icons/textfield_search_icon.svg'; static const String roundedAddIcon = 'assets/icons/rounded_add_icon.svg'; static const String addIcon = 'assets/icons/add_icon.svg'; - static const String smartThermostatIcon = 'assets/icons/smart_thermostat_icon.svg'; + static const String smartThermostatIcon = + 'assets/icons/smart_thermostat_icon.svg'; static const String smartLightIcon = 'assets/icons/smart_light_icon.svg'; static const String presenceSensor = 'assets/icons/presence_sensor.svg'; static const String Gang3SwitchIcon = 'assets/icons/3_Gang_switch_icon.svg'; @@ -205,7 +215,8 @@ class Assets { //assets/icons/water_leak_normal.svg static const String waterLeakNormal = 'assets/icons/water_leak_normal.svg'; //assets/icons/water_leak_detected.svg - static const String waterLeakDetected = 'assets/icons/water_leak_detected.svg'; + static const String waterLeakDetected = + 'assets/icons/water_leak_detected.svg'; //assets/icons/automation_records.svg static const String automationRecords = 'assets/icons/automation_records.svg'; @@ -276,13 +287,16 @@ class Assets { "assets/icons/functions_icons/sensitivity.svg"; static const String assetsSensitivityOperationIcon = "assets/icons/functions_icons/sesitivity_operation_icon.svg"; - static const String assetsAcPower = "assets/icons/functions_icons/ac_power.svg"; + static const String assetsAcPower = + "assets/icons/functions_icons/ac_power.svg"; static const String assetsAcPowerOFF = "assets/icons/functions_icons/ac_power_off.svg"; static const String assetsChildLock = "assets/icons/functions_icons/child_lock.svg"; - static const String assetsFreezing = "assets/icons/functions_icons/freezing.svg"; - static const String assetsFanSpeed = "assets/icons/functions_icons/fan_speed.svg"; + static const String assetsFreezing = + "assets/icons/functions_icons/freezing.svg"; + static const String assetsFanSpeed = + "assets/icons/functions_icons/fan_speed.svg"; static const String assetsAcCooling = "assets/icons/functions_icons/ac_cooling.svg"; static const String assetsAcHeating = @@ -291,7 +305,8 @@ class Assets { "assets/icons/functions_icons/celsius_degrees.svg"; static const String assetsTempreture = "assets/icons/functions_icons/tempreture.svg"; - static const String assetsAcFanLow = "assets/icons/functions_icons/ac_fan_low.svg"; + static const String assetsAcFanLow = + "assets/icons/functions_icons/ac_fan_low.svg"; static const String assetsAcFanMiddle = "assets/icons/functions_icons/ac_fan_middle.svg"; static const String assetsAcFanHigh = @@ -310,7 +325,8 @@ class Assets { "assets/icons/functions_icons/far_detection.svg"; static const String assetsFarDetectionFunction = "assets/icons/functions_icons/far_detection_function.svg"; - static const String assetsIndicator = "assets/icons/functions_icons/indicator.svg"; + static const String assetsIndicator = + "assets/icons/functions_icons/indicator.svg"; static const String assetsMotionDetection = "assets/icons/functions_icons/motion_detection.svg"; static const String assetsMotionlessDetection = @@ -323,7 +339,8 @@ class Assets { "assets/icons/functions_icons/master_state.svg"; static const String assetsSwitchAlarmSound = "assets/icons/functions_icons/switch_alarm_sound.svg"; - static const String assetsResetOff = "assets/icons/functions_icons/reset_off.svg"; + static const String assetsResetOff = + "assets/icons/functions_icons/reset_off.svg"; // Assets for automation_functions static const String assetsCardUnlock = @@ -367,12 +384,14 @@ class Assets { static const String activeUser = 'assets/icons/active_user.svg'; static const String deActiveUser = 'assets/icons/deactive_user.svg'; static const String invitedIcon = 'assets/icons/invited_icon.svg'; - static const String rectangleCheckBox = 'assets/icons/rectangle_check_box.png'; + static const String rectangleCheckBox = + 'assets/icons/rectangle_check_box.png'; static const String CheckBoxChecked = 'assets/icons/box_checked.png'; static const String emptyBox = 'assets/icons/empty_box.png'; static const String completeProcessIcon = 'assets/icons/compleate_process_icon.svg'; - static const String currentProcessIcon = 'assets/icons/current_process_icon.svg'; + static const String currentProcessIcon = + 'assets/icons/current_process_icon.svg'; static const String uncomplete_ProcessIcon = 'assets/icons/uncompleate_process_icon.svg'; static const String wrongProcessIcon = 'assets/icons/wrong_process_icon.svg'; @@ -393,9 +412,11 @@ class Assets { static const String successIcon = 'assets/icons/success_icon.svg'; static const String spaceLocationIcon = 'assets/icons/spaseLocationIcon.svg'; static const String scenesPlayIcon = 'assets/icons/scenesPlayIcon.png'; - static const String scenesPlayIconCheck = 'assets/icons/scenesPlayIconCheck.png'; + static const String scenesPlayIconCheck = + 'assets/icons/scenesPlayIconCheck.png'; static const String presenceStateIcon = 'assets/icons/presence_state.svg'; - static const String currentDistanceIcon = 'assets/icons/current_distance_icon.svg'; + static const String currentDistanceIcon = + 'assets/icons/current_distance_icon.svg'; static const String farDetectionIcon = 'assets/icons/far_detection_icon.svg'; static const String motionDetectionSensitivityIcon = @@ -418,29 +439,44 @@ class Assets { static const String cpsMode4 = 'assets/icons/cps_mode4.svg'; static const String closeToMotion = 'assets/icons/close_to_motion.svg'; static const String farAwayMotion = 'assets/icons/far_away_motion.svg'; - static const String communicationFault = 'assets/icons/communication_fault.svg'; + static const String communicationFault = + 'assets/icons/communication_fault.svg'; static const String radarFault = 'assets/icons/radar_fault.svg'; - static const String selfTestingSuccess = 'assets/icons/self_testing_success.svg'; - static const String selfTestingFailure = 'assets/icons/self_testing_failure.svg'; - static const String selfTestingTimeout = 'assets/icons/self_testing_timeout.svg'; + static const String selfTestingSuccess = + 'assets/icons/self_testing_success.svg'; + static const String selfTestingFailure = + 'assets/icons/self_testing_failure.svg'; + static const String selfTestingTimeout = + 'assets/icons/self_testing_timeout.svg'; static const String movingSpeed = 'assets/icons/moving_speed.svg'; static const String boundary = 'assets/icons/boundary.svg'; static const String motionMeter = 'assets/icons/motion_meter.svg'; - static const String spatialStaticValue = 'assets/icons/spatial_static_value.svg'; - static const String spatialMotionValue = 'assets/icons/spatial_motion_value.svg'; + static const String spatialStaticValue = + 'assets/icons/spatial_static_value.svg'; + static const String spatialMotionValue = + 'assets/icons/spatial_motion_value.svg'; static const String presenceJudgementThrshold = 'assets/icons/presence_judgement_threshold.svg'; static const String spaceType = 'assets/icons/space_type.svg'; static const String sportsPara = 'assets/icons/sports_para.svg'; - static const String sensitivityFeature1 = 'assets/icons/sensitivity_feature_1.svg'; - static const String sensitivityFeature2 = 'assets/icons/sensitivity_feature_2.svg'; - static const String sensitivityFeature3 = 'assets/icons/sensitivity_feature_3.svg'; - static const String sensitivityFeature4 = 'assets/icons/sensitivity_feature_4.svg'; - static const String sensitivityFeature5 = 'assets/icons/sensitivity_feature_5.svg'; - static const String sensitivityFeature6 = 'assets/icons/sensitivity_feature_6.svg'; - static const String sensitivityFeature7 = 'assets/icons/sensitivity_feature_7.svg'; - static const String sensitivityFeature8 = 'assets/icons/sensitivity_feature_8.svg'; - static const String sensitivityFeature9 = 'assets/icons/sensitivity_feature_9.svg'; + static const String sensitivityFeature1 = + 'assets/icons/sensitivity_feature_1.svg'; + static const String sensitivityFeature2 = + 'assets/icons/sensitivity_feature_2.svg'; + static const String sensitivityFeature3 = + 'assets/icons/sensitivity_feature_3.svg'; + static const String sensitivityFeature4 = + 'assets/icons/sensitivity_feature_4.svg'; + static const String sensitivityFeature5 = + 'assets/icons/sensitivity_feature_5.svg'; + static const String sensitivityFeature6 = + 'assets/icons/sensitivity_feature_6.svg'; + static const String sensitivityFeature7 = + 'assets/icons/sensitivity_feature_7.svg'; + static const String sensitivityFeature8 = + 'assets/icons/sensitivity_feature_8.svg'; + static const String sensitivityFeature9 = + 'assets/icons/sensitivity_feature_9.svg'; static const String deviceTagIcon = 'assets/icons/device_tag_ic.svg'; static const String targetConfirmTimeIcon = 'assets/icons/target_confirm_time_icon.svg'; From 24a7f3ac2a2f214eefaf32c1800591d7e8bfd557 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 4 Jun 2025 13:06:27 +0300 Subject: [PATCH 176/181] SP-1594-device-location-api-integration. --- .../analytics/models/analytics_device.dart | 6 +++ .../models/device_location_info.dart | 5 +-- .../fetch_air_quality_data_helper.dart | 8 ++-- .../analytics/views/analytics_page.dart | 10 ++++- ...ce_location_details_service_decorator.dart | 40 +++++++++++++++++++ .../remote_device_location_service.dart | 7 +++- ...ode_device_location_service_decorator.dart | 40 ------------------- 7 files changed, 65 insertions(+), 51 deletions(-) create mode 100644 lib/pages/analytics/services/device_location/device_location_details_service_decorator.dart delete mode 100644 lib/pages/analytics/services/device_location/reverse_geocode_device_location_service_decorator.dart diff --git a/lib/pages/analytics/models/analytics_device.dart b/lib/pages/analytics/models/analytics_device.dart index eaac8b2b..3340a41d 100644 --- a/lib/pages/analytics/models/analytics_device.dart +++ b/lib/pages/analytics/models/analytics_device.dart @@ -8,6 +8,8 @@ class AnalyticsDevice { this.isActive, this.productDevice, this.spaceUuid, + this.latitude, + this.longitude, }); final String uuid; @@ -18,6 +20,8 @@ class AnalyticsDevice { final bool? isActive; final ProductDevice? productDevice; final String? spaceUuid; + final double? latitude; + final double? longitude; factory AnalyticsDevice.fromJson(Map json) { return AnalyticsDevice( @@ -35,6 +39,8 @@ class AnalyticsDevice { ? ProductDevice.fromJson(json['productDevice'] as Map) : null, spaceUuid: json['spaceUuid'] as String?, + latitude: json['lat'] != null ? double.parse(json['lat'] as String) : null, + longitude: json['lon'] != null ? double.parse(json['lon'] as String) : null, ); } } diff --git a/lib/pages/analytics/models/device_location_info.dart b/lib/pages/analytics/models/device_location_info.dart index 9b0095f7..aef7eebb 100644 --- a/lib/pages/analytics/models/device_location_info.dart +++ b/lib/pages/analytics/models/device_location_info.dart @@ -19,11 +19,8 @@ class DeviceLocationInfo extends Equatable { factory DeviceLocationInfo.fromJson(Map json) { return DeviceLocationInfo( - airQuality: json['airQuality'] as double?, + airQuality: json['aqi'] as double?, humidity: json['humidity'] as double?, - city: json['city'] as String?, - country: json['country'] as String?, - address: json['address'] as String?, temperature: json['temperature'] as double?, ); } 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 7f26bb5a..cb37484c 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 @@ -65,7 +65,7 @@ abstract final class FetchAirQualityDataHelper { communityUuid: communityUuid, spaceUuid: spaceUuid, deviceTypes: ['AQI'], - requestType: AnalyticsDeviceRequestType.energyManagement, + requestType: AnalyticsDeviceRequestType.occupancy, ), onSuccess: (device) { context.read() @@ -73,10 +73,10 @@ abstract final class FetchAirQualityDataHelper { ..add(RealtimeDeviceChangesStarted(device.uuid)); context.read().add( - const LoadDeviceLocationEvent( + LoadDeviceLocationEvent( GetDeviceLocationDataParam( - latitude: 35.6895, - longitude: 139.6917, + latitude: device.latitude ?? 0, + longitude: device.longitude ?? 0, ), ), ); diff --git a/lib/pages/analytics/modules/analytics/views/analytics_page.dart b/lib/pages/analytics/modules/analytics/views/analytics_page.dart index 01dc8ef5..1ecd9aa3 100644 --- a/lib/pages/analytics/modules/analytics/views/analytics_page.dart +++ b/lib/pages/analytics/modules/analytics/views/analytics_page.dart @@ -1,3 +1,4 @@ +import 'package:dio/dio.dart'; 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'; @@ -19,8 +20,8 @@ import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/fa 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/device_location/device_location_details_service_decorator.dart'; import 'package:syncrow_web/pages/analytics/services/device_location/remote_device_location_service.dart'; -import 'package:syncrow_web/pages/analytics/services/device_location/reverse_geocode_device_location_service_decorator.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/occupacy/remote_occupancy_service.dart'; @@ -113,8 +114,13 @@ class _AnalyticsPageState extends State { ), BlocProvider( create: (context) => DeviceLocationBloc( - ReverseGeocodeDeviceLocationServiceDecorator( + DeviceLocationDetailsServiceDecorator( RemoteDeviceLocationService(_httpService), + Dio( + BaseOptions( + baseUrl: 'https://nominatim.openstreetmap.org/', + ), + ), ), ), ), diff --git a/lib/pages/analytics/services/device_location/device_location_details_service_decorator.dart b/lib/pages/analytics/services/device_location/device_location_details_service_decorator.dart new file mode 100644 index 00000000..0239bcb7 --- /dev/null +++ b/lib/pages/analytics/services/device_location/device_location_details_service_decorator.dart @@ -0,0 +1,40 @@ +import 'package:dio/dio.dart'; +import 'package:syncrow_web/pages/analytics/models/device_location_info.dart'; +import 'package:syncrow_web/pages/analytics/params/get_device_location_data_param.dart'; +import 'package:syncrow_web/pages/analytics/services/device_location/device_location_service.dart'; + +class DeviceLocationDetailsServiceDecorator implements DeviceLocationService { + const DeviceLocationDetailsServiceDecorator(this._decoratee, this._dio); + + final DeviceLocationService _decoratee; + final Dio _dio; + + @override + Future get(GetDeviceLocationDataParam param) async { + try { + final deviceLocationInfo = await _decoratee.get(param); + final response = await _dio.get>( + 'reverse', + queryParameters: { + 'format': 'json', + 'lat': param.latitude, + 'lon': param.longitude, + }, + ); + + final data = response.data; + if (data != null) { + final addressData = data['address'] as Map; + return deviceLocationInfo.copyWith( + city: addressData['city'], + country: addressData['country_code'].toString().toUpperCase(), + address: addressData['state'], + ); + } + + return deviceLocationInfo; + } catch (e) { + throw Exception('Failed to load device location info: ${e.toString()}'); + } + } +} diff --git a/lib/pages/analytics/services/device_location/remote_device_location_service.dart b/lib/pages/analytics/services/device_location/remote_device_location_service.dart index dce547a2..b8820180 100644 --- a/lib/pages/analytics/services/device_location/remote_device_location_service.dart +++ b/lib/pages/analytics/services/device_location/remote_device_location_service.dart @@ -17,7 +17,12 @@ class RemoteDeviceLocationService implements DeviceLocationService { final response = await _httpService.get( path: '/weather', queryParameters: param.toJson(), - expectedResponseModel: (data) => DeviceLocationInfo.fromJson(data), + expectedResponseModel: (data) { + final response = data as Map; + final location = response['data'] as Map; + + return DeviceLocationInfo.fromJson(location); + }, ); return response; } on DioException catch (e) { diff --git a/lib/pages/analytics/services/device_location/reverse_geocode_device_location_service_decorator.dart b/lib/pages/analytics/services/device_location/reverse_geocode_device_location_service_decorator.dart deleted file mode 100644 index a3ac1e55..00000000 --- a/lib/pages/analytics/services/device_location/reverse_geocode_device_location_service_decorator.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:geocoding/geocoding.dart'; -import 'package:syncrow_web/pages/analytics/models/device_location_info.dart'; -import 'package:syncrow_web/pages/analytics/params/get_device_location_data_param.dart'; -import 'package:syncrow_web/pages/analytics/services/device_location/device_location_service.dart'; - -class ReverseGeocodeDeviceLocationServiceDecorator implements DeviceLocationService { - const ReverseGeocodeDeviceLocationServiceDecorator(this._decoratee); - - final DeviceLocationService _decoratee; - - @override - Future get(GetDeviceLocationDataParam param) async { - try { - final deviceLocationInfo = await _decoratee.get(param); - - final placemarks = await placemarkFromCoordinates( - param.latitude, - param.longitude, - ); - - if (placemarks.isNotEmpty) { - final place = placemarks.first; - - final city = place.locality; - final country = place.country; - final address = place.street; - - return deviceLocationInfo.copyWith( - city: city, - country: country, - address: address, - ); - } - - return deviceLocationInfo; - } catch (e) { - throw Exception('Failed to reverse load device location info'); - } - } -} From 3a98f71ff3eed552403bfb925b94d82af705f347 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 4 Jun 2025 14:42:41 +0300 Subject: [PATCH 177/181] SP-1665-FE-Return-a-readable-error-when-a-connection-error-Exception-occurs-on-the-charts. --- .../analytics_devices_bloc.dart | 8 +++++++ .../energy_consumption_by_phases_bloc.dart | 8 +++++++ .../energy_consumption_per_device_bloc.dart | 11 +++++++++- .../power_clamp_info_bloc.dart | 8 +++++++ .../total_energy_consumption_bloc.dart | 8 +++++++ .../blocs/occupancy/occupancy_bloc.dart | 3 +++ .../occupancy_heat_map_bloc.dart | 8 +++++++ ..._management_analytics_devices_service.dart | 12 ++++++++++- ...e_occupancy_analytics_devices_service.dart | 21 +++++++++++++++++-- ..._energy_consumption_by_phases_service.dart | 13 +++++++++++- ...energy_consumption_per_device_service.dart | 13 +++++++++++- .../occupacy/remote_occupancy_service.dart | 15 +++++++++++-- .../remote_occupancy_heat_map_service.dart | 13 +++++++++++- .../remote_power_clamp_info_service.dart | 13 +++++++++++- ...mote_total_energy_consumption_service.dart | 13 +++++++++++- 15 files changed, 156 insertions(+), 11 deletions(-) diff --git a/lib/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart b/lib/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart index 244b2fa0..fbd28dee 100644 --- a/lib/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart +++ b/lib/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart @@ -3,6 +3,7 @@ import 'package:equatable/equatable.dart'; import 'package:syncrow_web/pages/analytics/models/analytics_device.dart'; import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart'; import 'package:syncrow_web/pages/analytics/services/analytics_devices/analytics_devices_service.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; part 'analytics_devices_event.dart'; part 'analytics_devices_state.dart'; @@ -36,6 +37,13 @@ class AnalyticsDevicesBloc if (devices.isNotEmpty) { event.onSuccess(devices.first); } + } on APIException catch (e) { + emit( + AnalyticsDevicesState( + status: AnalyticsDevicesStatus.failure, + errorMessage: e.message, + ), + ); } catch (e) { emit( AnalyticsDevicesState( diff --git a/lib/pages/analytics/modules/energy_management/blocs/energy_consumption_by_phases/energy_consumption_by_phases_bloc.dart b/lib/pages/analytics/modules/energy_management/blocs/energy_consumption_by_phases/energy_consumption_by_phases_bloc.dart index 012f435a..1acf7df5 100644 --- a/lib/pages/analytics/modules/energy_management/blocs/energy_consumption_by_phases/energy_consumption_by_phases_bloc.dart +++ b/lib/pages/analytics/modules/energy_management/blocs/energy_consumption_by_phases/energy_consumption_by_phases_bloc.dart @@ -3,6 +3,7 @@ import 'package:equatable/equatable.dart'; import 'package:syncrow_web/pages/analytics/models/phases_energy_consumption.dart'; import 'package:syncrow_web/pages/analytics/params/get_energy_consumption_by_phases_param.dart'; import 'package:syncrow_web/pages/analytics/services/energy_consumption_by_phases/energy_consumption_by_phases_service.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; part 'energy_consumption_by_phases_event.dart'; part 'energy_consumption_by_phases_state.dart'; @@ -31,6 +32,13 @@ class EnergyConsumptionByPhasesBloc chartData: chartData, ), ); + } on APIException catch (e) { + emit( + state.copyWith( + status: EnergyConsumptionByPhasesStatus.failure, + errorMessage: e.message, + ), + ); } catch (e) { emit( state.copyWith( diff --git a/lib/pages/analytics/modules/energy_management/blocs/energy_consumption_per_device/energy_consumption_per_device_bloc.dart b/lib/pages/analytics/modules/energy_management/blocs/energy_consumption_per_device/energy_consumption_per_device_bloc.dart index c1c51a16..97d182c5 100644 --- a/lib/pages/analytics/modules/energy_management/blocs/energy_consumption_per_device/energy_consumption_per_device_bloc.dart +++ b/lib/pages/analytics/modules/energy_management/blocs/energy_consumption_per_device/energy_consumption_per_device_bloc.dart @@ -3,6 +3,7 @@ import 'package:equatable/equatable.dart'; import 'package:syncrow_web/pages/analytics/models/device_energy_data_model.dart'; import 'package:syncrow_web/pages/analytics/params/get_energy_consumption_per_device_param.dart'; import 'package:syncrow_web/pages/analytics/services/energy_consumption_per_device/energy_consumption_per_device_service.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; part 'energy_consumption_per_device_event.dart'; part 'energy_consumption_per_device_state.dart'; @@ -13,7 +14,8 @@ class EnergyConsumptionPerDeviceBloc this._energyConsumptionPerDeviceService, ) : super(const EnergyConsumptionPerDeviceState()) { on(_onLoadEnergyConsumptionPerDeviceEvent); - on(_onClearEnergyConsumptionPerDeviceEvent); + on( + _onClearEnergyConsumptionPerDeviceEvent); } final EnergyConsumptionPerDeviceService _energyConsumptionPerDeviceService; @@ -31,6 +33,13 @@ class EnergyConsumptionPerDeviceBloc chartData: chartData, ), ); + } on APIException catch (e) { + emit( + state.copyWith( + status: EnergyConsumptionPerDeviceStatus.failure, + errorMessage: e.message, + ), + ); } catch (e) { emit( state.copyWith( diff --git a/lib/pages/analytics/modules/energy_management/blocs/power_clamp_info/power_clamp_info_bloc.dart b/lib/pages/analytics/modules/energy_management/blocs/power_clamp_info/power_clamp_info_bloc.dart index d0e7aab6..2aefd798 100644 --- a/lib/pages/analytics/modules/energy_management/blocs/power_clamp_info/power_clamp_info_bloc.dart +++ b/lib/pages/analytics/modules/energy_management/blocs/power_clamp_info/power_clamp_info_bloc.dart @@ -3,6 +3,7 @@ import 'package:equatable/equatable.dart'; import 'package:syncrow_web/pages/analytics/services/power_clamp_info/power_clamp_info_service.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; import 'package:syncrow_web/pages/device_managment/power_clamp/models/power_clamp_model.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; part 'power_clamp_info_event.dart'; part 'power_clamp_info_state.dart'; @@ -31,6 +32,13 @@ class PowerClampInfoBloc extends Bloc powerClampModel: powerClampModel, ), ); + } on APIException catch (e) { + emit( + state.copyWith( + status: PowerClampInfoStatus.error, + errorMessage: e.message, + ), + ); } catch (e) { emit( state.copyWith( diff --git a/lib/pages/analytics/modules/energy_management/blocs/total_energy_consumption/total_energy_consumption_bloc.dart b/lib/pages/analytics/modules/energy_management/blocs/total_energy_consumption/total_energy_consumption_bloc.dart index 42ad57e8..f51d20cf 100644 --- a/lib/pages/analytics/modules/energy_management/blocs/total_energy_consumption/total_energy_consumption_bloc.dart +++ b/lib/pages/analytics/modules/energy_management/blocs/total_energy_consumption/total_energy_consumption_bloc.dart @@ -3,6 +3,7 @@ import 'package:equatable/equatable.dart'; import 'package:syncrow_web/pages/analytics/models/energy_data_model.dart'; import 'package:syncrow_web/pages/analytics/params/get_total_energy_consumption_param.dart'; import 'package:syncrow_web/pages/analytics/services/total_energy_consumption/total_energy_consumption_service.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; part 'total_energy_consumption_event.dart'; part 'total_energy_consumption_state.dart'; @@ -31,6 +32,13 @@ class TotalEnergyConsumptionBloc status: TotalEnergyConsumptionStatus.loaded, ), ); + } on APIException catch (e) { + emit( + state.copyWith( + errorMessage: e.message, + status: TotalEnergyConsumptionStatus.failure, + ), + ); } catch (e) { emit( state.copyWith( diff --git a/lib/pages/analytics/modules/occupancy/blocs/occupancy/occupancy_bloc.dart b/lib/pages/analytics/modules/occupancy/blocs/occupancy/occupancy_bloc.dart index 6eeda29b..110f3c60 100644 --- a/lib/pages/analytics/modules/occupancy/blocs/occupancy/occupancy_bloc.dart +++ b/lib/pages/analytics/modules/occupancy/blocs/occupancy/occupancy_bloc.dart @@ -3,6 +3,7 @@ import 'package:equatable/equatable.dart'; import 'package:syncrow_web/pages/analytics/models/occupacy.dart'; import 'package:syncrow_web/pages/analytics/params/get_occupancy_param.dart'; import 'package:syncrow_web/pages/analytics/services/occupacy/occupacy_service.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; part 'occupancy_event.dart'; part 'occupancy_state.dart'; @@ -23,6 +24,8 @@ class OccupancyBloc extends Bloc { try { final chartData = await _occupacyService.load(event.param); emit(state.copyWith(chartData: chartData, status: OccupancyStatus.loaded)); + } on APIException catch (e) { + emit(state.copyWith(status: OccupancyStatus.failure, errorMessage: e.message)); } catch (e) { emit(state.copyWith(status: OccupancyStatus.failure, errorMessage: '$e')); } diff --git a/lib/pages/analytics/modules/occupancy/blocs/occupancy_heat_map/occupancy_heat_map_bloc.dart b/lib/pages/analytics/modules/occupancy/blocs/occupancy_heat_map/occupancy_heat_map_bloc.dart index 5d5cb914..453b68ce 100644 --- a/lib/pages/analytics/modules/occupancy/blocs/occupancy_heat_map/occupancy_heat_map_bloc.dart +++ b/lib/pages/analytics/modules/occupancy/blocs/occupancy_heat_map/occupancy_heat_map_bloc.dart @@ -3,6 +3,7 @@ import 'package:equatable/equatable.dart'; import 'package:syncrow_web/pages/analytics/models/occupancy_heat_map_model.dart'; import 'package:syncrow_web/pages/analytics/params/get_occupancy_heat_map_param.dart'; import 'package:syncrow_web/pages/analytics/services/occupancy_heat_map/occupancy_heat_map_service.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; part 'occupancy_heat_map_event.dart'; part 'occupancy_heat_map_state.dart'; @@ -30,6 +31,13 @@ class OccupancyHeatMapBloc heatMapData: occupancyHeatMap, ), ); + } on APIException catch (e) { + emit( + state.copyWith( + status: OccupancyHeatMapStatus.failure, + errorMessage: e.message, + ), + ); } catch (e) { emit( state.copyWith( diff --git a/lib/pages/analytics/services/analytics_devices/remote_energy_management_analytics_devices_service.dart b/lib/pages/analytics/services/analytics_devices/remote_energy_management_analytics_devices_service.dart index adf8a6fa..9ef711e9 100644 --- a/lib/pages/analytics/services/analytics_devices/remote_energy_management_analytics_devices_service.dart +++ b/lib/pages/analytics/services/analytics_devices/remote_energy_management_analytics_devices_service.dart @@ -1,6 +1,8 @@ +import 'package:dio/dio.dart'; import 'package:syncrow_web/pages/analytics/models/analytics_device.dart'; import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart'; import 'package:syncrow_web/pages/analytics/services/analytics_devices/analytics_devices_service.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; import 'package:syncrow_web/services/api/http_service.dart'; final class RemoteEnergyManagementAnalyticsDevicesService @@ -9,6 +11,8 @@ final class RemoteEnergyManagementAnalyticsDevicesService final HTTPService _httpService; + static const _defaultErrorMessage = 'Failed to load analytics devices'; + @override Future> getDevices(GetAnalyticsDevicesParam param) async { try { @@ -29,8 +33,14 @@ final class RemoteEnergyManagementAnalyticsDevicesService ); return response; + } on DioException catch (e) { + final message = e.response?.data as Map?; + final error = message?['error'] as Map?; + final errorMessage = error?['error'] as String? ?? ''; + final formattedErrorMessage = [_defaultErrorMessage, errorMessage].join(': '); + throw APIException(formattedErrorMessage); } catch (e) { - throw Exception('Failed to load total energy consumption: $e'); + throw APIException('$_defaultErrorMessage: $e'); } } } diff --git a/lib/pages/analytics/services/analytics_devices/remote_occupancy_analytics_devices_service.dart b/lib/pages/analytics/services/analytics_devices/remote_occupancy_analytics_devices_service.dart index 91bbe1f4..736b0804 100644 --- a/lib/pages/analytics/services/analytics_devices/remote_occupancy_analytics_devices_service.dart +++ b/lib/pages/analytics/services/analytics_devices/remote_occupancy_analytics_devices_service.dart @@ -1,7 +1,9 @@ +import 'package:dio/dio.dart'; import 'package:syncrow_web/pages/analytics/models/analytics_device.dart'; import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart'; import 'package:syncrow_web/pages/analytics/services/analytics_devices/analytics_devices_service.dart'; import 'package:syncrow_web/pages/common/bloc/project_manager.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; import 'package:syncrow_web/services/api/http_service.dart'; class RemoteOccupancyAnalyticsDevicesService implements AnalyticsDevicesService { @@ -9,6 +11,8 @@ class RemoteOccupancyAnalyticsDevicesService implements AnalyticsDevicesService final HTTPService _httpService; + static const _defaultErrorMessage = 'Failed to load analytics devices'; + @override Future> getDevices(GetAnalyticsDevicesParam param) async { try { @@ -26,8 +30,15 @@ class RemoteOccupancyAnalyticsDevicesService implements AnalyticsDevicesService final result = requests.map((e) => e.first).toList(); return result; + } on DioException catch (e) { + final message = e.response?.data as Map?; + final error = message?['error'] as Map?; + final errorMessage = error?['error'] as String? ?? ''; + final formattedErrorMessage = [_defaultErrorMessage, errorMessage].join(': '); + throw APIException(formattedErrorMessage); } catch (e) { - throw Exception('Failed to load total energy consumption: $e'); + final formattedErrorMessage = [_defaultErrorMessage, e.toString()].join(': '); + throw APIException(formattedErrorMessage); } } @@ -54,8 +65,14 @@ class RemoteOccupancyAnalyticsDevicesService implements AnalyticsDevicesService }, ); return response; + } on DioException catch (e) { + final message = e.response?.data as Map?; + final error = message?['error'] as Map?; + final errorMessage = error?['error'] as String? ?? ''; + final formattedErrorMessage = [_defaultErrorMessage, errorMessage].join(': '); + throw APIException(formattedErrorMessage); } catch (e) { - rethrow; + throw APIException('$_defaultErrorMessage: $e'); } } } diff --git a/lib/pages/analytics/services/energy_consumption_by_phases/remote_energy_consumption_by_phases_service.dart b/lib/pages/analytics/services/energy_consumption_by_phases/remote_energy_consumption_by_phases_service.dart index 28df5eed..17f9baff 100644 --- a/lib/pages/analytics/services/energy_consumption_by_phases/remote_energy_consumption_by_phases_service.dart +++ b/lib/pages/analytics/services/energy_consumption_by_phases/remote_energy_consumption_by_phases_service.dart @@ -1,6 +1,8 @@ +import 'package:dio/dio.dart'; import 'package:syncrow_web/pages/analytics/models/phases_energy_consumption.dart'; import 'package:syncrow_web/pages/analytics/params/get_energy_consumption_by_phases_param.dart'; import 'package:syncrow_web/pages/analytics/services/energy_consumption_by_phases/energy_consumption_by_phases_service.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; import 'package:syncrow_web/services/api/http_service.dart'; final class RemoteEnergyConsumptionByPhasesService @@ -9,6 +11,8 @@ final class RemoteEnergyConsumptionByPhasesService final HTTPService _httpService; + static const _defaultErrorMessage = 'Failed to load energy consumption per phase'; + @override Future> load( GetEnergyConsumptionByPhasesParam param, @@ -28,8 +32,15 @@ final class RemoteEnergyConsumptionByPhasesService }, ); return response; + } on DioException catch (e) { + final message = e.response?.data as Map?; + final error = message?['error'] as Map?; + final errorMessage = error?['error'] as String? ?? ''; + final formattedErrorMessage = [_defaultErrorMessage, errorMessage].join(': '); + throw APIException(formattedErrorMessage); } catch (e) { - throw Exception('Failed to load energy consumption per phase: $e'); + final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': '); + throw APIException(formattedErrorMessage); } } } diff --git a/lib/pages/analytics/services/energy_consumption_per_device/remote_energy_consumption_per_device_service.dart b/lib/pages/analytics/services/energy_consumption_per_device/remote_energy_consumption_per_device_service.dart index 165ab5ab..82b21b1c 100644 --- a/lib/pages/analytics/services/energy_consumption_per_device/remote_energy_consumption_per_device_service.dart +++ b/lib/pages/analytics/services/energy_consumption_per_device/remote_energy_consumption_per_device_service.dart @@ -1,8 +1,10 @@ +import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/analytics/models/device_energy_data_model.dart'; import 'package:syncrow_web/pages/analytics/models/energy_data_model.dart'; import 'package:syncrow_web/pages/analytics/params/get_energy_consumption_per_device_param.dart'; import 'package:syncrow_web/pages/analytics/services/energy_consumption_per_device/energy_consumption_per_device_service.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; import 'package:syncrow_web/services/api/http_service.dart'; class RemoteEnergyConsumptionPerDeviceService @@ -11,6 +13,8 @@ class RemoteEnergyConsumptionPerDeviceService final HTTPService _httpService; + static const _defaultErrorMessage = 'Failed to load energy consumption per device'; + @override Future> load( GetEnergyConsumptionPerDeviceParam param, @@ -23,8 +27,15 @@ class RemoteEnergyConsumptionPerDeviceService expectedResponseModel: _EnergyConsumptionPerDeviceMapper.map, ); return response; + } on DioException catch (e) { + final message = e.response?.data as Map?; + final error = message?['error'] as Map?; + final errorMessage = error?['error'] as String? ?? ''; + final formattedErrorMessage = [_defaultErrorMessage, errorMessage].join(': '); + throw APIException(formattedErrorMessage); } catch (e) { - throw Exception('Failed to load energy consumption per device: $e'); + final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': '); + throw APIException(formattedErrorMessage); } } } diff --git a/lib/pages/analytics/services/occupacy/remote_occupancy_service.dart b/lib/pages/analytics/services/occupacy/remote_occupancy_service.dart index b8cce70a..afd3f79e 100644 --- a/lib/pages/analytics/services/occupacy/remote_occupancy_service.dart +++ b/lib/pages/analytics/services/occupacy/remote_occupancy_service.dart @@ -1,6 +1,8 @@ +import 'package:dio/dio.dart'; import 'package:syncrow_web/pages/analytics/models/occupacy.dart'; import 'package:syncrow_web/pages/analytics/params/get_occupancy_param.dart'; import 'package:syncrow_web/pages/analytics/services/occupacy/occupacy_service.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; import 'package:syncrow_web/services/api/http_service.dart'; final class RemoteOccupancyService implements OccupacyService { @@ -8,6 +10,8 @@ final class RemoteOccupancyService implements OccupacyService { final HTTPService _httpService; + static const _defaultErrorMessage = 'Failed to load occupancy'; + @override Future> load(GetOccupancyParam param) async { try { @@ -25,8 +29,15 @@ final class RemoteOccupancyService implements OccupacyService { }, ); return response; + } on DioException catch (e) { + final message = e.response?.data as Map?; + final error = message?['error'] as Map?; + final errorMessage = error?['error'] as String? ?? ''; + final formattedErrorMessage = [_defaultErrorMessage, errorMessage].join(': '); + throw APIException(formattedErrorMessage); } catch (e) { - throw Exception('Failed to load energy consumption per phase: $e'); + final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': '); + throw APIException(formattedErrorMessage); } } -} \ No newline at end of file +} diff --git a/lib/pages/analytics/services/occupancy_heat_map/remote_occupancy_heat_map_service.dart b/lib/pages/analytics/services/occupancy_heat_map/remote_occupancy_heat_map_service.dart index ac06ccf7..0d7f6500 100644 --- a/lib/pages/analytics/services/occupancy_heat_map/remote_occupancy_heat_map_service.dart +++ b/lib/pages/analytics/services/occupancy_heat_map/remote_occupancy_heat_map_service.dart @@ -1,6 +1,8 @@ +import 'package:dio/dio.dart'; import 'package:syncrow_web/pages/analytics/models/occupancy_heat_map_model.dart'; import 'package:syncrow_web/pages/analytics/params/get_occupancy_heat_map_param.dart'; import 'package:syncrow_web/pages/analytics/services/occupancy_heat_map/occupancy_heat_map_service.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; import 'package:syncrow_web/services/api/http_service.dart'; final class RemoteOccupancyHeatMapService implements OccupancyHeatMapService { @@ -8,6 +10,8 @@ final class RemoteOccupancyHeatMapService implements OccupancyHeatMapService { final HTTPService _httpService; + static const _defaultErrorMessage = 'Failed to load occupancy heat map'; + @override Future> load(GetOccupancyHeatMapParam param) async { try { @@ -28,8 +32,15 @@ final class RemoteOccupancyHeatMapService implements OccupancyHeatMapService { ); return response; + } on DioException catch (e) { + final message = e.response?.data as Map?; + final error = message?['error'] as Map?; + final errorMessage = error?['error'] as String? ?? ''; + final formattedErrorMessage = [_defaultErrorMessage, errorMessage].join(': '); + throw APIException(formattedErrorMessage); } catch (e) { - throw Exception('Failed to load total energy consumption:'); + final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': '); + throw APIException(formattedErrorMessage); } } } diff --git a/lib/pages/analytics/services/power_clamp_info/remote_power_clamp_info_service.dart b/lib/pages/analytics/services/power_clamp_info/remote_power_clamp_info_service.dart index 17d5a7fc..b4bc82c6 100644 --- a/lib/pages/analytics/services/power_clamp_info/remote_power_clamp_info_service.dart +++ b/lib/pages/analytics/services/power_clamp_info/remote_power_clamp_info_service.dart @@ -1,5 +1,7 @@ +import 'package:dio/dio.dart'; import 'package:syncrow_web/pages/analytics/services/power_clamp_info/power_clamp_info_service.dart'; import 'package:syncrow_web/pages/device_managment/power_clamp/models/power_clamp_model.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; import 'package:syncrow_web/services/api/http_service.dart'; final class RemotePowerClampInfoService implements PowerClampInfoService { @@ -7,6 +9,8 @@ final class RemotePowerClampInfoService implements PowerClampInfoService { final HTTPService _httpService; + static const _defaultErrorMessage = 'Failed to fetch power clamp info'; + @override Future getInfo(String deviceId) async { try { @@ -20,8 +24,15 @@ final class RemotePowerClampInfoService implements PowerClampInfoService { }, ); return response; + } on DioException catch (e) { + final message = e.response?.data as Map?; + final error = message?['error'] as Map?; + final errorMessage = error?['error'] as String? ?? ''; + final formattedErrorMessage = [_defaultErrorMessage, errorMessage].join(': '); + throw APIException(formattedErrorMessage); } catch (e) { - throw Exception('Failed to fetch power clamp info: $e'); + final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': '); + throw APIException(formattedErrorMessage); } } } diff --git a/lib/pages/analytics/services/total_energy_consumption/remote_total_energy_consumption_service.dart b/lib/pages/analytics/services/total_energy_consumption/remote_total_energy_consumption_service.dart index 8c3041eb..838cc5e7 100644 --- a/lib/pages/analytics/services/total_energy_consumption/remote_total_energy_consumption_service.dart +++ b/lib/pages/analytics/services/total_energy_consumption/remote_total_energy_consumption_service.dart @@ -1,6 +1,8 @@ +import 'package:dio/dio.dart'; import 'package:syncrow_web/pages/analytics/models/energy_data_model.dart'; import 'package:syncrow_web/pages/analytics/params/get_total_energy_consumption_param.dart'; import 'package:syncrow_web/pages/analytics/services/total_energy_consumption/total_energy_consumption_service.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; import 'package:syncrow_web/services/api/http_service.dart'; class RemoteTotalEnergyConsumptionService implements TotalEnergyConsumptionService { @@ -8,6 +10,8 @@ class RemoteTotalEnergyConsumptionService implements TotalEnergyConsumptionServi final HTTPService _httpService; + static const _defaultErrorMessage = 'Failed to load total energy consumption'; + @override Future> load( GetTotalEnergyConsumptionParam param, @@ -21,8 +25,15 @@ class RemoteTotalEnergyConsumptionService implements TotalEnergyConsumptionServi ); return response; + } on DioException catch (e) { + final message = e.response?.data as Map?; + final error = message?['error'] as Map?; + final errorMessage = error?['error'] as String? ?? ''; + final formattedErrorMessage = [_defaultErrorMessage, errorMessage].join(': '); + throw APIException(formattedErrorMessage); } catch (e) { - throw Exception('Failed to load total energy consumption: $e'); + final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': '); + throw APIException(formattedErrorMessage); } } } From beb5239c4ff8d062a790b1deea27934182d3c244 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 4 Jun 2025 10:12:19 +0300 Subject: [PATCH 178/181] enhanced ci/cd by not running the deply jobs on the PR itself, and now we only deploy when we merged a PR to `dev` or `main`, and created a separate GitHub action that only builds and install dependencies, which only runs on the PR itself. --- ...e-static-web-apps-mango-bush-01e607f10.yml | 4 --- ...static-web-apps-polite-smoke-017c65c10.yml | 6 ---- .github/workflows/pr-check.yml | 29 +++++++++++++++++++ 3 files changed, 29 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/pr-check.yml diff --git a/.github/workflows/azure-static-web-apps-mango-bush-01e607f10.yml b/.github/workflows/azure-static-web-apps-mango-bush-01e607f10.yml index db94e74f..892381f3 100644 --- a/.github/workflows/azure-static-web-apps-mango-bush-01e607f10.yml +++ b/.github/workflows/azure-static-web-apps-mango-bush-01e607f10.yml @@ -4,10 +4,6 @@ on: push: branches: - main - pull_request: - types: [opened, synchronize, reopened, closed] - branches: - - main jobs: build_and_deploy_job: diff --git a/.github/workflows/azure-static-web-apps-polite-smoke-017c65c10.yml b/.github/workflows/azure-static-web-apps-polite-smoke-017c65c10.yml index 738bd279..6a91fe27 100644 --- a/.github/workflows/azure-static-web-apps-polite-smoke-017c65c10.yml +++ b/.github/workflows/azure-static-web-apps-polite-smoke-017c65c10.yml @@ -4,18 +4,12 @@ on: push: branches: - dev - pull_request: - types: [opened, synchronize, reopened, closed] - branches: - - dev jobs: build_and_deploy_job: - if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed') runs-on: ubuntu-latest name: Build and Deploy Job steps: - - name: Checkout Code uses: actions/checkout@v3 with: diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml new file mode 100644 index 00000000..621a54d3 --- /dev/null +++ b/.github/workflows/pr-check.yml @@ -0,0 +1,29 @@ +name: Pull Request Check + +on: + pull_request: + branches: + - dev + - main + +jobs: + setup_flutter: + runs-on: ubuntu-latest + name: Setup Flutter and Dependencies + steps: + - name: Checkout Code + uses: actions/checkout@v3 + with: + submodules: true + lfs: false + + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: '3.27.3' + + - name: Install dependencies + run: flutter pub get + + - name: Run Flutter Build + run: flutter build web --web-renderer canvaskit -t lib/main_dev.dart From 3e329682097e24c0d529e80397499f49f48fb846 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 28 May 2025 16:46:24 +0300 Subject: [PATCH 179/181] Update pull_request_template.md --- .github/pull_request_template.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index d18a89f3..70d6e519 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -7,11 +7,7 @@ --> ## Jira Ticket - - -## Status - -**READY/IN DEVELOPMENT/HOLD** +[SP-0000](https://syncrow.atlassian.net/browse/SP-0000) ## Description @@ -27,4 +23,4 @@ - [ ] 🧹 Code refactor - [ ] ✅ Build configuration change - [ ] 📝 Documentation -- [ ] 🗑️ Chore \ No newline at end of file +- [ ] 🗑️ Chore From 0c6e4fed80cec7f070a442133bed3e8dc45f98ee Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 4 Jun 2025 15:09:40 +0300 Subject: [PATCH 180/181] correctly fetch energy management data using `spaceUuid`. --- .../analytics_energy_management_view.dart | 25 +------------------ ...t_energy_consumption_per_device_param.dart | 2 +- .../get_total_energy_consumption_param.dart | 2 +- 3 files changed, 3 insertions(+), 26 deletions(-) diff --git a/lib/pages/analytics/modules/energy_management/views/analytics_energy_management_view.dart b/lib/pages/analytics/modules/energy_management/views/analytics_energy_management_view.dart index ffb09113..f88febcc 100644 --- a/lib/pages/analytics/modules/energy_management/views/analytics_energy_management_view.dart +++ b/lib/pages/analytics/modules/energy_management/views/analytics_energy_management_view.dart @@ -1,34 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/fetch_energy_management_data_helper.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart_box.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/power_clamp_energy_data_widget.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart_box.dart'; -import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart'; -class AnalyticsEnergyManagementView extends StatefulWidget { +class AnalyticsEnergyManagementView extends StatelessWidget { const AnalyticsEnergyManagementView({super.key}); - @override - State createState() => - _AnalyticsEnergyManagementViewState(); -} - -class _AnalyticsEnergyManagementViewState - extends State { - @override - void initState() { - final spaceTreeBloc = context.read(); - final communityId = spaceTreeBloc.state.selectedCommunities.firstOrNull; - final spaceId = spaceTreeBloc.state.selectedSpaces.firstOrNull; - FetchEnergyManagementDataHelper.loadEnergyManagementData( - context, - communityId: communityId ?? '', - spaceId: spaceId ?? '', - ); - super.initState(); - } - static const _padding = EdgeInsetsDirectional.all(32); @override Widget build(BuildContext context) { 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 79d0f2f4..c219893e 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 @@ -10,7 +10,7 @@ class GetEnergyConsumptionPerDeviceParam { Map toJson() => { 'monthDate': '${monthDate?.year}-${monthDate?.month.toString().padLeft(2, '0')}', - if (spaceId == null || spaceId == null) 'spaceUuid': spaceId, + if (spaceId != null) 'spaceUuid': spaceId, '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 6428fd30..f5615cca 100644 --- a/lib/pages/analytics/params/get_total_energy_consumption_param.dart +++ b/lib/pages/analytics/params/get_total_energy_consumption_param.dart @@ -11,7 +11,7 @@ class GetTotalEnergyConsumptionParam { return { 'monthDate': '${monthDate?.year}-${monthDate?.month.toString().padLeft(2, '0')}', - if (spaceId == null || spaceId == null) 'spaceUuid': spaceId, + if (spaceId != null) 'spaceUuid': spaceId, 'groupByDevice': false, }; } From 8a274af7be25936751709fe321bd360bf750161c Mon Sep 17 00:00:00 2001 From: mohammad Date: Wed, 11 Jun 2025 10:47:59 +0300 Subject: [PATCH 181/181] Update button behavior in DeviceManagementPage based on routineTab state --- .../view/device_managment_page.dart | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) 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 755bc8b7..2379c22d 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 @@ -40,17 +40,18 @@ class DeviceManagementPage extends StatelessWidget with HelperResponsiveLayout { style: TextButton.styleFrom( backgroundColor: null, ), - onPressed: () { - BlocProvider.of(context) - .add(const ResetSelectedEvent()); + onPressed: !state.routineTab + ? null + : () { + BlocProvider.of(context) + .add(const ResetSelectedEvent()); - context - .read() - .add(const TriggerSwitchTabsEvent(isRoutineTab: false)); - context - .read() - .add(FetchDevices(context)); - }, + context.read().add( + const TriggerSwitchTabsEvent(isRoutineTab: false)); + context + .read() + .add(FetchDevices(context)); + }, child: Text( 'Devices', style: context.textTheme.titleMedium?.copyWith( @@ -66,14 +67,15 @@ class DeviceManagementPage extends StatelessWidget with HelperResponsiveLayout { style: TextButton.styleFrom( backgroundColor: null, ), - onPressed: () { - BlocProvider.of(context) - .add(const ResetSelectedEvent()); + onPressed: state.routineTab + ? null + : () { + BlocProvider.of(context) + .add(const ResetSelectedEvent()); - context - .read() - .add(const TriggerSwitchTabsEvent(isRoutineTab: true)); - }, + context.read().add( + const TriggerSwitchTabsEvent(isRoutineTab: true)); + }, child: Text( 'Routines', style: context.textTheme.titleMedium?.copyWith(