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" 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/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/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/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/views/air_quality_view.dart b/lib/pages/analytics/modules/air_quality/views/air_quality_view.dart index 03df3ba9..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 @@ -33,7 +33,7 @@ class AirQualityView extends StatelessWidget { return SingleChildScrollView( child: Container( padding: _padding, - height: height, + height: height * 1.1, child: const Column( children: [ Expanded( @@ -41,7 +41,7 @@ class AirQualityView extends StatelessWidget { spacing: 32, children: [ Expanded( - flex: 5, + flex: 10, child: Column( spacing: 20, children: [ @@ -50,7 +50,7 @@ class AirQualityView extends StatelessWidget { ], ), ), - Expanded(flex: 2, child: AirQualityEndSideWidget()), + Expanded(flex: 6, 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 new file mode 100644 index 00000000..93904604 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_gauge_and_info.dart @@ -0,0 +1,52 @@ +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'; + +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; + + @override + Widget build(BuildContext context) { + return Expanded( + flex: 2, + child: Row( + children: [ + Expanded( + flex: 2, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [Expanded(child: AqiGauge(aqi: aqi))], + ), + ), + const Spacer(), + Expanded( + child: Padding( + padding: const EdgeInsetsDirectional.only(end: 20), + child: AqiHumidityAndTemperature( + temperature: temperature, + humidity: humidity, + ), + ), + ), + ], + ), + ); + } + + double get aqi => switch (aqiLevel) { + 'level_1' => 25.0, + 'level_2' => 75.0, + 'level_3' => 125.0, + _ => 0.0, + }; +} 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 106685c1..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,10 +1,7 @@ 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/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/style.dart'; class AirQualityEndSideWidget extends StatelessWidget { @@ -17,70 +14,15 @@ class AirQualityEndSideWidget extends StatelessWidget { borderRadius: BorderRadius.circular(30), ), padding: const EdgeInsetsDirectional.all(32), - child: Column( + child: const 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, - ), - ), + AnalyticsSidebarHeader(title: 'AQI Sensor'), + Expanded(flex: 15, child: AqiDeviceInfo()), + SizedBox(height: 20), + Expanded(flex: 6, child: AqiLocationInfo()), ], ), ); } - - Widget _buildHeader(BuildContext context) { - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded( - flex: 3, - child: FittedBox( - 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: 4, - child: FittedBox( - alignment: AlignmentDirectional.centerEnd, - child: AnalyticsDeviceDropdown( - onChanged: (value) { - context.read().add( - SelectAnalyticsDeviceEvent(value), - ); - FetchEnergyManagementDataHelper.loadRealtimeDeviceChanges( - context, - deviceUuid: value.uuid, - ); - }, - ), - ), - ), - ], - ); - } } 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..f3773c29 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_device_info.dart @@ -0,0 +1,129 @@ +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}); + + String _getValueForStatus( + List deviceStatusList, + String code, { + double defaultValue = 0, + String Function(int value)? formatter, + }) { + try { + final foundStatus = deviceStatusList.firstWhere((e) => e.code == code); + final value = foundStatus.value.toString(); + final intValue = int.parse(value); + return formatter != null ? formatter(intValue) : intValue.toString(); + } catch (e) { + return defaultValue.toString(); + } + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final status = state.deviceStatusList; + final humidityValue = _getValueForStatus( + status, + 'humidity_value', + formatter: (value) => value.toStringAsFixed(0), + ); + + final tempValue = _getValueForStatus( + status, + 'temp_current', + formatter: (value) => (value / 10).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 []), + 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, + ), + ], + ), + ), + ); + }, + ); + } +} 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..fc7f923a --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_gauge.dart @@ -0,0 +1,115 @@ +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; + + static const _minRange = 0.0; + static const _goodRange = 50.0; + static const _moderateRange = 100.0; + static const _maxRange = 150.0; + + @override + Widget build(BuildContext context) { + final (status, statusColor) = _getStatusData(aqi); + return AnimatedRadialGauge( + value: aqi, + debug: false, + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOut, + 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: 'Air Quality\n', + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w400, + fontSize: 12, + ), + children: [ + TextSpan( + text: status, + style: context.textTheme.bodySmall?.copyWith( + color: _darkenColor(statusColor), + fontWeight: FontWeight.w400, + fontSize: 30, + ), + ), + ], + ), + textAlign: TextAlign.center, + ), + ), + ); + }, + axis: GaugeAxis( + progressBar: const GaugeProgressBar.basic(color: Colors.transparent), + style: const GaugeAxisStyle( + cornerRadius: Radius.circular(16), + thickness: 14, + segmentSpacing: 4, + ), + min: _minRange, + max: _maxRange, + pointer: GaugePointer.circle( + position: const GaugePointerPosition.surface(), + radius: MediaQuery.sizeOf(context).width * 0.004, + color: ColorsManager.whiteColors, + border: GaugePointerBorder( + width: 6, + color: statusColor, + ), + shadow: const BoxShadow( + color: ColorsManager.blackColor, + blurRadius: 6, + offset: Offset(0, 2), + ), + ), + segments: const [ + GaugeSegment( + from: _minRange, + to: _goodRange, + cornerRadius: Radius.circular(16), + color: ColorsManager.goodGreen, + ), + GaugeSegment( + from: _goodRange + 1, + to: _moderateRange, + cornerRadius: Radius.circular(16), + color: ColorsManager.moderateYellow, + ), + GaugeSegment( + from: _moderateRange + 1, + to: _maxRange, + cornerRadius: Radius.circular(16), + color: ColorsManager.poorOrange, + ), + ], + ), + ); + } + + (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)!; + } +} 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..7726ff32 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_humidity_and_temperature.dart @@ -0,0 +1,60 @@ +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({ + required this.temperature, + required this.humidity, + super.key, + }); + + final int temperature; + final int humidity; + + 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: 16, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _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), + ], + ); + } +} 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..3f1d1f09 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_location.dart @@ -0,0 +1,52 @@ +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 [], + borderRadius: BorderRadius.circular(10), + ), + 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, + ), + ); + } +} 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_location_info_cell.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info_cell.dart new file mode 100644 index 00000000..fa0216a1 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info_cell.dart @@ -0,0 +1,87 @@ +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: 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, + ), + ), + ), + ), + ), + ), + Align( + alignment: AlignmentDirectional.bottomEnd, + child: Padding( + padding: const EdgeInsetsDirectional.all(10), + child: SizedBox( + height: 40, + width: 120, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: AlignmentDirectional.bottomEnd, + child: Text( + value, + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.vividBlue.withValues(alpha: 0.7), + fontWeight: FontWeight.w700, + fontSize: 24, + ), + ), + ), + ), + ), + ), + Align( + alignment: AlignmentDirectional.bottomStart, + child: SizedBox.square( + dimension: MediaQuery.sizeOf(context).width * 0.45, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: AlignmentDirectional.bottomStart, + child: SvgPicture.asset(svgPath), + ), + ), + ), + ], + ), + ), + ); + } +} 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..5a8e6e6c --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_sub_value_widget.dart @@ -0,0 +1,158 @@ +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 double max; + final Color color; +} + +class AqiSubValueWidget extends StatelessWidget { + const AqiSubValueWidget({ + required this.label, + required this.value, + required this.unit, + 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; + return [ + _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), + ]; + } + + 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; + } + + @override + Widget build(BuildContext context) { + final activeSegment = _getActiveSegmentByRange(_parsedValue, range); + 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), + ], + ), + ), + ); + } + + 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, + 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: AnimatedContainer( + duration: const Duration(milliseconds: 150), + curve: Curves.linear, + 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, + ), + ), + ), + ); + } +} 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..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 @@ -3,16 +3,17 @@ 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'); + + const AqiType(this.value, this.unit); final String value; - const AqiType(this.value); + final String unit; } class AqiTypeDropdown extends StatefulWidget { 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..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()) @@ -42,18 +42,11 @@ 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()); + context.read().add( + const AnalyticsClearAllSpaceTreeSelectionsEvent(), + ); FetchAirQualityDataHelper.clearAllData(context); } } 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 e73b5179..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 @@ -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, @@ -62,18 +62,11 @@ class EnergyManagementDataLoadingStrategy 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()); + 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..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 @@ -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()) @@ -42,18 +42,11 @@ 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(const SpaceTreeClearSelectionEvent()); + context.read().add( + const AnalyticsClearAllSpaceTreeSelectionsEvent(), + ); FetchOccupancyDataHelper.clearAllData(context); } } 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); }, ), ); 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), + ], + ); + } +} 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, ), ); 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) { diff --git a/lib/pages/routines/widgets/condition_toggle.dart b/lib/pages/routines/widgets/condition_toggle.dart index 99ea2f04..541ad431 100644 --- a/lib/pages/routines/widgets/condition_toggle.dart +++ b/lib/pages/routines/widgets/condition_toggle.dart @@ -12,22 +12,53 @@ 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: 30, + width: MediaQuery.of(context).size.width * 0.1, + decoration: BoxDecoration( + color: ColorsManager.softGray.withOpacity(0.5), + 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: InkWell( + onTap: () => onChanged(_conditions[index]), + child: AnimatedContainer( + duration: const Duration(milliseconds: 180), + curve: Curves.ease, + decoration: BoxDecoration( + color: + isSelected ? ColorsManager.vividBlue : Colors.transparent, + ), + child: Center( + child: Icon( + _icons[index], + size: 20, + color: isSelected + ? ColorsManager.whiteColors + : ColorsManager.blackColor, + weight: isSelected ? 700 : 500, + ), + ), + ), + ), + ); + }), ), - isSelected: _conditions.map((c) => c == (currentCondition ?? "==")).toList(), - children: _conditions.map((c) => Text(c)).toList(), ); } } 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..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 @@ -99,7 +99,27 @@ class _EnergyClampDialogState extends State { mainAxisSize: MainAxisSize.min, children: [ const DialogHeader('Energy Clamp Conditions'), - Expanded(child: _buildMainContent(context, state)), + Expanded( + 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), ], ), 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(); +} diff --git a/lib/utils/constants/assets.dart b/lib/utils/constants/assets.dart index 515ede28..5eb0eb05 100644 --- a/lib/utils/constants/assets.dart +++ b/lib/utils/constants/assets.dart @@ -452,9 +452,17 @@ 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'; + + 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'; + static const String temperatureAqiSidebar = 'assets/icons/thermometer.svg'; + static const String humidityAqiSidebar = 'assets/icons/humidity.svg'; } 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: