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/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]; +} 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..febbcf58 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart @@ -0,0 +1,42 @@ +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( + RangeOfAqiState( + status: RangeOfAqiStatus.loading, + rangeOfAqi: state.rangeOfAqi, + ), + ); + 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]; +} 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..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,8 +1,12 @@ 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'; 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 +16,18 @@ 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, + aqiType: AqiType.aqi, + ); } static void clearAllData(BuildContext context) { @@ -26,6 +37,8 @@ abstract final class FetchAirQualityDataHelper { context.read().add( const RealtimeDeviceChangesClosed(), ); + + context.read().add(const ClearRangeOfAqiEvent()); } static void loadAnalyticsDevices( @@ -49,4 +62,21 @@ abstract final class FetchAirQualityDataHelper { ), ); } + + static void loadRangeOfAqi( + 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/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..21cb2a9e --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart @@ -0,0 +1,115 @@ +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'; +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, List data) { + final titlesData = EnergyManagementChartsHelper.titlesData(context); + return titlesData.copyWith( + bottomTitles: titlesData.bottomTitles.copyWith( + sideTitles: titlesData.bottomTitles.sideTitles.copyWith( + 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, + ), + ), + ), + ), + ), + leftTitles: titlesData.leftTitles.copyWith( + sideTitles: titlesData.leftTitles.sideTitles.copyWith( + reservedSize: 70, + interval: 50, + maxIncluded: false, + getTitlesWidget: (value, meta) { + final text = value >= 300 ? '301+' : value.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, + ), + ), + ), + ); + }, + ), + ), + ); + } + + 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/views/air_quality_view.dart b/lib/pages/analytics/modules/air_quality/views/air_quality_view.dart index f2b485f7..9cf35128 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/air_quality_end_side_widget.dart b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart index 8c4bc9c1..0a91fc49 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 @@ -92,7 +92,6 @@ class AirQualityEndSideWidget extends StatelessWidget { Expanded( flex: 3, child: FittedBox( - fit: BoxFit.scaleDown, alignment: AlignmentDirectional.centerStart, child: SelectableText( 'AQI Sensor', @@ -106,9 +105,8 @@ class AirQualityEndSideWidget extends StatelessWidget { ), const Spacer(), Expanded( - flex: 2, + flex: 4, child: FittedBox( - fit: BoxFit.scaleDown, alignment: AlignmentDirectional.centerEnd, child: AnalyticsDeviceDropdown( onChanged: (value) { 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..ea85f075 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart @@ -0,0 +1,75 @@ +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'), + hcho('HCHO'), + tvoc('TVOC'), + co2('CO2'), + c6h6('C6H6'); + + 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); + + @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, + 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: const EdgeInsetsDirectional.symmetric( + horizontal: 12, + vertical: 2, + ), + 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: 12, + ); + } +} 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..08a036c0 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart @@ -0,0 +1,97 @@ +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 chartData; + + const RangeOfAqiChart({ + super.key, + 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, + ), + ]; + + @override + Widget build(BuildContext context) { + return LineChart( + LineChartData( + minY: 0, + maxY: 301, + clipData: const FlClipData.vertical(), + gridData: EnergyManagementChartsHelper.gridData(horizontalInterval: 50), + titlesData: RangeOfAqiChartsHelper.titlesData(context, chartData), + borderData: EnergyManagementChartsHelper.borderData(), + lineTouchData: RangeOfAqiChartsHelper.lineTouchData(chartData), + betweenBarsData: [ + BetweenBarsData( + fromIndex: 0, + toIndex: 2, + 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.6); + }).toList(), + ), + ), + ], + lineBarsData: _lines.map((e) { + final (values, color, dotColor) = e; + return _buildLine(values: values, color: color, dotColor: dotColor); + }).toList(), + ), + duration: Duration.zero, + ); + } + + FlDotData _buildDotData(Color color) { + return FlDotData( + show: true, + getDotPainter: (_, __, ___, ____) => FlDotCirclePainter( + radius: 2, + color: ColorsManager.whiteColors, + strokeWidth: 2, + strokeColor: color, + ), + ); + } + + LineChartBarData _buildLine({ + required List values, + required Color color, + Color? dotColor, + }) { + const invisibleDot = FlDotData(show: false); + 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) : invisibleDot, + belowBarData: BarAreaData(show: false), + ); + } +} 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..0fe4c4bd --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart @@ -0,0 +1,42 @@ +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/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'; + +class RangeOfAqiChartBox extends StatelessWidget { + const RangeOfAqiChartBox({super.key}); + + @override + Widget build(BuildContext context) { + 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), + ], + RangeOfAqiChartTitle( + isLoading: state.status == RangeOfAqiStatus.loading, + ), + const SizedBox(height: 10), + const Divider(), + const SizedBox(height: 20), + Expanded(child: RangeOfAqiChart(chartData: state.rangeOfAqi)), + ], + ), + ); + }, + ); + } +} 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..04cefd6c --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart @@ -0,0 +1,82 @@ +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/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}); + final bool isLoading; + + 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( + children: [ + ChartsLoadingWidget(isLoading: isLoading), + 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 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 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; + + FetchAirQualityDataHelper.loadRangeOfAqi( + context, + spaceUuid: spaceUuid, + date: context.read().state.monthlyDate, + aqiType: value ?? AqiType.aqi, + ); + }, + ), + ), + ), + ], + ); + } +} 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(), ); 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..eec31998 --- /dev/null +++ b/lib/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart @@ -0,0 +1,59 @@ +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.0385, + 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, + ), + ), + DefaultTextStyle( + style: const TextStyle( + color: ColorsManager.blackColor, + fontWeight: FontWeight.w400, + fontSize: 14, + ), + child: title, + ), + ], + ), + ), + ); + } +} 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 11c088e8..b1af85c8 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, 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 001f4d2c..52c6f591 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/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), ); } } 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..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, + ), ); } @@ -35,9 +37,6 @@ class TotalEnergyConsumptionChart extends StatelessWidget { List get _lineBarsData { return [ LineChartBarData( - preventCurveOvershootingThreshold: 0.1, - curveSmoothness: 0.55, - preventCurveOverShooting: true, spots: chartData .asMap() .entries 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..bbf24658 --- /dev/null +++ b/lib/pages/analytics/params/get_range_of_aqi_param.dart @@ -0,0 +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( + { + required this.date, + required this.spaceUuid, + required this.aqiType, + }); + + @override + List get props => [date, spaceUuid]; +} 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..13173c94 --- /dev/null +++ b/lib/pages/analytics/services/range_of_aqi/fake_range_of_aqi_service.dart @@ -0,0 +1,30 @@ +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 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)); + + 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: 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 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/color_manager.dart b/lib/utils/color_manager.dart index 5a892aa6..41ceb29a 100644 --- a/lib/utils/color_manager.dart +++ b/lib/utils/color_manager.dart @@ -73,4 +73,14 @@ 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); + static const Color maxPurple = Color(0xFF962DFF); + static const Color maxPurpleDot = Color(0xFF5F00BD); + static const Color minBlue = Color(0xFF93AAFD); + static const Color minBlueDot = Color(0xFF023DFE); } diff --git a/lib/utils/constants/assets.dart b/lib/utils/constants/assets.dart index 51053c9f..13d51ea5 100644 --- a/lib/utils/constants/assets.dart +++ b/lib/utils/constants/assets.dart @@ -448,5 +448,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, };