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/close_settings_icon.svg b/assets/icons/close_settings_icon.svg new file mode 100644 index 00000000..93e615d8 --- /dev/null +++ b/assets/icons/close_settings_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/edit_name_icon_settings.svg b/assets/icons/edit_name_icon_settings.svg new file mode 100644 index 00000000..54bee0af --- /dev/null +++ b/assets/icons/edit_name_icon_settings.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/pages/analytics/models/air_quality_data_model.dart b/lib/pages/analytics/models/air_quality_data_model.dart new file mode 100644 index 00000000..2eab2ddb --- /dev/null +++ b/lib/pages/analytics/models/air_quality_data_model.dart @@ -0,0 +1,57 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class AirQualityDataModel extends Equatable { + const AirQualityDataModel({ + required this.date, + required this.data, + }); + + final DateTime date; + final List data; + + factory AirQualityDataModel.fromJson(Map json) { + return AirQualityDataModel( + date: DateTime.parse(json['date'] as String), + data: (json['data'] as List) + .map((e) => AirQualityPercentageData.fromJson(e as Map)) + .toList(), + ); + } + + static final Map metricColors = { + 'good': ColorsManager.goodGreen.withValues(alpha: 0.7), + 'moderate': ColorsManager.moderateYellow.withValues(alpha: 0.7), + 'poor': ColorsManager.poorOrange.withValues(alpha: 0.7), + 'unhealthy': ColorsManager.unhealthyRed.withValues(alpha: 0.7), + 'severe': ColorsManager.severePink.withValues(alpha: 0.7), + 'hazardous': ColorsManager.hazardousPurple.withValues(alpha: 0.7), + }; + + @override + List get props => [date, data]; +} + +class AirQualityPercentageData extends Equatable { + const AirQualityPercentageData({ + required this.type, + required this.name, + required this.percentage, + }); + + final String type; + final String name; + final double percentage; + + factory AirQualityPercentageData.fromJson(Map json) { + return AirQualityPercentageData( + type: json['type'] as String? ?? '', + name: json['name'] as String? ?? '', + percentage: (json['percentage'] as num?)?.toDouble() ?? 0, + ); + } + + @override + List get props => [type, name, percentage]; +} diff --git a/lib/pages/analytics/models/analytics_device.dart b/lib/pages/analytics/models/analytics_device.dart index 88f18ec5..eaac8b2b 100644 --- a/lib/pages/analytics/models/analytics_device.dart +++ b/lib/pages/analytics/models/analytics_device.dart @@ -23,17 +23,18 @@ class AnalyticsDevice { return AnalyticsDevice( uuid: json['uuid'] as String, name: json['name'] as String, - createdAt: json['createdAt'] != null ? DateTime.parse(json['createdAt'] as String) : null, - updatedAt: json['updatedAt'] != null ? DateTime.parse(json['updatedAt'] as String) : null, + createdAt: json['createdAt'] != null + ? DateTime.parse(json['createdAt'] as String) + : null, + updatedAt: json['updatedAt'] != null + ? DateTime.parse(json['updatedAt'] as String) + : null, deviceTuyaUuid: json['deviceTuyaUuid'] as String?, isActive: json['isActive'] as bool?, - productDevice: json['productDevice'] != null - ? ProductDevice.fromJson(json['productDevice'] as Map) - : null, - spaceUuid: (json['spaces'] as List?) - ?.map((e) => e['uuid']) - .firstOrNull - ?.toString(), + productDevice: json['productDevice'] != null + ? ProductDevice.fromJson(json['productDevice'] as Map) + : null, + spaceUuid: json['spaceUuid'] as String?, ); } } @@ -60,8 +61,12 @@ class ProductDevice { factory ProductDevice.fromJson(Map json) { return ProductDevice( uuid: json['uuid'] as String?, - createdAt: json['createdAt'] != null ? DateTime.parse(json['createdAt'] as String) : null, - updatedAt: json['updatedAt'] != null ? DateTime.parse(json['updatedAt'] as String) : null, + createdAt: json['createdAt'] != null + ? DateTime.parse(json['createdAt'] as String) + : null, + updatedAt: json['updatedAt'] != null + ? DateTime.parse(json['updatedAt'] as String) + : null, catName: json['catName'] as String?, prodId: json['prodId'] as String?, name: json['name'] as String?, diff --git a/lib/pages/analytics/models/range_of_aqi.dart b/lib/pages/analytics/models/range_of_aqi.dart index 759666c2..0308d564 100644 --- a/lib/pages/analytics/models/range_of_aqi.dart +++ b/lib/pages/analytics/models/range_of_aqi.dart @@ -1,18 +1,49 @@ import 'package:equatable/equatable.dart'; class RangeOfAqi extends Equatable { - final double min; - final double avg; - final double max; final DateTime date; + final List data; const RangeOfAqi({ - required this.min, - required this.avg, - required this.max, + required this.data, required this.date, }); + factory RangeOfAqi.fromJson(Map json) { + return RangeOfAqi( + date: DateTime.parse(json['date'] as String), + data: (json['data'] as List) + .map((e) => RangeOfAqiValue.fromJson(e as Map)) + .toList(), + ); + } + @override - List get props => [min, avg, max, date]; + List get props => [data, date]; +} + +class RangeOfAqiValue extends Equatable { + final String type; + final double min; + final double average; + final double max; + + const RangeOfAqiValue({ + required this.type, + required this.min, + required this.average, + required this.max, + }); + + factory RangeOfAqiValue.fromJson(Map json) { + return RangeOfAqiValue( + type: json['type'] as String, + min: (json['min'] as num).toDouble(), + average: (json['average'] as num).toDouble(), + max: (json['max'] as num).toDouble(), + ); + } + + @override + List get props => [type, min, average, max]; } diff --git a/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart b/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart new file mode 100644 index 00000000..fb7e2352 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart @@ -0,0 +1,81 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart'; +import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart'; +import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/air_quality_distribution_service.dart'; + +part 'air_quality_distribution_event.dart'; +part 'air_quality_distribution_state.dart'; + +class AirQualityDistributionBloc + extends Bloc { + final AirQualityDistributionService _aqiDistributionService; + + AirQualityDistributionBloc( + this._aqiDistributionService, + ) : super(const AirQualityDistributionState()) { + on(_onLoadAirQualityDistribution); + on(_onClearAirQualityDistribution); + on(_onUpdateAqiTypeEvent); + } + + Future _onLoadAirQualityDistribution( + LoadAirQualityDistribution event, + Emitter emit, + ) async { + try { + emit(state.copyWith(status: AirQualityDistributionStatus.loading)); + final result = await _aqiDistributionService.getAirQualityDistribution( + event.param, + ); + emit( + state.copyWith( + status: AirQualityDistributionStatus.success, + chartData: result, + filteredChartData: _arrangeChartDataByType(result, state.selectedAqiType), + ), + ); + } catch (e) { + emit( + AirQualityDistributionState( + status: AirQualityDistributionStatus.failure, + errorMessage: e.toString(), + selectedAqiType: state.selectedAqiType, + ), + ); + } + } + + Future _onClearAirQualityDistribution( + ClearAirQualityDistribution event, + Emitter emit, + ) async { + emit(const AirQualityDistributionState()); + } + + void _onUpdateAqiTypeEvent( + UpdateAqiTypeEvent event, + Emitter emit, + ) { + emit( + state.copyWith( + selectedAqiType: event.aqiType, + filteredChartData: _arrangeChartDataByType(state.chartData, event.aqiType), + ), + ); + } + + List _arrangeChartDataByType( + List data, + AqiType aqiType, + ) { + final filteredData = data.map( + (data) => AirQualityDataModel( + date: data.date, + data: data.data.where((value) => value.type == aqiType.code).toList(), + ), + ); + return filteredData.toList(); + } +} diff --git a/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_event.dart b/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_event.dart new file mode 100644 index 00000000..b91dafe5 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_event.dart @@ -0,0 +1,30 @@ +part of 'air_quality_distribution_bloc.dart'; + +sealed class AirQualityDistributionEvent extends Equatable { + const AirQualityDistributionEvent(); + + @override + List get props => []; +} + +final class LoadAirQualityDistribution extends AirQualityDistributionEvent { + final GetAirQualityDistributionParam param; + + const LoadAirQualityDistribution(this.param); + + @override + List get props => [param]; +} + +final class UpdateAqiTypeEvent extends AirQualityDistributionEvent { + const UpdateAqiTypeEvent(this.aqiType); + + final AqiType aqiType; + + @override + List get props => [aqiType]; +} + +final class ClearAirQualityDistribution extends AirQualityDistributionEvent { + const ClearAirQualityDistribution(); +} diff --git a/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_state.dart b/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_state.dart new file mode 100644 index 00000000..65665882 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_state.dart @@ -0,0 +1,43 @@ +part of 'air_quality_distribution_bloc.dart'; + +enum AirQualityDistributionStatus { + initial, + loading, + success, + failure, +} + +class AirQualityDistributionState extends Equatable { + const AirQualityDistributionState({ + this.status = AirQualityDistributionStatus.initial, + this.chartData = const [], + this.filteredChartData = const [], + this.errorMessage, + this.selectedAqiType = AqiType.aqi, + }); + + final AirQualityDistributionStatus status; + final List chartData; + final List filteredChartData; + final String? errorMessage; + final AqiType selectedAqiType; + + AirQualityDistributionState copyWith({ + AirQualityDistributionStatus? status, + List? chartData, + List? filteredChartData, + String? errorMessage, + AqiType? selectedAqiType, + }) { + return AirQualityDistributionState( + status: status ?? this.status, + chartData: chartData ?? this.chartData, + filteredChartData: filteredChartData ?? this.filteredChartData, + errorMessage: errorMessage ?? this.errorMessage, + selectedAqiType: selectedAqiType ?? this.selectedAqiType, + ); + } + + @override + List get props => [status, chartData, errorMessage, selectedAqiType]; +} diff --git a/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart b/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart index febbcf58..88c3715e 100644 --- a/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart +++ b/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart @@ -1,6 +1,7 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:syncrow_web/pages/analytics/models/range_of_aqi.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart'; import 'package:syncrow_web/pages/analytics/params/get_range_of_aqi_param.dart'; import 'package:syncrow_web/pages/analytics/services/range_of_aqi/range_of_aqi_service.dart'; @@ -11,6 +12,7 @@ class RangeOfAqiBloc extends Bloc { RangeOfAqiBloc(this._rangeOfAqiService) : super(const RangeOfAqiState()) { on(_onLoadRangeOfAqiEvent); on(_onClearRangeOfAqiEvent); + on(_onUpdateAqiTypeEvent); } final RangeOfAqiService _rangeOfAqiService; @@ -20,19 +22,55 @@ class RangeOfAqiBloc extends Bloc { Emitter emit, ) async { emit( - RangeOfAqiState( - status: RangeOfAqiStatus.loading, - rangeOfAqi: state.rangeOfAqi, - ), + state.copyWith(status: RangeOfAqiStatus.loading), ); try { final rangeOfAqi = await _rangeOfAqiService.load(event.param); - emit(RangeOfAqiState(status: RangeOfAqiStatus.loaded, rangeOfAqi: rangeOfAqi)); + emit( + state.copyWith( + status: RangeOfAqiStatus.loaded, + rangeOfAqi: rangeOfAqi, + filteredRangeOfAqi: _arrangeChartDataByType( + rangeOfAqi, + state.selectedAqiType, + ), + ), + ); } catch (e) { - emit(RangeOfAqiState(status: RangeOfAqiStatus.failure, errorMessage: '$e')); + emit( + state.copyWith( + status: RangeOfAqiStatus.failure, + errorMessage: '$e', + ), + ); } } + void _onUpdateAqiTypeEvent( + UpdateAqiTypeEvent event, + Emitter emit, + ) { + emit( + state.copyWith( + selectedAqiType: event.aqiType, + filteredRangeOfAqi: _arrangeChartDataByType(state.rangeOfAqi, event.aqiType), + ), + ); + } + + List _arrangeChartDataByType( + List rangeOfAqi, + AqiType aqiType, + ) { + final filteredRangeOfAqi = rangeOfAqi.map( + (data) => RangeOfAqi( + date: data.date, + data: data.data.where((value) => value.type == aqiType.code).toList(), + ), + ); + return filteredRangeOfAqi.toList(); + } + void _onClearRangeOfAqiEvent( ClearRangeOfAqiEvent event, Emitter emit, diff --git a/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_event.dart b/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_event.dart index 8a429587..6a08df5b 100644 --- a/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_event.dart +++ b/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_event.dart @@ -16,6 +16,15 @@ class LoadRangeOfAqiEvent extends RangeOfAqiEvent { List get props => [param]; } +class UpdateAqiTypeEvent extends RangeOfAqiEvent { + const UpdateAqiTypeEvent(this.aqiType); + + final AqiType aqiType; + + @override + List get props => [aqiType]; +} + class ClearRangeOfAqiEvent extends RangeOfAqiEvent { const ClearRangeOfAqiEvent(); } diff --git a/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_state.dart b/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_state.dart index 392e98c1..9308020c 100644 --- a/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_state.dart +++ b/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_state.dart @@ -5,14 +5,35 @@ enum RangeOfAqiStatus { initial, loading, loaded, failure } final class RangeOfAqiState extends Equatable { const RangeOfAqiState({ this.rangeOfAqi = const [], + this.filteredRangeOfAqi = const [], this.status = RangeOfAqiStatus.initial, this.errorMessage, + this.selectedAqiType = AqiType.aqi, }); final RangeOfAqiStatus status; final List rangeOfAqi; + final List filteredRangeOfAqi; final String? errorMessage; + final AqiType selectedAqiType; + + RangeOfAqiState copyWith({ + RangeOfAqiStatus? status, + List? rangeOfAqi, + List? filteredRangeOfAqi, + String? errorMessage, + AqiType? selectedAqiType, + }) { + return RangeOfAqiState( + status: status ?? this.status, + rangeOfAqi: rangeOfAqi ?? this.rangeOfAqi, + filteredRangeOfAqi: filteredRangeOfAqi ?? this.filteredRangeOfAqi, + errorMessage: errorMessage ?? this.errorMessage, + selectedAqiType: selectedAqiType ?? this.selectedAqiType, + ); + } @override - List get props => [status, rangeOfAqi, errorMessage]; + List get props => + [status, rangeOfAqi, filteredRangeOfAqi, errorMessage, selectedAqiType]; } diff --git a/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart b/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart index aaffc3fd..7f26bb5a 100644 --- a/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart +++ b/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart @@ -1,11 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/device_location/device_location_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart'; -import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart'; import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart'; +import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart'; import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart'; import 'package:syncrow_web/pages/analytics/params/get_device_location_data_param.dart'; import 'package:syncrow_web/pages/analytics/params/get_range_of_aqi_param.dart'; @@ -15,8 +16,10 @@ abstract final class FetchAirQualityDataHelper { static void loadAirQualityData( BuildContext context, { + required DateTime date, required String communityUuid, required String spaceUuid, + bool shouldFetchAnalyticsDevices = true, }) { final date = context.read().state.monthlyDate; loadAnalyticsDevices( @@ -28,7 +31,11 @@ abstract final class FetchAirQualityDataHelper { context, spaceUuid: spaceUuid, date: date, - aqiType: AqiType.aqi, + ); + loadAirQualityDistribution( + context, + spaceUuid: spaceUuid, + date: date, ); } @@ -39,7 +46,9 @@ abstract final class FetchAirQualityDataHelper { context.read().add( const RealtimeDeviceChangesClosed(), ); - + context.read().add( + const ClearAirQualityDistribution(), + ); context.read().add(const ClearRangeOfAqiEvent()); context.read().add(const ClearDeviceLocationEvent()); @@ -80,16 +89,26 @@ abstract final class FetchAirQualityDataHelper { BuildContext context, { required String spaceUuid, required DateTime date, - required AqiType aqiType, }) { context.read().add( LoadRangeOfAqiEvent( GetRangeOfAqiParam( date: date, spaceUuid: spaceUuid, - aqiType: aqiType, ), ), ); } + + static void loadAirQualityDistribution( + BuildContext context, { + required String spaceUuid, + required DateTime date, + }) { + context.read().add( + LoadAirQualityDistribution( + GetAirQualityDistributionParam(spaceUuid: spaceUuid, date: date), + ), + ); + } } diff --git a/lib/pages/analytics/modules/air_quality/views/air_quality_view.dart b/lib/pages/analytics/modules/air_quality/views/air_quality_view.dart index 17ecbc22..b6d403eb 100644 --- a/lib/pages/analytics/modules/air_quality/views/air_quality_view.dart +++ b/lib/pages/analytics/modules/air_quality/views/air_quality_view.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart'; class AirQualityView extends StatelessWidget { @@ -23,8 +24,14 @@ class AirQualityView extends StatelessWidget { height: height * 1.2, child: const AirQualityEndSideWidget(), ), - SizedBox(height: height * 0.5, child: const RangeOfAqiChartBox()), - SizedBox(height: height * 0.5, child: const Placeholder()), + SizedBox( + height: height * 0.5, + child: const RangeOfAqiChartBox(), + ), + SizedBox( + height: height * 0.5, + child: const AqiDistributionChartBox(), + ), ], ), ); @@ -46,7 +53,7 @@ class AirQualityView extends StatelessWidget { spacing: 20, children: [ Expanded(child: RangeOfAqiChartBox()), - Expanded(child: Placeholder()), + Expanded(child: AqiDistributionChartBox()), ], ), ), diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart new file mode 100644 index 00000000..373e36ca --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart @@ -0,0 +1,174 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart'; +import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class AqiDistributionChart extends StatelessWidget { + const AqiDistributionChart({super.key, required this.chartData}); + final List chartData; + + static const _rodStackItemsSpacing = 0.4; + static const _barWidth = 13.0; + static final _barBorderRadius = BorderRadius.circular(22); + + @override + Widget build(BuildContext context) { + final sortedData = List.from(chartData) + ..sort( + (a, b) => a.date.compareTo(b.date), + ); + + return BarChart( + BarChartData( + maxY: 100.1, + gridData: EnergyManagementChartsHelper.gridData( + horizontalInterval: 20, + ), + borderData: EnergyManagementChartsHelper.borderData(), + barTouchData: _barTouchData(context), + titlesData: _titlesData(context), + barGroups: _buildBarGroups(sortedData), + ), + duration: Duration.zero, + ); + } + + List _buildBarGroups(List sortedData) { + return List.generate(sortedData.length, (index) { + final data = sortedData[index]; + final stackItems = []; + double currentY = 0; + bool isFirstElement = true; + + // Sort data by type to ensure consistent order + final sortedPercentageData = List.from(data.data) + ..sort((a, b) => a.type.compareTo(b.type)); + + for (final percentageData in sortedPercentageData) { + stackItems.add( + BarChartRodData( + fromY: currentY, + toY: currentY + percentageData.percentage , + color: AirQualityDataModel.metricColors[percentageData.name]!, + borderRadius: isFirstElement + ? const BorderRadius.only( + topLeft: Radius.circular(22), + topRight: Radius.circular(22), + ) + : _barBorderRadius, + width: _barWidth, + ), + ); + currentY += percentageData.percentage + _rodStackItemsSpacing; + isFirstElement = false; + } + + return BarChartGroupData( + x: index, + barRods: stackItems, + groupVertically: true, + ); + }); + } + + BarTouchData _barTouchData(BuildContext context) { + return BarTouchData( + touchTooltipData: BarTouchTooltipData( + getTooltipColor: (_) => ColorsManager.whiteColors, + tooltipBorder: const BorderSide( + color: ColorsManager.semiTransparentBlack, + ), + tooltipRoundedRadius: 16, + tooltipPadding: const EdgeInsets.all(8), + getTooltipItem: (group, groupIndex, rod, rodIndex) { + final data = chartData[group.x.toInt()]; + + final List children = []; + + final textStyle = context.textTheme.bodySmall?.copyWith( + color: ColorsManager.blackColor, + fontSize: 12, + ); + + // Sort data by type to ensure consistent order + final sortedPercentageData = List.from(data.data) + ..sort((a, b) => a.type.compareTo(b.type)); + + for (final percentageData in sortedPercentageData) { + children.add(TextSpan( + text: + '\n${percentageData.type.toUpperCase()}: ${percentageData.percentage.toStringAsFixed(1)}%', + style: textStyle, + )); + } + + return BarTooltipItem( + DateFormat('dd/MM/yyyy').format(data.date), + context.textTheme.bodyMedium!.copyWith( + color: ColorsManager.blackColor, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + children: children, + ); + }, + ), + ); + } + + FlTitlesData _titlesData(BuildContext context) { + final titlesData = EnergyManagementChartsHelper.titlesData( + context, + leftTitlesInterval: 20, + ); + + final leftTitles = titlesData.leftTitles.copyWith( + sideTitles: titlesData.leftTitles.sideTitles.copyWith( + reservedSize: 70, + interval: 20, + maxIncluded: false, + minIncluded: true, + getTitlesWidget: (value, meta) => Padding( + padding: const EdgeInsetsDirectional.only(end: 12), + child: FittedBox( + alignment: AlignmentDirectional.centerStart, + fit: BoxFit.scaleDown, + child: Text( + '${value.toStringAsFixed(0)}%', + style: context.textTheme.bodySmall?.copyWith( + fontSize: 12, + color: ColorsManager.lightGreyColor, + ), + ), + ), + ), + ), + ); + + final bottomTitles = AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (value, _) => FittedBox( + alignment: AlignmentDirectional.bottomCenter, + fit: BoxFit.scaleDown, + child: Text( + chartData[value.toInt()].date.day.toString(), + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.lightGreyColor, + fontSize: 8, + ), + ), + ), + reservedSize: 36, + ), + ); + + return titlesData.copyWith( + leftTitles: leftTitles, + bottomTitles: bottomTitles, + ); + } +} diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart new file mode 100644 index 00000000..8a57fe0b --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart'; +import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart'; +import 'package:syncrow_web/utils/style.dart'; + +class AqiDistributionChartBox extends StatelessWidget { + const AqiDistributionChartBox({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), + ], + AqiDistributionChartTitle( + isLoading: state.status == AirQualityDistributionStatus.loading, + ), + const SizedBox(height: 10), + const Divider(), + const SizedBox(height: 20), + Expanded( + child: AqiDistributionChart(chartData: state.filteredChartData), + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart new file mode 100644 index 00000000..e32043c5 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart'; +import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart'; +import 'package:syncrow_web/pages/analytics/widgets/charts_loading_widget.dart'; + +class AqiDistributionChartTitle extends StatelessWidget { + const AqiDistributionChartTitle({required this.isLoading, super.key}); + + final bool isLoading; + + @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('Distribution over Air Quality Index'), + ), + ), + ), + FittedBox( + alignment: AlignmentDirectional.centerEnd, + fit: BoxFit.scaleDown, + child: AqiTypeDropdown( + onChanged: (value) { + if (value != null) { + context + .read() + .add(UpdateAqiTypeEvent(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 index c725d1fa..60a686ff 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart @@ -3,17 +3,18 @@ import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; enum AqiType { - aqi('AQI', ''), - pm25('PM2.5', 'µg/m³'), - pm10('PM10', 'µg/m³'), - hcho('HCHO', 'mg/m³'), - tvoc('TVOC', 'µg/m³'), - co2('CO2', 'ppm'); + aqi('AQI', '', 'aqi'), + pm25('PM2.5', 'µg/m³', 'pm25'), + pm10('PM10', 'µg/m³', 'pm10'), + hcho('HCHO', 'mg/m³', 'hcho'), + tvoc('TVOC', 'µg/m³', 'tvoc'), + co2('CO2', 'ppm', 'co2'); - const AqiType(this.value, this.unit); + const AqiType(this.value, this.unit, this.code); final String value; final String unit; + final String code; } class AqiTypeDropdown extends StatefulWidget { diff --git a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart index 08a036c0..fc63e413 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart @@ -13,23 +13,37 @@ class RangeOfAqiChart extends StatelessWidget { required this.chartData, }); - List<(List values, Color color, Color? dotColor)> get _lines => [ - ( - chartData.map((e) => e.max).toList(), - ColorsManager.maxPurple, - ColorsManager.maxPurpleDot, - ), - ( - chartData.map((e) => e.avg).toList(), - Colors.white, - null, - ), - ( - chartData.map((e) => e.min).toList(), - ColorsManager.minBlue, - ColorsManager.minBlueDot, - ), - ]; + List<(List values, Color color, Color? dotColor)> get _lines { + final sortedData = List.from(chartData) + ..sort((a, b) => a.date.compareTo(b.date)); + + return [ + ( + sortedData.map((e) { + final value = e.data.firstOrNull; + return value?.max ?? 0; + }).toList(), + ColorsManager.maxPurple, + ColorsManager.maxPurpleDot, + ), + ( + sortedData.map((e) { + final value = e.data.firstOrNull; + return value?.average ?? 0; + }).toList(), + Colors.white, + null, + ), + ( + sortedData.map((e) { + final value = e.data.firstOrNull; + return value?.min ?? 0; + }).toList(), + ColorsManager.minBlue, + ColorsManager.minBlueDot, + ), + ]; + } @override Widget build(BuildContext context) { diff --git a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart index 0fe4c4bd..6548c696 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart @@ -32,7 +32,7 @@ class RangeOfAqiChartBox extends StatelessWidget { const SizedBox(height: 10), const Divider(), const SizedBox(height: 20), - Expanded(child: RangeOfAqiChart(chartData: state.rangeOfAqi)), + Expanded(child: RangeOfAqiChart(chartData: state.filteredRangeOfAqi)), ], ), ); diff --git a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart index 04cefd6c..1b0da288 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart @@ -1,15 +1,18 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart'; -import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart'; import 'package:syncrow_web/pages/analytics/widgets/charts_loading_widget.dart'; import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart'; class RangeOfAqiChartTitle extends StatelessWidget { - const RangeOfAqiChartTitle({required this.isLoading, super.key}); + const RangeOfAqiChartTitle({ + required this.isLoading, + super.key, + }); + final bool isLoading; static const List<(Color color, String title, bool hasBorder)> _colors = [ @@ -66,12 +69,9 @@ class RangeOfAqiChartTitle extends StatelessWidget { if (spaceUuid == null) return; - FetchAirQualityDataHelper.loadRangeOfAqi( - context, - spaceUuid: spaceUuid, - date: context.read().state.monthlyDate, - aqiType: value ?? AqiType.aqi, - ); + if (value != null) { + context.read().add(UpdateAqiTypeEvent(value)); + } }, ), ), diff --git a/lib/pages/analytics/modules/analytics/strategies/air_quality_data_loading_strategy.dart b/lib/pages/analytics/modules/analytics/strategies/air_quality_data_loading_strategy.dart index dc3b1c5e..8b1802af 100644 --- a/lib/pages/analytics/modules/analytics/strategies/air_quality_data_loading_strategy.dart +++ b/lib/pages/analytics/modules/analytics/strategies/air_quality_data_loading_strategy.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart'; +import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/analytics/strategies/analytics_data_loading_strategy.dart'; import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart'; import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart'; @@ -26,10 +27,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()) @@ -39,21 +40,15 @@ final class AirQualityDataLoadingStrategy implements AnalyticsDataLoadingStrateg context, communityUuid: community.uuid, spaceUuid: space.uuid ?? '', + date: context.read().state.monthlyDate, ); } - @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/analytics/widgets/analytics_page_tabs_and_children.dart b/lib/pages/analytics/modules/analytics/widgets/analytics_page_tabs_and_children.dart index 5e9e347a..f6197e46 100644 --- a/lib/pages/analytics/modules/analytics/widgets/analytics_page_tabs_and_children.dart +++ b/lib/pages/analytics/modules/analytics/widgets/analytics_page_tabs_and_children.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart'; import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_tab/analytics_tab_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/analytics/enums/analytics_page_tab.dart'; @@ -56,33 +57,16 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget { const Spacer(), Visibility( key: ValueKey(selectedTab), - visible: selectedTab == AnalyticsPageTab.energyManagement, + visible: selectedTab == AnalyticsPageTab.energyManagement || + selectedTab == AnalyticsPageTab.airQuality, child: Expanded( flex: 2, child: FittedBox( fit: BoxFit.scaleDown, alignment: AlignmentDirectional.centerEnd, child: AnalyticsDateFilterButton( - onDateSelected: (DateTime value) { - context.read().add( - UpdateAnalyticsDatePickerEvent(montlyDate: value), - ); - - final spaceTreeState = - context.read().state; - if (spaceTreeState.selectedSpaces.isNotEmpty) { - FetchEnergyManagementDataHelper - .loadEnergyManagementData( - context, - shouldFetchAnalyticsDevices: false, - selectedDate: value, - communityId: - spaceTreeState.selectedCommunities.firstOrNull ?? - '', - spaceId: - spaceTreeState.selectedSpaces.firstOrNull ?? '', - ); - } + onDateSelected: (value) { + _onDateChanged(context, value, selectedTab); }, selectedDate: context .watch() @@ -112,4 +96,73 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget { child: child, ); } + + void _onDateChanged( + BuildContext context, + DateTime date, + AnalyticsPageTab selectedTab, + ) { + context.read().add( + UpdateAnalyticsDatePickerEvent(montlyDate: date), + ); + + final spaceTreeState = context.read().state; + final communities = spaceTreeState.selectedCommunities; + final spaces = spaceTreeState.selectedSpaces; + if (spaceTreeState.selectedSpaces.isNotEmpty) { + switch (selectedTab) { + case AnalyticsPageTab.energyManagement: + _onEnergyManagementDateChanged( + context, + date: date, + communityUuid: communities.firstOrNull ?? '', + spaceUuid: spaces.firstOrNull ?? '', + ); + break; + case AnalyticsPageTab.airQuality: + _onAirQualityDateChanged( + context, + date: date, + communityUuid: communities.firstOrNull ?? '', + spaceUuid: spaces.firstOrNull ?? '', + ); + default: + break; + } + } + } + + void _onEnergyManagementDateChanged( + BuildContext context, { + required DateTime date, + required String communityUuid, + required String spaceUuid, + }) { + context.read().add( + UpdateAnalyticsDatePickerEvent(montlyDate: date), + ); + + FetchEnergyManagementDataHelper.loadEnergyManagementData( + context, + shouldFetchAnalyticsDevices: false, + selectedDate: date, + communityId: communityUuid, + spaceId: spaceUuid, + ); + } + + void _onAirQualityDateChanged( + BuildContext context, { + required DateTime date, + required String communityUuid, + required String spaceUuid, + }) { + FetchAirQualityDataHelper.loadAirQualityData( + context, + date: date, + communityUuid: communityUuid, + spaceUuid: spaceUuid, + shouldFetchAnalyticsDevices: false, + ); + } } diff --git a/lib/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart b/lib/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart index b1af85c8..2ed68e76 100644 --- a/lib/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart +++ b/lib/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart @@ -38,7 +38,7 @@ abstract final class EnergyManagementChartsHelper { sideTitles: SideTitles( showTitles: true, maxIncluded: false, - minIncluded: false, + minIncluded: true, interval: leftTitlesInterval, reservedSize: 110, getTitlesWidget: (value, meta) => Padding( diff --git a/lib/pages/analytics/modules/energy_management/helpers/fetch_energy_management_data_helper.dart b/lib/pages/analytics/modules/energy_management/helpers/fetch_energy_management_data_helper.dart index a6fe4703..8de92098 100644 --- a/lib/pages/analytics/modules/energy_management/helpers/fetch_energy_management_data_helper.dart +++ b/lib/pages/analytics/modules/energy_management/helpers/fetch_energy_management_data_helper.dart @@ -16,7 +16,6 @@ import 'package:syncrow_web/pages/analytics/params/get_total_energy_consumption_ abstract final class FetchEnergyManagementDataHelper { const FetchEnergyManagementDataHelper._(); - // static const String _powerClampId = 'cb71d6ad-6e29-4eaa-ae3e-1a0d1c5f60fa'; static AnalyticsDevice? getSelectedDevice(BuildContext context) { return context.read().state.selectedDevice; } @@ -48,7 +47,6 @@ abstract final class FetchEnergyManagementDataHelper { loadTotalEnergyConsumption( context, selectedDate: selectedDate0, - communityId: communityId, spaceId: spaceId, ); final selectedDevice = getSelectedDevice(context); @@ -61,7 +59,6 @@ abstract final class FetchEnergyManagementDataHelper { } loadEnergyConsumptionPerDevice( context, - communityId: communityId, spaceId: spaceId, selectedDate: selectedDate0, ); @@ -84,12 +81,10 @@ abstract final class FetchEnergyManagementDataHelper { static void loadTotalEnergyConsumption( BuildContext context, { DateTime? selectedDate, - required String communityId, required String spaceId, }) { final param = GetTotalEnergyConsumptionParam( spaceId: spaceId, - communityId: communityId, monthDate: selectedDate, ); context.read().add( @@ -100,12 +95,10 @@ abstract final class FetchEnergyManagementDataHelper { static void loadEnergyConsumptionPerDevice( BuildContext context, { DateTime? selectedDate, - required String communityId, required String spaceId, }) { final param = GetEnergyConsumptionPerDeviceParam( spaceId: spaceId, - communityId: communityId, monthDate: selectedDate, ); context.read().add( diff --git a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart_box.dart b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart_box.dart index f22517d5..be5faf57 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart_box.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart_box.dart @@ -23,7 +23,6 @@ class EnergyConsumptionPerDeviceChartBox extends StatelessWidget { ), padding: const EdgeInsets.all(30), child: Column( - spacing: 20, crossAxisAlignment: CrossAxisAlignment.start, children: [ AnalyticsErrorWidget(state.errorMessage), @@ -52,7 +51,9 @@ class EnergyConsumptionPerDeviceChartBox extends StatelessWidget { ), ], ), + const SizedBox(height: 20), const Divider(height: 0), + const SizedBox(height: 20), Expanded( child: EnergyConsumptionPerDeviceChart(chartData: state.chartData), ), diff --git a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_devices_list.dart b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_devices_list.dart index b7205424..f0cb5d64 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_devices_list.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_devices_list.dart @@ -41,7 +41,7 @@ class EnergyConsumptionPerDeviceDevicesList extends StatelessWidget { .color; return Tooltip( - message: '${device.name}\n${device.productDevice?.uuid ?? ''}', + message: '${device.name}\n${device.spaceUuid ?? ''}', child: ChartInformativeCell(title: Text(device.name), color: deviceColor), ); } diff --git a/lib/pages/analytics/modules/energy_management/widgets/power_clamp_energy_data_widget.dart b/lib/pages/analytics/modules/energy_management/widgets/power_clamp_energy_data_widget.dart index 4d04a36b..f95ff7d1 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/power_clamp_energy_data_widget.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/power_clamp_energy_data_widget.dart @@ -41,7 +41,7 @@ class PowerClampEnergyDataWidget extends StatelessWidget { AnalyticsErrorWidget(state.errorMessage), AnalyticsSidebarHeader( title: 'Smart Power Clamp', - showSpaceUuid: true, + showSpaceUuidInDevicesDropdown: true, onChanged: (device) { FetchEnergyManagementDataHelper.loadEnergyConsumptionByPhases( context, diff --git a/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart_box.dart b/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart_box.dart index 9e70e45e..e197c297 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart_box.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart_box.dart @@ -19,7 +19,6 @@ class TotalEnergyConsumptionChartBox extends StatelessWidget { ), padding: const EdgeInsets.all(30), child: Column( - spacing: 20, crossAxisAlignment: CrossAxisAlignment.start, children: [ AnalyticsErrorWidget(state.errorMessage), @@ -39,7 +38,9 @@ class TotalEnergyConsumptionChartBox extends StatelessWidget { const Spacer(flex: 4), ], ), + const SizedBox(height: 20), const Divider(), + const SizedBox(height: 20), TotalEnergyConsumptionChart(chartData: state.chartData), ], ), diff --git a/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart b/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart index 4ff85841..70087c46 100644 --- a/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart +++ b/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart @@ -16,7 +16,7 @@ class OccupancyChart extends StatelessWidget { Widget build(BuildContext context) { return BarChart( BarChartData( - maxY: 100.0, + maxY: 100.001, gridData: EnergyManagementChartsHelper.gridData().copyWith( checkToShowHorizontalLine: (value) => true, horizontalInterval: 20, @@ -134,7 +134,7 @@ class OccupancyChart extends StatelessWidget { alignment: AlignmentDirectional.bottomCenter, fit: BoxFit.scaleDown, child: Text( - (value + 1).toString(), + chartData[value.toInt()].date.day.toString(), style: context.textTheme.bodySmall?.copyWith( color: ColorsManager.greyColor, fontSize: 8, diff --git a/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart_box.dart b/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart_box.dart index ab1d1699..08f7223f 100644 --- a/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart_box.dart +++ b/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart_box.dart @@ -22,7 +22,6 @@ class OccupancyChartBox extends StatelessWidget { padding: const EdgeInsets.all(30), decoration: containerWhiteDecoration, child: Column( - spacing: 20, mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -65,7 +64,9 @@ class OccupancyChartBox extends StatelessWidget { ), ], ), - const Divider(height: 0), + const SizedBox(height: 20), + const Divider(), + const SizedBox(height: 20), Expanded(child: OccupancyChart(chartData: state.chartData)), ], ), diff --git a/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_box.dart b/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_box.dart index cab9eab4..c3b537e0 100644 --- a/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_box.dart +++ b/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_box.dart @@ -22,7 +22,6 @@ class OccupancyHeatMapBox extends StatelessWidget { padding: const EdgeInsets.all(30), decoration: containerWhiteDecoration, child: Column( - spacing: 20, mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -66,7 +65,9 @@ class OccupancyHeatMapBox extends StatelessWidget { ), ], ), - const Divider(height: 0), + const SizedBox(height: 20), + const Divider(), + const SizedBox(height: 20), Expanded( child: OccupancyHeatMap( heatMapData: state.heatMapData.asMap().map( diff --git a/lib/pages/analytics/params/get_air_quality_distribution_param.dart b/lib/pages/analytics/params/get_air_quality_distribution_param.dart new file mode 100644 index 00000000..f1d3fe9f --- /dev/null +++ b/lib/pages/analytics/params/get_air_quality_distribution_param.dart @@ -0,0 +1,9 @@ +class GetAirQualityDistributionParam { + final DateTime date; + final String spaceUuid; + + const GetAirQualityDistributionParam({ + required this.date, + required this.spaceUuid, + }); +} diff --git a/lib/pages/analytics/params/get_energy_consumption_per_device_param.dart b/lib/pages/analytics/params/get_energy_consumption_per_device_param.dart index ba659ae7..79d0f2f4 100644 --- a/lib/pages/analytics/params/get_energy_consumption_per_device_param.dart +++ b/lib/pages/analytics/params/get_energy_consumption_per_device_param.dart @@ -2,18 +2,15 @@ class GetEnergyConsumptionPerDeviceParam { const GetEnergyConsumptionPerDeviceParam({ this.monthDate, this.spaceId, - this.communityId, }); final DateTime? monthDate; final String? spaceId; - final String? communityId; Map toJson() => { 'monthDate': '${monthDate?.year}-${monthDate?.month.toString().padLeft(2, '0')}', if (spaceId == null || spaceId == null) 'spaceUuid': spaceId, - 'communityUuid': communityId, 'groupByDevice': true, }; } diff --git a/lib/pages/analytics/params/get_range_of_aqi_param.dart b/lib/pages/analytics/params/get_range_of_aqi_param.dart index bbf24658..ef53fe76 100644 --- a/lib/pages/analytics/params/get_range_of_aqi_param.dart +++ b/lib/pages/analytics/params/get_range_of_aqi_param.dart @@ -1,16 +1,12 @@ import 'package:equatable/equatable.dart'; -import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart'; class GetRangeOfAqiParam extends Equatable { final DateTime date; final String spaceUuid; - final AqiType aqiType; - const GetRangeOfAqiParam( - { + const GetRangeOfAqiParam({ required this.date, required this.spaceUuid, - required this.aqiType, }); @override diff --git a/lib/pages/analytics/params/get_total_energy_consumption_param.dart b/lib/pages/analytics/params/get_total_energy_consumption_param.dart index c47e5bfe..6428fd30 100644 --- a/lib/pages/analytics/params/get_total_energy_consumption_param.dart +++ b/lib/pages/analytics/params/get_total_energy_consumption_param.dart @@ -1,12 +1,10 @@ class GetTotalEnergyConsumptionParam { final DateTime? monthDate; final String? spaceId; - final String? communityId; const GetTotalEnergyConsumptionParam({ this.monthDate, this.spaceId, - this.communityId, }); Map toJson() { @@ -14,7 +12,6 @@ class GetTotalEnergyConsumptionParam { 'monthDate': '${monthDate?.year}-${monthDate?.month.toString().padLeft(2, '0')}', if (spaceId == null || spaceId == null) 'spaceUuid': spaceId, - 'communityUuid': communityId, 'groupByDevice': false, }; } diff --git a/lib/pages/analytics/services/air_quality_distribution/air_quality_distribution_service.dart b/lib/pages/analytics/services/air_quality_distribution/air_quality_distribution_service.dart new file mode 100644 index 00000000..ef63856a --- /dev/null +++ b/lib/pages/analytics/services/air_quality_distribution/air_quality_distribution_service.dart @@ -0,0 +1,8 @@ +import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart'; +import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart'; + +abstract interface class AirQualityDistributionService { + Future> getAirQualityDistribution( + GetAirQualityDistributionParam param, + ); +} diff --git a/lib/pages/analytics/services/air_quality_distribution/fake_air_quality_distribution_service.dart b/lib/pages/analytics/services/air_quality_distribution/fake_air_quality_distribution_service.dart new file mode 100644 index 00000000..e0023f53 --- /dev/null +++ b/lib/pages/analytics/services/air_quality_distribution/fake_air_quality_distribution_service.dart @@ -0,0 +1,95 @@ +import 'dart:math'; + +import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart'; +import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart'; +import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/air_quality_distribution_service.dart'; + +class FakeAirQualityDistributionService implements AirQualityDistributionService { + final _random = Random(); + + @override + Future> getAirQualityDistribution( + GetAirQualityDistributionParam param, + ) async { + return Future.delayed( + const Duration(milliseconds: 400), + () => List.generate(30, (index) { + final date = DateTime(2025, 5, 1).add(Duration(days: index)); + + final values = _generateRandomPercentages(); + final nullMask = List.generate(6, (_) => _shouldBeNull()); + + if (nullMask.every((isNull) => isNull)) { + nullMask[_random.nextInt(6)] = false; + } + + final nonNullValues = _redistributePercentages(values, nullMask); + + return AirQualityDataModel( + date: date, + data: [ + AirQualityPercentageData( + type: AqiType.aqi.code, + percentage: nonNullValues[0], + name: 'good', + ), + AirQualityPercentageData( + name: 'moderate', + type: AqiType.co2.code, + percentage: nonNullValues[1], + ), + AirQualityPercentageData( + name: 'poor', + percentage: nonNullValues[2], + type: AqiType.hcho.code, + + ), + AirQualityPercentageData( + name: 'unhealthy', + percentage: nonNullValues[3], + type: AqiType.pm10.code, + ), + AirQualityPercentageData( + name: 'severe', + type: AqiType.pm25.code, + percentage: nonNullValues[4], + ), + AirQualityPercentageData( + name: 'hazardous', + percentage: nonNullValues[5], + type: AqiType.co2.code, + ), + ], + ); + }), + ); + } + + List _redistributePercentages( + List originalValues, + List nullMask, + ) { + double nonNullSum = 0; + for (int i = 0; i < originalValues.length; i++) { + if (!nullMask[i]) { + nonNullSum += originalValues[i]; + } + } + + return List.generate(originalValues.length, (i) { + if (nullMask[i]) return 0; + return (originalValues[i] / nonNullSum * 100).roundToDouble(); + }); + } + + bool _shouldBeNull() => _random.nextDouble() < 0.6; + + List _generateRandomPercentages() { + final values = List.generate(6, (_) => _random.nextDouble()); + + final sum = values.reduce((a, b) => a + b); + + return values.map((value) => (value / sum * 100).roundToDouble()).toList(); + } +} diff --git a/lib/pages/analytics/services/air_quality_distribution/remote_air_quality_distribution_service.dart b/lib/pages/analytics/services/air_quality_distribution/remote_air_quality_distribution_service.dart new file mode 100644 index 00000000..dcf00600 --- /dev/null +++ b/lib/pages/analytics/services/air_quality_distribution/remote_air_quality_distribution_service.dart @@ -0,0 +1,36 @@ +import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart'; +import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart'; +import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/air_quality_distribution_service.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; + +class RemoteAirQualityDistributionService implements AirQualityDistributionService { + RemoteAirQualityDistributionService(this._httpService); + + final HTTPService _httpService; + + @override + Future> getAirQualityDistribution( + GetAirQualityDistributionParam param, + ) async { + try { + final response = await _httpService.get( + path: 'endpoint', + queryParameters: { + 'spaceUuid': param.spaceUuid, + 'date': param.date.toIso8601String(), + }, + expectedResponseModel: (data) { + final json = data as Map? ?? {}; + final mappedData = json['data'] as List? ?? []; + return mappedData.map((e) { + final jsonData = e as Map; + return AirQualityDataModel.fromJson(jsonData); + }).toList(); + }, + ); + return response; + } catch (e) { + throw Exception('Failed to load energy consumption per phase: $e'); + } + } +} diff --git a/lib/pages/analytics/services/range_of_aqi/fake_range_of_aqi_service.dart b/lib/pages/analytics/services/range_of_aqi/fake_range_of_aqi_service.dart index 13173c94..01ad6fa1 100644 --- a/lib/pages/analytics/services/range_of_aqi/fake_range_of_aqi_service.dart +++ b/lib/pages/analytics/services/range_of_aqi/fake_range_of_aqi_service.dart @@ -1,4 +1,5 @@ import 'package:syncrow_web/pages/analytics/models/range_of_aqi.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart'; import 'package:syncrow_web/pages/analytics/params/get_range_of_aqi_param.dart'; import 'package:syncrow_web/pages/analytics/services/range_of_aqi/range_of_aqi_service.dart'; @@ -18,10 +19,15 @@ class FakeRangeOfAqiService implements RangeOfAqiService { final avg = (min + avgDelta).clamp(0.0, 301.0); final max = (avg + maxDelta).clamp(0.0, 301.0); - return RangeOfAqi( - min: min, - avg: avg, - max: max, + return RangeOfAqi( + data: [ + RangeOfAqiValue(type: AqiType.aqi.code, min: min, average: avg, max: max), + RangeOfAqiValue(type: AqiType.pm25.code, min: min, average: avg, max: max), + RangeOfAqiValue(type: AqiType.pm10.code, min: min, average: avg, max: max), + RangeOfAqiValue(type: AqiType.hcho.code, min: min, average: avg, max: max), + RangeOfAqiValue(type: AqiType.tvoc.code, min: min, average: avg, max: max), + RangeOfAqiValue(type: AqiType.co2.code, min: min, average: avg, max: max), + ], date: date, ); }); diff --git a/lib/pages/analytics/services/range_of_aqi/remote_range_of_aqi_service.dart b/lib/pages/analytics/services/range_of_aqi/remote_range_of_aqi_service.dart new file mode 100644 index 00000000..1a80ef33 --- /dev/null +++ b/lib/pages/analytics/services/range_of_aqi/remote_range_of_aqi_service.dart @@ -0,0 +1,34 @@ +import 'package:syncrow_web/pages/analytics/models/range_of_aqi.dart'; +import 'package:syncrow_web/pages/analytics/params/get_range_of_aqi_param.dart'; +import 'package:syncrow_web/pages/analytics/services/range_of_aqi/range_of_aqi_service.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; + +final class RemoteRangeOfAqiService implements RangeOfAqiService { + const RemoteRangeOfAqiService(this._httpService); + + final HTTPService _httpService; + + @override + Future> load(GetRangeOfAqiParam param) async { + try { + final response = await _httpService.get( + path: 'endpoint', + queryParameters: { + 'spaceUuid': param.spaceUuid, + 'date': param.date.toIso8601String(), + }, + expectedResponseModel: (data) { + final json = data as Map? ?? {}; + final mappedData = json['data'] as List? ?? []; + return mappedData.map((e) { + final jsonData = e as Map; + return RangeOfAqi.fromJson(jsonData); + }).toList(); + }, + ); + return response; + } catch (e) { + throw Exception('Failed to load energy consumption per phase: $e'); + } + } +} diff --git a/lib/pages/analytics/widgets/analytics_error_widget.dart b/lib/pages/analytics/widgets/analytics_error_widget.dart index 60167992..7c560da4 100644 --- a/lib/pages/analytics/widgets/analytics_error_widget.dart +++ b/lib/pages/analytics/widgets/analytics_error_widget.dart @@ -11,14 +11,17 @@ class AnalyticsErrorWidget extends StatelessWidget { Widget build(BuildContext context) { return Visibility( visible: errorMessage != null || (errorMessage?.isNotEmpty ?? false), - child: Text( - errorMessage ?? 'Something went wrong', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: context.textTheme.bodySmall?.copyWith( - color: ColorsManager.red, - fontWeight: FontWeight.w400, - fontSize: 8, + child: Padding( + padding: const EdgeInsetsDirectional.only(bottom: 10), + child: Text( + errorMessage ?? 'Something went wrong', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.red, + fontWeight: FontWeight.w400, + fontSize: 8, + ), ), ), ); diff --git a/lib/pages/analytics/widgets/analytics_sidebar_header.dart b/lib/pages/analytics/widgets/analytics_sidebar_header.dart index 5e454ea4..5ff1d042 100644 --- a/lib/pages/analytics/widgets/analytics_sidebar_header.dart +++ b/lib/pages/analytics/widgets/analytics_sidebar_header.dart @@ -10,13 +10,13 @@ import 'package:syncrow_web/utils/extension/build_context_x.dart'; class AnalyticsSidebarHeader extends StatelessWidget { const AnalyticsSidebarHeader({ required this.title, - this.showSpaceUuid = false, + this.showSpaceUuidInDevicesDropdown = false, this.onChanged, super.key, }); final String title; - final bool showSpaceUuid; + final bool showSpaceUuidInDevicesDropdown; final void Function(AnalyticsDevice device)? onChanged; @override @@ -49,6 +49,7 @@ class AnalyticsSidebarHeader extends StatelessWidget { alignment: AlignmentDirectional.centerEnd, fit: BoxFit.scaleDown, child: AnalyticsDeviceDropdown( + showSpaceUuid: showSpaceUuidInDevicesDropdown, onChanged: (value) { context.read().add( SelectAnalyticsDeviceEvent(value), diff --git a/lib/pages/auth/bloc/auth_bloc.dart b/lib/pages/auth/bloc/auth_bloc.dart index 35663557..58950089 100644 --- a/lib/pages/auth/bloc/auth_bloc.dart +++ b/lib/pages/auth/bloc/auth_bloc.dart @@ -13,6 +13,7 @@ import 'package:syncrow_web/pages/common/bloc/project_manager.dart'; import 'package:syncrow_web/pages/home/bloc/home_bloc.dart'; import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart'; import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; import 'package:syncrow_web/services/auth_api.dart'; import 'package:syncrow_web/utils/constants/strings_manager.dart'; import 'package:syncrow_web/utils/helpers/shared_preferences_helper.dart'; @@ -99,7 +100,8 @@ class AuthBloc extends Bloc { emit(const TimerState(isButtonEnabled: true, remainingTime: 0)); } - Future changePassword(ChangePasswordEvent event, Emitter emit) async { +Future changePassword( + ChangePasswordEvent event, Emitter emit) async { emit(LoadingForgetState()); try { var response = await AuthenticationAPI.verifyOtp( @@ -113,14 +115,14 @@ class AuthBloc extends Bloc { emit(const TimerState(isButtonEnabled: true, remainingTime: 0)); emit(SuccessForgetState()); } - } on DioException catch (e) { - final errorData = e.response!.data; - String errorMessage = errorData['error']['message'] ?? 'something went wrong'; + } on APIException catch (e) { + final errorMessage = e.message; validate = errorMessage; emit(AuthInitialState()); } } + String? validateCode(String? value) { if (value == null || value.isEmpty) { return 'Code is required'; @@ -149,6 +151,7 @@ class AuthBloc extends Bloc { static UserModel? user; bool showValidationMessage = false; + void _login(LoginButtonPressed event, Emitter emit) async { emit(AuthLoading()); if (isChecked) { @@ -161,25 +164,24 @@ class AuthBloc extends Bloc { token = await AuthenticationAPI.loginWithEmail( model: LoginWithEmailModel( - email: event.username, + email: event.username.toLowerCase(), password: event.password, ), ); - } on DioException catch (e) { - final errorData = e.response!.data; - String errorMessage = errorData['error']['message']; - if (errorMessage == "Access denied for web platform") { - validate = errorMessage; - } else { - validate = 'Invalid Credentials!'; - } + } on APIException catch (e) { + validate = e.message; + emit(LoginInitial()); + return; + } catch (e) { + validate = 'Something went wrong'; emit(LoginInitial()); return; } if (token.accessTokenIsNotEmpty) { FlutterSecureStorage storage = const FlutterSecureStorage(); - await storage.write(key: Token.loginAccessTokenKey, value: token.accessToken); + await storage.write( + key: Token.loginAccessTokenKey, value: token.accessToken); const FlutterSecureStorage().write( key: UserModel.userUuidKey, value: Token.decodeToken(token.accessToken)['uuid'].toString()); @@ -195,6 +197,7 @@ class AuthBloc extends Bloc { } } + checkBoxToggle( CheckBoxEvent event, Emitter emit, diff --git a/lib/pages/common/custom_table.dart b/lib/pages/common/custom_table.dart index 62760a16..0abe075b 100644 --- a/lib/pages/common/custom_table.dart +++ b/lib/pages/common/custom_table.dart @@ -211,6 +211,7 @@ class _DynamicTableState extends State { onChanged: widget.withSelectAll && widget.data.isNotEmpty ? _toggleSelectAll : null, + ), ); } @@ -281,6 +282,7 @@ class _DynamicTableState extends State { padding: EdgeInsets.symmetric( horizontal: index == widget.headers.length - 1 ? 12 : 8.0, vertical: 4), + child: Text( title, style: context.textTheme.titleSmall!.copyWith( @@ -301,6 +303,7 @@ class _DynamicTableState extends State { required int rowIndex, required int columnIndex, }) { + bool isBatteryLevel = content.endsWith('%'); double? batteryLevel; @@ -312,6 +315,7 @@ class _DynamicTableState extends State { if (isSettingsColumn) { return _buildSettingsIcon(rowIndex, size); } + Color? statusColor; switch (content) { diff --git a/lib/pages/device_managment/ac/bloc/ac_bloc.dart b/lib/pages/device_managment/ac/bloc/ac_bloc.dart index 501d29d8..af5a7b0a 100644 --- a/lib/pages/device_managment/ac/bloc/ac_bloc.dart +++ b/lib/pages/device_managment/ac/bloc/ac_bloc.dart @@ -1,21 +1,27 @@ import 'dart:async'; -import 'package:dio/dio.dart'; + import 'package:firebase_database/firebase_database.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_event.dart'; import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_state.dart'; import 'package:syncrow_web/pages/device_managment/ac/model/ac_model.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; +import 'package:syncrow_web/services/batch_control_devices_service.dart'; +import 'package:syncrow_web/services/control_device_service.dart'; import 'package:syncrow_web/services/devices_mang_api.dart'; class AcBloc extends Bloc { late AcStatusModel deviceStatus; final String deviceId; - Timer? _timer; + final ControlDeviceService controlDeviceService; + final BatchControlDevicesService batchControlDevicesService; Timer? _countdownTimer; - AcBloc({required this.deviceId}) : super(AcsInitialState()) { + AcBloc({ + required this.deviceId, + required this.controlDeviceService, + required this.batchControlDevicesService, + }) : super(AcsInitialState()) { on(_onFetchAcStatus); on(_onFetchAcBatchStatus); on(_onAcControl); @@ -34,14 +40,14 @@ class AcBloc extends Bloc { int scheduledMinutes = 0; FutureOr _onFetchAcStatus( - AcFetchDeviceStatusEvent event, Emitter emit) async { + AcFetchDeviceStatusEvent event, + Emitter emit, + ) async { emit(AcsLoadingState()); try { - final status = - await DevicesManagementApi().getDeviceStatus(event.deviceId); + final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); deviceStatus = AcStatusModel.fromJson(event.deviceId, status.status); if (deviceStatus.countdown1 != 0) { - // Convert API value to minutes final totalMinutes = deviceStatus.countdown1 * 6; scheduledHours = totalMinutes ~/ 60; scheduledMinutes = totalMinutes % 60; @@ -62,30 +68,24 @@ class AcBloc extends Bloc { } } - _listenToChanges(deviceId) { + void _listenToChanges(deviceId) { try { - DatabaseReference ref = - FirebaseDatabase.instance.ref('device-status/$deviceId'); - Stream stream = ref.onValue; + final ref = FirebaseDatabase.instance.ref('device-status/$deviceId'); + final stream = ref.onValue; stream.listen((DatabaseEvent event) async { if (event.snapshot.value == null) return; - if (_timer != null) { - await Future.delayed(const Duration(seconds: 1)); - } Map usersMap = event.snapshot.value as Map; List statusList = []; usersMap['status'].forEach((element) { - statusList - .add(Status(code: element['code'], value: element['value'])); + statusList.add(Status(code: element['code'], value: element['value'])); }); - deviceStatus = - AcStatusModel.fromJson(usersMap['productUuid'], statusList); + deviceStatus = AcStatusModel.fromJson(usersMap['productUuid'], statusList); if (!isClosed) { add(AcStatusUpdated(deviceStatus)); } @@ -93,146 +93,44 @@ class AcBloc extends Bloc { } catch (_) {} } - void _onAcStatusUpdated(AcStatusUpdated event, Emitter emit) { + void _onAcStatusUpdated( + AcStatusUpdated event, + Emitter emit, + ) { deviceStatus = event.deviceStatus; emit(ACStatusLoaded(status: deviceStatus)); } FutureOr _onAcControl( - AcControlEvent event, Emitter emit) async { - final oldValue = _getValueByCode(event.code); - - _updateLocalValue(event.code, event.value, emit); - + AcControlEvent event, + Emitter emit, + ) async { + emit(AcsLoadingState()); + _updateDeviceFunctionFromCode(event.code, event.value); emit(ACStatusLoaded(status: deviceStatus)); - await _runDebounce( - isBatch: false, - deviceId: event.deviceId, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - ); - } + try { + final success = await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status(code: event.code, value: event.value), + ); - Future _runDebounce({ - required dynamic deviceId, - required String code, - required dynamic value, - required dynamic oldValue, - required Emitter emit, - required bool isBatch, - }) async { - late String id; - - if (deviceId is List) { - id = deviceId.first; - } else { - id = deviceId; - } - - if (_timer != null) { - _timer!.cancel(); - } - _timer = Timer(const Duration(seconds: 1), () async { - try { - late bool response; - if (isBatch) { - response = await DevicesManagementApi() - .deviceBatchControl(deviceId, code, value); - } else { - response = await DevicesManagementApi() - .deviceControl(deviceId, Status(code: code, value: value)); - } - - if (!response) { - _revertValueAndEmit(id, code, oldValue, emit); - } - } catch (e) { - if (e is DioException && e.response != null) { - debugPrint('Error response: ${e.response?.data}'); - } - _revertValueAndEmit(id, code, oldValue, emit); + if (!success) { + emit(const AcsFailedState(error: 'Failed to control device')); } - }); - } - - void _revertValueAndEmit( - String deviceId, String code, dynamic oldValue, Emitter emit) { - _updateLocalValue(code, oldValue, emit); - emit(ACStatusLoaded(status: deviceStatus)); - } - - void _updateLocalValue(String code, dynamic value, Emitter emit) { - switch (code) { - case 'switch': - if (value is bool) { - deviceStatus = deviceStatus.copyWith(acSwitch: value); - } - break; - case 'temp_set': - if (value is int) { - deviceStatus = deviceStatus.copyWith(tempSet: value); - } - break; - case 'mode': - if (value is String) { - deviceStatus = deviceStatus.copyWith( - modeString: value, - ); - } - break; - case 'level': - if (value is String) { - deviceStatus = deviceStatus.copyWith( - fanSpeedsString: value, - ); - } - break; - case 'child_lock': - if (value is bool) { - deviceStatus = deviceStatus.copyWith(childLock: value); - } - - case 'countdown_time': - if (value is int) { - deviceStatus = deviceStatus.copyWith(countdown1: value); - } - break; - default: - break; - } - emit(ACStatusLoaded(status: deviceStatus)); - } - - dynamic _getValueByCode(String code) { - switch (code) { - case 'switch': - return deviceStatus.acSwitch; - case 'temp_set': - return deviceStatus.tempSet; - case 'mode': - return deviceStatus.modeString; - case 'level': - return deviceStatus.fanSpeedsString; - case 'child_lock': - return deviceStatus.childLock; - case 'countdown_time': - return deviceStatus.countdown1; - default: - return null; + } catch (e) { + emit(AcsFailedState(error: e.toString())); } } FutureOr _onFetchAcBatchStatus( - AcFetchBatchStatusEvent event, Emitter emit) async { + AcFetchBatchStatusEvent event, + Emitter emit, + ) async { emit(AcsLoadingState()); try { - final status = - await DevicesManagementApi().getBatchStatus(event.devicesIds); - deviceStatus = - AcStatusModel.fromJson(event.devicesIds.first, status.status); + final status = await DevicesManagementApi().getBatchStatus(event.devicesIds); + deviceStatus = AcStatusModel.fromJson(event.devicesIds.first, status.status); emit(ACStatusLoaded(status: deviceStatus)); } catch (e) { emit(AcsFailedState(error: e.toString())); @@ -240,25 +138,32 @@ class AcBloc extends Bloc { } FutureOr _onAcBatchControl( - AcBatchControlEvent event, Emitter emit) async { - final oldValue = _getValueByCode(event.code); - - _updateLocalValue(event.code, event.value, emit); - + AcBatchControlEvent event, + Emitter emit, + ) async { + emit(AcsLoadingState()); + _updateDeviceFunctionFromCode(event.code, event.value); emit(ACStatusLoaded(status: deviceStatus)); - await _runDebounce( - isBatch: true, - deviceId: event.devicesIds, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - ); + try { + final success = await batchControlDevicesService.batchControlDevices( + uuids: event.devicesIds, + code: event.code, + value: event.value, + ); + + if (!success) { + emit(const AcsFailedState(error: 'Failed to control devices')); + } + } catch (e) { + emit(AcsFailedState(error: e.toString())); + } } - FutureOr _onFactoryReset( - AcFactoryResetEvent event, Emitter emit) async { + Future _onFactoryReset( + AcFactoryResetEvent event, + Emitter emit, + ) async { emit(AcsLoadingState()); try { final response = await DevicesManagementApi().factoryReset( @@ -275,9 +180,11 @@ class AcBloc extends Bloc { } } - void _onClose(OnClose event, Emitter emit) { + void _onClose( + OnClose event, + Emitter emit, + ) { _countdownTimer?.cancel(); - _timer?.cancel(); } void _handleIncreaseTime(IncreaseTimeEvent event, Emitter emit) { @@ -300,7 +207,10 @@ class AcBloc extends Bloc { )); } - void _handleDecreaseTime(DecreaseTimeEvent event, Emitter emit) { + void _handleDecreaseTime( + DecreaseTimeEvent event, + Emitter emit, + ) { if (state is! ACStatusLoaded) return; final currentState = state as ACStatusLoaded; int totalMinutes = (scheduledHours * 60) + scheduledMinutes; @@ -315,7 +225,9 @@ class AcBloc extends Bloc { } Future _handleToggleTimer( - ToggleScheduleEvent event, Emitter emit) async { + ToggleScheduleEvent event, + Emitter emit, + ) async { if (state is! ACStatusLoaded) return; final currentState = state as ACStatusLoaded; @@ -331,37 +243,44 @@ class AcBloc extends Bloc { try { final scaledValue = totalMinutes ~/ 6; - await _runDebounce( - isBatch: false, - deviceId: deviceId, - code: 'countdown_time', - value: scaledValue, - oldValue: scaledValue, - emit: emit, + final success = await controlDeviceService.controlDevice( + deviceUuid: deviceId, + status: Status(code: 'countdown_time', value: scaledValue), ); - _startCountdownTimer(emit); - emit(currentState.copyWith(isTimerActive: timerActive)); + + if (success) { + _startCountdownTimer(emit); + emit(currentState.copyWith(isTimerActive: timerActive)); + } else { + timerActive = false; + emit(const AcsFailedState(error: 'Failed to set timer')); + } } catch (e) { timerActive = false; emit(AcsFailedState(error: e.toString())); } } else { - await _runDebounce( - isBatch: false, - deviceId: deviceId, - code: 'countdown_time', - value: 0, - oldValue: 0, - emit: emit, - ); - _countdownTimer?.cancel(); - scheduledHours = 0; - scheduledMinutes = 0; - emit(currentState.copyWith( - isTimerActive: timerActive, - scheduledHours: 0, - scheduledMinutes: 0, - )); + try { + final success = await controlDeviceService.controlDevice( + deviceUuid: deviceId, + status: Status(code: 'countdown_time', value: 0), + ); + + if (success) { + _countdownTimer?.cancel(); + scheduledHours = 0; + scheduledMinutes = 0; + emit(currentState.copyWith( + isTimerActive: timerActive, + scheduledHours: 0, + scheduledMinutes: 0, + )); + } else { + emit(const AcsFailedState(error: 'Failed to stop timer')); + } + } catch (e) { + emit(AcsFailedState(error: e.toString())); + } } } @@ -385,7 +304,10 @@ class AcBloc extends Bloc { }); } - void _handleUpdateTimer(UpdateTimerEvent event, Emitter emit) { + void _handleUpdateTimer( + UpdateTimerEvent event, + Emitter emit, + ) { if (state is ACStatusLoaded) { final currentState = state as ACStatusLoaded; emit(currentState.copyWith( @@ -400,7 +322,6 @@ class AcBloc extends Bloc { ApiCountdownValueEvent event, Emitter emit) { if (state is ACStatusLoaded) { final totalMinutes = event.apiValue * 6; - final scheduledHours = totalMinutes ~/ 60; scheduledMinutes = totalMinutes % 60; _startCountdownTimer( emit, @@ -409,6 +330,43 @@ class AcBloc extends Bloc { } } + void _updateDeviceFunctionFromCode(String code, dynamic value) { + switch (code) { + case 'switch': + if (value is bool) { + deviceStatus = deviceStatus.copyWith(acSwitch: value); + } + break; + case 'temp_set': + if (value is int) { + deviceStatus = deviceStatus.copyWith(tempSet: value); + } + break; + case 'mode': + if (value is String) { + deviceStatus = deviceStatus.copyWith(modeString: value); + } + break; + case 'level': + if (value is String) { + deviceStatus = deviceStatus.copyWith(fanSpeedsString: value); + } + break; + case 'child_lock': + if (value is bool) { + deviceStatus = deviceStatus.copyWith(childLock: value); + } + break; + case 'countdown_time': + if (value is int) { + deviceStatus = deviceStatus.copyWith(countdown1: value); + } + break; + default: + break; + } + } + @override Future close() { add(OnClose()); diff --git a/lib/pages/device_managment/ac/factories/ac_bloc_factory.dart b/lib/pages/device_managment/ac/factories/ac_bloc_factory.dart new file mode 100644 index 00000000..9e5f4c1c --- /dev/null +++ b/lib/pages/device_managment/ac/factories/ac_bloc_factory.dart @@ -0,0 +1,18 @@ +import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/factories/device_bloc_dependencies_factory.dart'; + +abstract final class AcBlocFactory { + const AcBlocFactory._(); + + static AcBloc create({ + required String deviceId, + }) { + return AcBloc( + deviceId: deviceId, + controlDeviceService: + DeviceBlocDependenciesFactory.createControlDeviceService(), + batchControlDevicesService: + DeviceBlocDependenciesFactory.createBatchControlDevicesService(), + ); + } +} diff --git a/lib/pages/device_managment/ac/view/ac_device_batch_control.dart b/lib/pages/device_managment/ac/view/ac_device_batch_control.dart index 3005c1c5..aad0669b 100644 --- a/lib/pages/device_managment/ac/view/ac_device_batch_control.dart +++ b/lib/pages/device_managment/ac/view/ac_device_batch_control.dart @@ -3,12 +3,12 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_bloc.dart'; import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_event.dart'; import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_state.dart'; +import 'package:syncrow_web/pages/device_managment/ac/factories/ac_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/ac/view/batch_control_list/batch_ac_mode.dart'; import 'package:syncrow_web/pages/device_managment/ac/view/batch_control_list/batch_current_temp.dart'; import 'package:syncrow_web/pages/device_managment/ac/view/batch_control_list/batch_fan_speed.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; -// import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; @@ -26,8 +26,9 @@ class AcDeviceBatchControlView extends StatelessWidget with HelperResponsiveLayo final isLarge = isLargeScreenSize(context); final isMedium = isMediumScreenSize(context); return BlocProvider( - create: (context) => - AcBloc(deviceId: devicesIds.first)..add(AcFetchBatchStatusEvent(devicesIds)), + create: (context) => AcBlocFactory.create( + deviceId: devicesIds.first, + )..add(AcFetchBatchStatusEvent(devicesIds)), child: BlocBuilder( builder: (context, state) { if (state is ACStatusLoaded) { diff --git a/lib/pages/device_managment/ac/view/ac_device_control.dart b/lib/pages/device_managment/ac/view/ac_device_control.dart index 8c33c853..a882e6d5 100644 --- a/lib/pages/device_managment/ac/view/ac_device_control.dart +++ b/lib/pages/device_managment/ac/view/ac_device_control.dart @@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_bloc.dart'; import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_event.dart'; import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_state.dart'; +import 'package:syncrow_web/pages/device_managment/ac/factories/ac_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/ac/view/control_list/ac_mode.dart'; import 'package:syncrow_web/pages/device_managment/ac/view/control_list/current_temp.dart'; import 'package:syncrow_web/pages/device_managment/ac/view/control_list/fan_speed.dart'; @@ -24,8 +25,9 @@ class AcDeviceControlsView extends StatelessWidget with HelperResponsiveLayout { final isMedium = isMediumScreenSize(context); return BlocProvider( - create: (context) => AcBloc(deviceId: device.uuid!) - ..add(AcFetchDeviceStatusEvent(device.uuid!)), + create: (context) => AcBlocFactory.create( + deviceId: device.uuid!, + )..add(AcFetchDeviceStatusEvent(device.uuid!)), child: BlocBuilder( builder: (context, state) { final acBloc = BlocProvider.of(context); diff --git a/lib/pages/device_managment/all_devices/view/device_managment_page.dart b/lib/pages/device_managment/all_devices/view/device_managment_page.dart index fd3a2574..755bc8b7 100644 --- a/lib/pages/device_managment/all_devices/view/device_managment_page.dart +++ b/lib/pages/device_managment/all_devices/view/device_managment_page.dart @@ -95,7 +95,7 @@ class DeviceManagementPage extends StatelessWidget with HelperResponsiveLayout { return const RoutinesView(); } if (state.createRoutineView) { - return CreateNewRoutineView(); + return const CreateNewRoutineView(); } return BlocBuilder( diff --git a/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart b/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart index a3c975c1..f4baad0c 100644 --- a/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart +++ b/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart @@ -6,9 +6,11 @@ import 'package:syncrow_web/pages/common/filter/filter_widget.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/widgets/device_search_filters.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/device_settings_panel.dart'; import 'package:syncrow_web/pages/device_managment/shared/device_batch_control_dialog.dart'; import 'package:syncrow_web/pages/device_managment/shared/device_control_dialog.dart'; import 'package:syncrow_web/pages/space_tree/view/space_tree_view.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/format_date_time.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; import 'package:syncrow_web/utils/style.dart'; @@ -58,7 +60,8 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { 'Low Battery ($lowBatteryCount)', ]; - final buttonLabel = (selectedDevices.length > 1) ? 'Batch Control' : 'Control'; + final buttonLabel = + (selectedDevices.length > 1) ? 'Batch Control' : 'Control'; return Row( children: [ @@ -105,18 +108,23 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { if (selectedDevices.length == 1) { showDialog( context: context, - builder: (context) => DeviceControlDialog( + builder: (context) => + DeviceControlDialog( device: selectedDevices.first, ), ); - } else if (selectedDevices.length > 1) { - final productTypes = selectedDevices - .map((device) => device.productType) - .toSet(); + } else if (selectedDevices.length > + 1) { + final productTypes = + selectedDevices + .map((device) => + device.productType) + .toSet(); if (productTypes.length == 1) { showDialog( context: context, - builder: (context) => DeviceBatchControlDialog( + builder: (context) => + DeviceBatchControlDialog( devices: selectedDevices, ), ); @@ -130,7 +138,9 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { textAlign: TextAlign.center, style: TextStyle( fontSize: 12, - color: isControlButtonEnabled ? Colors.white : Colors.grey, + color: isControlButtonEnabled + ? Colors.white + : Colors.grey, ), ), ), @@ -166,29 +176,40 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { 'Installation Date and Time', 'Status', 'Last Offline Date and Time', + 'Settings' ], data: devicesToShow.map((device) { final combinedSpaceNames = device.spaces != null - ? device.spaces!.map((space) => space.spaceName).join(' > ') + + ? device.spaces! + .map((space) => space.spaceName) + .join(' > ') + (device.community != null ? ' > ${device.community!.name}' : '') - : (device.community != null ? device.community!.name : ''); + : (device.community != null + ? device.community!.name + : ''); return [ device.name ?? '', device.productName ?? '', device.uuid ?? '', - (device.spaces != null && device.spaces!.isNotEmpty) + (device.spaces != null && + device.spaces!.isNotEmpty) ? device.spaces![0].spaceName : '', combinedSpaceNames, - device.batteryLevel != null ? '${device.batteryLevel}%' : '-', - formatDateTime(DateTime.fromMillisecondsSinceEpoch( - (device.createTime ?? 0) * 1000)), + device.batteryLevel != null + ? '${device.batteryLevel}%' + : '-', + formatDateTime( + DateTime.fromMillisecondsSinceEpoch( + (device.createTime ?? 0) * 1000)), device.online == true ? 'Online' : 'Offline', - formatDateTime(DateTime.fromMillisecondsSinceEpoch( - (device.updateTime ?? 0) * 1000)), + formatDateTime( + DateTime.fromMillisecondsSinceEpoch( + (device.updateTime ?? 0) * 1000)), + 'Settings', ]; }).toList(), onSelectionChanged: (selectedRows) { @@ -202,6 +223,10 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { .map((device) => device.uuid!) .toList(), isEmpty: devicesToShow.isEmpty, + onSettingsPressed: (rowIndex) { + final device = devicesToShow[rowIndex]; + showDeviceSettingsSidebar(context, device); + }, ), ), ) @@ -213,4 +238,37 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { }, ); } + + void showDeviceSettingsSidebar(BuildContext context, AllDevicesModel device) { + showGeneralDialog( + context: context, + barrierDismissible: true, + barrierLabel: "Device Settings", + transitionDuration: const Duration(milliseconds: 300), + pageBuilder: (context, anim1, anim2) { + return Align( + alignment: Alignment.centerRight, + child: Material( + child: Container( + width: MediaQuery.of(context).size.width * 0.3, + color: ColorsManager.whiteColors, + child: DeviceSettingsPanel( + device: device, + onClose: () => Navigator.of(context).pop(), + ), + ), + ), + ); + }, + transitionBuilder: (context, anim1, anim2, child) { + return SlideTransition( + position: Tween( + begin: const Offset(1, 0), + end: Offset.zero, + ).animate(anim1), + child: child, + ); + }, + ); + } } diff --git a/lib/pages/device_managment/ceiling_sensor/bloc/ceiling_bloc.dart b/lib/pages/device_managment/ceiling_sensor/bloc/ceiling_bloc.dart index 4e8d5a8b..42387e57 100644 --- a/lib/pages/device_managment/ceiling_sensor/bloc/ceiling_bloc.dart +++ b/lib/pages/device_managment/ceiling_sensor/bloc/ceiling_bloc.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import 'package:firebase_database/firebase_database.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; @@ -7,14 +5,21 @@ import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/ceiling_e import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/ceiling_state.dart'; import 'package:syncrow_web/pages/device_managment/ceiling_sensor/model/ceiling_sensor_model.dart'; import 'package:syncrow_web/pages/device_managment/ceiling_sensor/model/help_description.dart'; +import 'package:syncrow_web/services/batch_control_devices_service.dart'; +import 'package:syncrow_web/services/control_device_service.dart'; import 'package:syncrow_web/services/devices_mang_api.dart'; class CeilingSensorBloc extends Bloc { final String deviceId; + final ControlDeviceService controlDeviceService; + final BatchControlDevicesService batchControlDevicesService; late CeilingSensorModel deviceStatus; - Timer? _timer; - CeilingSensorBloc({required this.deviceId}) : super(CeilingInitialState()) { + CeilingSensorBloc({ + required this.deviceId, + required this.controlDeviceService, + required this.batchControlDevicesService, + }) : super(CeilingInitialState()) { on(_fetchCeilingSensorStatus); on(_fetchCeilingSensorBatchControl); on(_changeValue); @@ -26,35 +31,34 @@ class CeilingSensorBloc extends Bloc { on(_onStatusUpdated); } - void _fetchCeilingSensorStatus( - CeilingInitialEvent event, Emitter emit) async { + Future _fetchCeilingSensorStatus( + CeilingInitialEvent event, + Emitter emit, + ) async { emit(CeilingLoadingInitialState()); try { - var response = - await DevicesManagementApi().getDeviceStatus(event.deviceId); + final response = await DevicesManagementApi().getDeviceStatus(event.deviceId); deviceStatus = CeilingSensorModel.fromJson(response.status); emit(CeilingUpdateState(ceilingSensorModel: deviceStatus)); _listenToChanges(event.deviceId); } catch (e) { emit(CeilingFailedState(error: e.toString())); - return; } } - _listenToChanges(deviceId) { + void _listenToChanges(String deviceId) { try { - DatabaseReference ref = - FirebaseDatabase.instance.ref('device-status/$deviceId'); - Stream stream = ref.onValue; + final ref = FirebaseDatabase.instance.ref('device-status/$deviceId'); + final stream = ref.onValue; stream.listen((DatabaseEvent event) { - Map usersMap = - event.snapshot.value as Map; + if (event.snapshot.value == null) return; + + final usersMap = event.snapshot.value as Map; + final statusList = []; - List statusList = []; usersMap['status'].forEach((element) { - statusList - .add(Status(code: element['code'], value: element['value'])); + statusList.add(Status(code: element['code'], value: element['value'])); }); deviceStatus = CeilingSensorModel.fromJson(statusList); @@ -65,149 +69,127 @@ class CeilingSensorBloc extends Bloc { } catch (_) {} } - void _onStatusUpdated(StatusUpdated event, Emitter emit) { + void _onStatusUpdated( + StatusUpdated event, + Emitter emit, + ) { deviceStatus = event.deviceStatus; emit(CeilingUpdateState(ceilingSensorModel: deviceStatus)); } - void _changeValue( - CeilingChangeValueEvent event, Emitter emit) async { + Future _changeValue( + CeilingChangeValueEvent event, + Emitter emit, + ) async { emit(CeilingLoadingNewSate(ceilingSensorModel: deviceStatus)); - if (event.code == 'sensitivity') { - deviceStatus.sensitivity = event.value; - } else if (event.code == 'none_body_time') { - deviceStatus.noBodyTime = event.value; - } else if (event.code == 'moving_max_dis') { - deviceStatus.maxDistance = event.value; - } else if (event.code == 'scene') { - deviceStatus.spaceType = getSpaceType(event.value); - } + _updateDeviceFunctionFromCode(event.code, event.value); emit(CeilingUpdateState(ceilingSensorModel: deviceStatus)); - await _runDeBouncer( - deviceId: deviceId, - code: event.code, - value: event.value, - emit: emit, - isBatch: false, - ); + + try { + await controlDeviceService.controlDevice( + deviceUuid: deviceId, + status: Status(code: event.code, value: event.value), + ); + } catch (e) { + emit(CeilingFailedState(error: e.toString())); + } } Future _onBatchControl( - CeilingBatchControlEvent event, Emitter emit) async { + CeilingBatchControlEvent event, + Emitter emit, + ) async { emit(CeilingLoadingNewSate(ceilingSensorModel: deviceStatus)); - if (event.code == 'sensitivity') { - deviceStatus.sensitivity = event.value; - } else if (event.code == 'none_body_time') { - deviceStatus.noBodyTime = event.value; - } else if (event.code == 'moving_max_dis') { - deviceStatus.maxDistance = event.value; - } else if (event.code == 'scene') { - deviceStatus.spaceType = getSpaceType(event.value); - } + _updateDeviceFunctionFromCode(event.code, event.value); emit(CeilingUpdateState(ceilingSensorModel: deviceStatus)); - await _runDeBouncer( - deviceId: event.deviceIds, - code: event.code, - value: event.value, - emit: emit, - isBatch: true, - ); - } - _runDeBouncer({ - required dynamic deviceId, - required String code, - required dynamic value, - required Emitter emit, - required bool isBatch, - }) { - late String id; + try { + final success = await batchControlDevicesService.batchControlDevices( + uuids: event.deviceIds, + code: event.code, + value: event.value, + ); - if (deviceId is List) { - id = deviceId.first; - } else { - id = deviceId; - } - - if (_timer != null) { - _timer!.cancel(); - } - _timer = Timer(const Duration(seconds: 1), () async { - try { - late bool response; - if (isBatch) { - response = await DevicesManagementApi() - .deviceBatchControl(deviceId, code, value); - } else { - response = await DevicesManagementApi() - .deviceControl(deviceId, Status(code: code, value: value)); - } - - if (!response) { - add(CeilingInitialEvent(id)); - } - if (response == true && code == 'scene') { - emit(CeilingLoadingInitialState()); - await Future.delayed(const Duration(seconds: 1)); - add(CeilingInitialEvent(id)); - } - } catch (_) { - await Future.delayed(const Duration(milliseconds: 500)); - add(CeilingInitialEvent(id)); + if (!success) { + emit(const CeilingFailedState(error: 'Failed to control devices')); } - }); + } catch (e) { + emit(CeilingFailedState(error: e.toString())); + } } - FutureOr _getDeviceReports(GetCeilingDeviceReportsEvent event, - Emitter emit) async { + void _updateDeviceFunctionFromCode(String code, dynamic value) { + switch (code) { + case 'sensitivity': + deviceStatus.sensitivity = value; + break; + case 'none_body_time': + deviceStatus.noBodyTime = value; + break; + case 'moving_max_dis': + deviceStatus.maxDistance = value; + break; + case 'scene': + deviceStatus.spaceType = getSpaceType(value); + break; + default: + break; + } + } + + Future _getDeviceReports( + GetCeilingDeviceReportsEvent event, + Emitter emit, + ) async { if (event.code.isEmpty) { emit(ShowCeilingDescriptionState(description: reportString)); return; - } else { - emit(CeilingReportsLoadingState()); - // final from = DateTime.now().subtract(const Duration(days: 30)).millisecondsSinceEpoch; - // final to = DateTime.now().millisecondsSinceEpoch; + } - try { - // await DevicesManagementApi.getDeviceReportsByDate(deviceId, event.code, from.toString(), to.toString()) - await DevicesManagementApi.getDeviceReports(deviceId, event.code) - .then((value) { - emit(CeilingReportsState(deviceReport: value)); - }); - } catch (e) { - emit(CeilingReportsFailedState(error: e.toString())); - return; - } + emit(CeilingReportsLoadingState()); + try { + final value = await DevicesManagementApi.getDeviceReports( + deviceId, + event.code, + ); + emit(CeilingReportsState(deviceReport: value)); + } catch (e) { + emit(CeilingReportsFailedState(error: e.toString())); } } void _showDescription( - ShowCeilingDescriptionEvent event, Emitter emit) { + ShowCeilingDescriptionEvent event, + Emitter emit, + ) { emit(ShowCeilingDescriptionState(description: event.description)); } void _backToGridView( - BackToCeilingGridViewEvent event, Emitter emit) { + BackToCeilingGridViewEvent event, + Emitter emit, + ) { emit(CeilingUpdateState(ceilingSensorModel: deviceStatus)); } - FutureOr _fetchCeilingSensorBatchControl( - CeilingFetchDeviceStatusEvent event, - Emitter emit) async { + Future _fetchCeilingSensorBatchControl( + CeilingFetchDeviceStatusEvent event, + Emitter emit, + ) async { emit(CeilingLoadingInitialState()); try { - var response = - await DevicesManagementApi().getBatchStatus(event.devicesIds); + final response = await DevicesManagementApi().getBatchStatus(event.devicesIds); deviceStatus = CeilingSensorModel.fromJson(response.status); emit(CeilingUpdateState(ceilingSensorModel: deviceStatus)); } catch (e) { emit(CeilingFailedState(error: e.toString())); - return; } } - FutureOr _onFactoryReset( - CeilingFactoryResetEvent event, Emitter emit) async { + Future _onFactoryReset( + CeilingFactoryResetEvent event, + Emitter emit, + ) async { emit(CeilingLoadingNewSate(ceilingSensorModel: deviceStatus)); try { final response = await DevicesManagementApi().factoryReset( diff --git a/lib/pages/device_managment/ceiling_sensor/factories/ceiling_sensor_bloc_factory.dart b/lib/pages/device_managment/ceiling_sensor/factories/ceiling_sensor_bloc_factory.dart new file mode 100644 index 00000000..d371efb1 --- /dev/null +++ b/lib/pages/device_managment/ceiling_sensor/factories/ceiling_sensor_bloc_factory.dart @@ -0,0 +1,18 @@ +import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/ceiling_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/factories/device_bloc_dependencies_factory.dart'; + +abstract final class CeilingSensorBlocFactory { + const CeilingSensorBlocFactory._(); + + static CeilingSensorBloc create({ + required String deviceId, + }) { + return CeilingSensorBloc( + deviceId: deviceId, + controlDeviceService: + DeviceBlocDependenciesFactory.createControlDeviceService(), + batchControlDevicesService: + DeviceBlocDependenciesFactory.createBatchControlDevicesService(), + ); + } +} diff --git a/lib/pages/device_managment/ceiling_sensor/view/ceiling_sensor_batch_control.dart b/lib/pages/device_managment/ceiling_sensor/view/ceiling_sensor_batch_control.dart index cf645b6f..9b5ab360 100644 --- a/lib/pages/device_managment/ceiling_sensor/view/ceiling_sensor_batch_control.dart +++ b/lib/pages/device_managment/ceiling_sensor/view/ceiling_sensor_batch_control.dart @@ -4,9 +4,9 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_re import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/ceiling_bloc.dart'; import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/ceiling_event.dart'; import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/ceiling_state.dart'; +import 'package:syncrow_web/pages/device_managment/ceiling_sensor/factories/ceiling_sensor_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/ceiling_sensor/model/ceiling_sensor_model.dart'; import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; -// import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presence_space_type.dart'; import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presence_update_data.dart'; import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presense_nobody_time.dart'; @@ -23,8 +23,9 @@ class CeilingSensorBatchControlView extends StatelessWidget with HelperResponsiv final isLarge = isLargeScreenSize(context); final isMedium = isMediumScreenSize(context); return BlocProvider( - create: (context) => CeilingSensorBloc(deviceId: devicesIds.first) - ..add(CeilingFetchDeviceStatusEvent(devicesIds)), + create: (context) => CeilingSensorBlocFactory.create( + deviceId: devicesIds.first, + )..add(CeilingFetchDeviceStatusEvent(devicesIds)), child: BlocBuilder( builder: (context, state) { if (state is CeilingLoadingInitialState || state is CeilingReportsLoadingState) { @@ -110,7 +111,6 @@ class CeilingSensorBatchControlView extends StatelessWidget with HelperResponsiv ), ), ), - // FirmwareUpdateWidget(deviceId: devicesIds.first, version: 4), FactoryResetWidget( callFactoryReset: () { context.read().add( diff --git a/lib/pages/device_managment/ceiling_sensor/view/ceiling_sensor_controls.dart b/lib/pages/device_managment/ceiling_sensor/view/ceiling_sensor_controls.dart index 36b676e9..f3017a7c 100644 --- a/lib/pages/device_managment/ceiling_sensor/view/ceiling_sensor_controls.dart +++ b/lib/pages/device_managment/ceiling_sensor/view/ceiling_sensor_controls.dart @@ -4,6 +4,7 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_mo import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/ceiling_bloc.dart'; import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/ceiling_event.dart'; import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/ceiling_state.dart'; +import 'package:syncrow_web/pages/device_managment/ceiling_sensor/factories/ceiling_sensor_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/ceiling_sensor/model/ceiling_sensor_model.dart'; import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presence_display_data.dart'; import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presence_space_type.dart'; @@ -28,8 +29,9 @@ class CeilingSensorControlsView extends StatelessWidget final isLarge = isLargeScreenSize(context); final isMedium = isMediumScreenSize(context); return BlocProvider( - create: (context) => CeilingSensorBloc(deviceId: device.uuid ?? '') - ..add(CeilingInitialEvent(device.uuid ?? '')), + create: (context) => CeilingSensorBlocFactory.create( + deviceId: device.uuid ?? '', + )..add(CeilingInitialEvent(device.uuid ?? '')), child: BlocBuilder( builder: (context, state) { if (state is CeilingLoadingInitialState || diff --git a/lib/pages/device_managment/curtain/bloc/curtain_bloc.dart b/lib/pages/device_managment/curtain/bloc/curtain_bloc.dart index 251d999f..749a7729 100644 --- a/lib/pages/device_managment/curtain/bloc/curtain_bloc.dart +++ b/lib/pages/device_managment/curtain/bloc/curtain_bloc.dart @@ -1,17 +1,25 @@ import 'dart:async'; + import 'package:firebase_database/firebase_database.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; import 'package:syncrow_web/pages/device_managment/curtain/bloc/curtain_event.dart'; import 'package:syncrow_web/pages/device_managment/curtain/bloc/curtain_state.dart'; +import 'package:syncrow_web/services/batch_control_devices_service.dart'; +import 'package:syncrow_web/services/control_device_service.dart'; import 'package:syncrow_web/services/devices_mang_api.dart'; class CurtainBloc extends Bloc { late bool deviceStatus; final String deviceId; - Timer? _timer; + final ControlDeviceService controlDeviceService; + final BatchControlDevicesService batchControlDevicesService; - CurtainBloc({required this.deviceId}) : super(CurtainInitial()) { + CurtainBloc({ + required this.deviceId, + required this.controlDeviceService, + required this.batchControlDevicesService, + }) : super(CurtainInitial()) { on(_onFetchDeviceStatus); on(_onFetchBatchStatus); on(_onCurtainControl); @@ -20,32 +28,31 @@ class CurtainBloc extends Bloc { on(_onStatusUpdated); } - FutureOr _onFetchDeviceStatus( - CurtainFetchDeviceStatus event, Emitter emit) async { + Future _onFetchDeviceStatus( + CurtainFetchDeviceStatus event, + Emitter emit, + ) async { emit(CurtainStatusLoading()); try { - final status = - await DevicesManagementApi().getDeviceStatus(event.deviceId); - _listenToChanges(event.deviceId); + final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); + _listenToChanges(event.deviceId, emit); deviceStatus = _checkStatus(status.status[0].value); - emit(CurtainStatusLoaded(deviceStatus)); } catch (e) { emit(CurtainError(e.toString())); } } - void _listenToChanges(String deviceId) { + void _listenToChanges(String deviceId, Emitter emit) { try { - DatabaseReference ref = - FirebaseDatabase.instance.ref('device-status/$deviceId'); - Stream stream = ref.onValue; + final ref = FirebaseDatabase.instance.ref('device-status/$deviceId'); + final stream = ref.onValue; stream.listen((DatabaseEvent event) { final data = event.snapshot.value as Map?; if (data == null) return; - List statusList = []; + final statusList = []; if (data['status'] != null) { for (var element in data['status']) { statusList.add( @@ -57,7 +64,7 @@ class CurtainBloc extends Bloc { } } if (statusList.isNotEmpty) { - bool newStatus = _checkStatus(statusList[0].value); + final newStatus = _checkStatus(statusList[0].value); if (newStatus != deviceStatus) { deviceStatus = newStatus; if (!isClosed) { @@ -71,76 +78,32 @@ class CurtainBloc extends Bloc { } } - void _onStatusUpdated(StatusUpdated event, Emitter emit) { + void _onStatusUpdated( + StatusUpdated event, + Emitter emit, + ) { emit(CurtainStatusLoading()); deviceStatus = event.deviceStatus; emit(CurtainStatusLoaded(deviceStatus)); } - FutureOr _onCurtainControl( - CurtainControl event, Emitter emit) async { - final oldValue = deviceStatus; - + Future _onCurtainControl( + CurtainControl event, + Emitter emit, + ) async { + emit(CurtainStatusLoading()); _updateLocalValue(event.value, emit); - emit(CurtainStatusLoaded(deviceStatus)); - - await _runDebounce( - deviceId: event.deviceId, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - isBatch: false, - ); - } - - Future _runDebounce({ - required dynamic deviceId, - required String code, - required bool value, - required bool oldValue, - required Emitter emit, - required bool isBatch, - }) async { - late String id; - - if (deviceId is List) { - id = deviceId.first; - } else { - id = deviceId; + try { + final controlValue = event.value ? 'open' : 'close'; + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status(code: event.code, value: controlValue), + ); + } catch (e) { + _updateLocalValue(!event.value, emit); + emit(CurtainControlError(e.toString())); } - - if (_timer != null) { - _timer!.cancel(); - } - _timer = Timer(const Duration(seconds: 1), () async { - try { - final controlValue = value ? 'open' : 'close'; - - late bool response; - if (isBatch) { - response = await DevicesManagementApi() - .deviceBatchControl(deviceId, code, controlValue); - } else { - response = await DevicesManagementApi() - .deviceControl(deviceId, Status(code: code, value: controlValue)); - } - - if (!response) { - _revertValueAndEmit(id, oldValue, emit); - } - } catch (e) { - _revertValueAndEmit(id, oldValue, emit); - } - }); - } - - void _revertValueAndEmit( - String deviceId, bool oldValue, Emitter emit) { - _updateLocalValue(oldValue, emit); - emit(CurtainStatusLoaded(deviceStatus)); - emit(const CurtainControlError('Failed to control the device.')); } void _updateLocalValue(bool value, Emitter emit) { @@ -152,41 +115,44 @@ class CurtainBloc extends Bloc { return command.toLowerCase() == 'open'; } - FutureOr _onFetchBatchStatus( - CurtainFetchBatchStatus event, Emitter emit) async { + Future _onFetchBatchStatus( + CurtainFetchBatchStatus event, + Emitter emit, + ) async { emit(CurtainStatusLoading()); try { - final status = - await DevicesManagementApi().getBatchStatus(event.devicesIds); - + final status = await DevicesManagementApi().getBatchStatus(event.devicesIds); deviceStatus = _checkStatus(status.status[0].value); - emit(CurtainStatusLoaded(deviceStatus)); } catch (e) { emit(CurtainError(e.toString())); } } - FutureOr _onCurtainBatchControl( - CurtainBatchControl event, Emitter emit) async { - final oldValue = deviceStatus; - + Future _onCurtainBatchControl( + CurtainBatchControl event, + Emitter emit, + ) async { + emit(CurtainStatusLoading()); _updateLocalValue(event.value, emit); - emit(CurtainStatusLoaded(deviceStatus)); - - await _runDebounce( - deviceId: event.devicesIds, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - isBatch: true, - ); + try { + final controlValue = event.value ? 'open' : 'stop'; + await batchControlDevicesService.batchControlDevices( + uuids: event.devicesIds, + code: event.code, + value: controlValue, + ); + } catch (e) { + _updateLocalValue(!event.value, emit); + emit(CurtainControlError(e.toString())); + } } - FutureOr _onFactoryReset( - CurtainFactoryReset event, Emitter emit) async { + Future _onFactoryReset( + CurtainFactoryReset event, + Emitter emit, + ) async { emit(CurtainStatusLoading()); try { final response = await DevicesManagementApi().factoryReset( diff --git a/lib/pages/device_managment/curtain/factories/curtain_bloc_factory.dart b/lib/pages/device_managment/curtain/factories/curtain_bloc_factory.dart new file mode 100644 index 00000000..f6257b0a --- /dev/null +++ b/lib/pages/device_managment/curtain/factories/curtain_bloc_factory.dart @@ -0,0 +1,18 @@ +import 'package:syncrow_web/pages/device_managment/curtain/bloc/curtain_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/factories/device_bloc_dependencies_factory.dart'; + +abstract final class CurtainBlocFactory { + const CurtainBlocFactory._(); + + static CurtainBloc create({ + required String deviceId, + }) { + return CurtainBloc( + deviceId: deviceId, + controlDeviceService: + DeviceBlocDependenciesFactory.createControlDeviceService(), + batchControlDevicesService: + DeviceBlocDependenciesFactory.createBatchControlDevicesService(), + ); + } +} diff --git a/lib/pages/device_managment/curtain/view/curtain_batch_status_view.dart b/lib/pages/device_managment/curtain/view/curtain_batch_status_view.dart index 7c873e20..41dcaf9e 100644 --- a/lib/pages/device_managment/curtain/view/curtain_batch_status_view.dart +++ b/lib/pages/device_managment/curtain/view/curtain_batch_status_view.dart @@ -5,6 +5,7 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_re import 'package:syncrow_web/pages/device_managment/curtain/bloc/curtain_bloc.dart'; import 'package:syncrow_web/pages/device_managment/curtain/bloc/curtain_event.dart'; import 'package:syncrow_web/pages/device_managment/curtain/bloc/curtain_state.dart'; +import 'package:syncrow_web/pages/device_managment/curtain/factories/curtain_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; // import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; @@ -18,7 +19,7 @@ class CurtainBatchStatusView extends StatelessWidget with HelperResponsiveLayout Widget build(BuildContext context) { return BlocProvider( create: (context) => - CurtainBloc(deviceId: devicesIds.first)..add(CurtainFetchBatchStatus(devicesIds)), + CurtainBlocFactory.create(deviceId: devicesIds.first)..add(CurtainFetchBatchStatus(devicesIds)), child: BlocBuilder( builder: (context, state) { if (state is CurtainStatusLoading) { diff --git a/lib/pages/device_managment/curtain/view/curtain_status_view.dart b/lib/pages/device_managment/curtain/view/curtain_status_view.dart index 2afe49f4..84b0a943 100644 --- a/lib/pages/device_managment/curtain/view/curtain_status_view.dart +++ b/lib/pages/device_managment/curtain/view/curtain_status_view.dart @@ -4,6 +4,7 @@ import 'package:syncrow_web/pages/common/curtain_toggle.dart'; import 'package:syncrow_web/pages/device_managment/curtain/bloc/curtain_bloc.dart'; import 'package:syncrow_web/pages/device_managment/curtain/bloc/curtain_event.dart'; import 'package:syncrow_web/pages/device_managment/curtain/bloc/curtain_state.dart'; +import 'package:syncrow_web/pages/device_managment/curtain/factories/curtain_bloc_factory.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; class CurtainStatusControlsView extends StatelessWidget @@ -15,7 +16,7 @@ class CurtainStatusControlsView extends StatelessWidget @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => CurtainBloc(deviceId: deviceId) + create: (context) => CurtainBlocFactory.create(deviceId: deviceId) ..add(CurtainFetchDeviceStatus(deviceId)), child: BlocBuilder( builder: (context, state) { diff --git a/lib/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart b/lib/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart new file mode 100644 index 00000000..c996cf72 --- /dev/null +++ b/lib/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart @@ -0,0 +1,165 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/common/bloc/project_manager.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/device_info_model.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/bloc/setting_bloc_state.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/sub_space_model.dart'; +import 'package:syncrow_web/services/devices_mang_api.dart'; +import 'package:syncrow_web/services/space_mana_api.dart'; +import 'package:syncrow_web/utils/snack_bar.dart'; +part 'setting_bloc_event.dart'; + +class SettingDeviceBloc extends Bloc { + final String deviceId; + SettingDeviceBloc({ + required this.deviceId, + }) : super(const DeviceSettingsInitial()) { + on(_fetchDeviceInfo); + on(_saveName); + on(_changeName); + on(_deleteDevice); + on(_fetchRooms); + on(_onAssignDevice); + } + final TextEditingController nameController = TextEditingController(); + List roomsList = []; + bool isEditingName = false; + + bool _validateInputs() { + final nameError = _fullNameValidator(nameController.text); + if (nameError != null) { + CustomSnackBar.displaySnackBar(nameError); + return true; + } + return false; + } + + String? _fullNameValidator(String? value) { + if (value == null) return 'name is required'; + final withoutExtraSpaces = value.replaceAll(RegExp(r"\s+"), ' ').trim(); + if (withoutExtraSpaces.length < 2 || withoutExtraSpaces.length > 30) { + return 'name must be between 2 and 30 characters long'; + } + if (RegExp(r"/[^ a-zA-Z0-9-\']/").hasMatch(withoutExtraSpaces)) { + return 'Only alphanumeric characters, space, dash and single quote are allowed'; + } + return null; + } + + Future _saveName( + SettingBlocSaveName event, Emitter emit) async { + if (_validateInputs()) return; + try { + emit(DeviceSettingsLoading()); + await DevicesManagementApi.putDeviceName( + deviceId: deviceId, deviceName: nameController.text); + add(DeviceSettingInitialInfo()); + CustomSnackBar.displaySnackBar('Save Successfully'); + emit(DeviceSettingsUpdate(deviceName: nameController.text)); + } catch (e) { + emit(DeviceSettingsError(message: e.toString())); + } + } + + Future _fetchDeviceInfo( + DeviceSettingInitialInfo event, Emitter emit) async { + try { + emit(DeviceSettingsLoading()); + var response = await DevicesManagementApi.getDeviceInfo(deviceId); + DeviceInfoModel deviceInfo = DeviceInfoModel.fromJson(response); + nameController.text = deviceInfo.name; + emit(DeviceSettingsUpdate( + deviceName: nameController.text, + deviceInfo: deviceInfo, + roomsList: roomsList, + )); + } catch (e) { + emit(DeviceSettingsError(message: e.toString())); + } + } + + bool editName = false; + final FocusNode focusNode = FocusNode(); + + void _changeName(ChangeNameEvent event, Emitter emit) { + emit(DeviceSettingsInitial( + deviceName: nameController.text, + deviceId: deviceId, + isEditingName: event.value ?? false, + editingNameValue: event.value?.toString() ?? '', + deviceInfo: state.deviceInfo, + )); + editName = event.value!; + if (editName) { + Future.delayed(const Duration(milliseconds: 500), () { + focusNode.requestFocus(); + }); + } else { + add(const SettingBlocSaveName()); + focusNode.unfocus(); + } + emit(DeviceSettingsUpdate( + deviceName: nameController.text, + deviceInfo: state.deviceInfo, + roomsList: roomsList, + )); + } + + void _deleteDevice( + SettingBlocDeleteDevice event, Emitter emit) async { + try { + emit(DeviceSettingsLoading()); + await DevicesManagementApi.resetDevice(devicesUuid: deviceId); + CustomSnackBar.displaySnackBar('Reset Successfully'); + emit(DeviceSettingsUpdate( + deviceName: nameController.text, + deviceInfo: state.deviceInfo, + roomsList: roomsList, + )); + } catch (e) { + emit(DeviceSettingsError(message: e.toString())); + return; + } + } + + void _onAssignDevice( + SettingBlocAssignRoom event, Emitter emit) async { + try { + emit(DeviceSettingsLoading()); + final projectUuid = await ProjectManager.getProjectUUID() ?? ''; + await CommunitySpaceManagementApi.assignDeviceToRoom( + communityId: event.communityUuid, + spaceId: event.spaceUuid, + subSpaceId: event.subSpaceUuid, + deviceId: deviceId, + projectId: projectUuid); + add(DeviceSettingInitialInfo()); + CustomSnackBar.displaySnackBar('Save Successfully'); + emit(DeviceSettingsSaveSelectionSuccess()); + } catch (e) { + emit(DeviceSettingsError(message: e.toString())); + return; + } + } + + void _fetchRooms( + SettingBlocFetchRooms event, Emitter emit) async { + try { + emit(DeviceSettingsLoading()); + final projectUuid = await ProjectManager.getProjectUUID() ?? ''; + roomsList = await CommunitySpaceManagementApi.getSubSpaceBySpaceId( + communityId: event.communityUuid, + spaceId: event.spaceUuid, + projectId: projectUuid); + emit(DeviceSettingsUpdate( + deviceName: nameController.text, + deviceInfo: state.deviceInfo, + roomsList: roomsList, + )); + } catch (e) { + emit(DeviceSettingsError(message: e.toString())); + return; + } + } +} diff --git a/lib/pages/device_managment/device_setting/bloc/setting_bloc_event.dart b/lib/pages/device_managment/device_setting/bloc/setting_bloc_event.dart new file mode 100644 index 00000000..7fb62ed9 --- /dev/null +++ b/lib/pages/device_managment/device_setting/bloc/setting_bloc_event.dart @@ -0,0 +1,69 @@ +part of 'setting_bloc_bloc.dart'; + +abstract class DeviceSettingEvent extends Equatable { + const DeviceSettingEvent(); + @override + List get props => []; +} + +class SettingBlocSaveDeviceName extends DeviceSettingEvent { + final String deviceName; + final String deviceId; + + const SettingBlocSaveDeviceName( + {required this.deviceName, required this.deviceId}); + + @override + List get props => [deviceName, deviceId]; +} + +class SettingBlocStartEditingName extends DeviceSettingEvent {} + +class SettingBlocCancelEditingName extends DeviceSettingEvent {} + +class SettingBlocChangeEditingNameValue extends DeviceSettingEvent { + final String value; + const SettingBlocChangeEditingNameValue(this.value); + + @override + List get props => [value]; +} + +class SettingBlocFetchRooms extends DeviceSettingEvent { + final String communityUuid; + final String spaceUuid; + + const SettingBlocFetchRooms( + {required this.communityUuid, required this.spaceUuid}); + + @override + List get props => [communityUuid, spaceUuid]; +} + +class SettingBlocSaveName extends DeviceSettingEvent { + const SettingBlocSaveName(); +} + +class DeviceSettingInitialInfo extends DeviceSettingEvent {} + +class ChangeNameEvent extends DeviceSettingEvent { + final bool? value; + const ChangeNameEvent({this.value}); +} + +class SettingBlocDeleteDevice extends DeviceSettingEvent {} + +class SettingBlocAssignRoom extends DeviceSettingEvent { + final String communityUuid; + final String spaceUuid; + final String subSpaceUuid; + + const SettingBlocAssignRoom({ + required this.communityUuid, + required this.spaceUuid, + required this.subSpaceUuid, + }); + + @override + List get props => [spaceUuid, communityUuid, subSpaceUuid]; +} diff --git a/lib/pages/device_managment/device_setting/bloc/setting_bloc_state.dart b/lib/pages/device_managment/device_setting/bloc/setting_bloc_state.dart new file mode 100644 index 00000000..55054c9a --- /dev/null +++ b/lib/pages/device_managment/device_setting/bloc/setting_bloc_state.dart @@ -0,0 +1,81 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/device_info_model.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/sub_space_model.dart'; + +abstract class DeviceSettingsState extends Equatable { + const DeviceSettingsState({this.deviceInfo}); + + final DeviceInfoModel? deviceInfo; + + @override + List get props => [deviceInfo]; +} + +class DeviceSettingsInitial extends DeviceSettingsState { + final String deviceName; + final String deviceId; + final bool isEditingName; + final String editingNameValue; + + const DeviceSettingsInitial({ + this.deviceName = '', + this.deviceId = '', + this.isEditingName = false, + this.editingNameValue = '', + super.deviceInfo, + }); + + DeviceSettingsInitial copyWith({ + String? deviceName, + String? deviceId, + bool? isEditingName, + String? editingNameValue, + }) => + DeviceSettingsInitial( + deviceName: deviceName ?? this.deviceName, + deviceId: deviceId ?? this.deviceId, + isEditingName: isEditingName ?? this.isEditingName, + editingNameValue: editingNameValue ?? this.editingNameValue, + ); + + @override + List get props => + [deviceName, deviceId, isEditingName, editingNameValue]; +} + +class DeviceSettingsLoading extends DeviceSettingsState {} + +class DeviceSettingsUpdate extends DeviceSettingsState { + final String? deviceName; + final List roomsList; + + const DeviceSettingsUpdate({ + this.deviceName, + super.deviceInfo, + this.roomsList = const [], + }); + + @override + List get props => [deviceName, deviceInfo, roomsList]; +} + +class DeviceSettingsError extends DeviceSettingsState { + final String message; + + const DeviceSettingsError({required this.message}); + @override + List get props => [message]; +} + +class DeviceSettingsFetchRooms extends DeviceSettingsState { + final List roomsList; + + const DeviceSettingsFetchRooms({required this.roomsList}); + + @override + List get props => [roomsList]; +} + +class DeviceSettingsSaveSelectionSuccess extends DeviceSettingsState {} + +class ChangeNameState extends DeviceSettingsState {} diff --git a/lib/pages/device_managment/device_setting/device_icon_type_helper.dart b/lib/pages/device_managment/device_setting/device_icon_type_helper.dart new file mode 100644 index 00000000..13f8abfe --- /dev/null +++ b/lib/pages/device_managment/device_setting/device_icon_type_helper.dart @@ -0,0 +1,28 @@ +import 'package:syncrow_web/utils/constants/assets.dart'; + +class DeviceIconTypeHelper { + static const Map _iconMap = { + 'AC': Assets.ac, + 'GW': Assets.gateway, + 'CPS': Assets.sensors, + 'DL': Assets.doorLock, + 'WPS': Assets.sensors, + '3G': Assets.gangSwitch, + '2G': Assets.twoGang, + '1G': Assets.oneGang, + 'CUR': Assets.curtain, + 'WH': Assets.waterHeater, + 'DS': Assets.doorSensor, + '1GT': Assets.oneTouchSwitch, + '2GT': Assets.twoTouchSwitch, + '3GT': Assets.threeTouchSwitch, + 'GD': Assets.garageDoor, + 'WL': Assets.waterLeakNormal, + 'NCPS': Assets.sensors, + }; + + static String getDeviceIconByTypeCode(String? typeCode) { + if (typeCode == null) return Assets.logoHorizontal; + return _iconMap[typeCode] ?? Assets.logoHorizontal; + } +} diff --git a/lib/pages/device_managment/device_setting/device_management_content.dart b/lib/pages/device_managment/device_setting/device_management_content.dart new file mode 100644 index 00000000..9c758341 --- /dev/null +++ b/lib/pages/device_managment/device_setting/device_management_content.dart @@ -0,0 +1,127 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/device_info_model.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/sub_space_model.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/sub_space_dialog.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:syncrow_web/web_layout/default_container.dart'; + +class DeviceManagementContent extends StatelessWidget { + const DeviceManagementContent({ + super.key, + required this.device, + required this.subSpaces, + required this.deviceInfo, + }); + + final AllDevicesModel device; + final List subSpaces; + final DeviceInfoModel deviceInfo; + + @override + Widget build(BuildContext context) { + Widget infoRow( + {required String label, + required String value, + Widget? trailing, + required Color? valueColor}) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: context.theme.textTheme.bodyMedium!.copyWith( + fontSize: 14, + color: ColorsManager.grayColor, + ), + ), + Expanded( + child: Text( + value, + textAlign: TextAlign.end, + style: context.theme.textTheme.bodyMedium! + .copyWith(fontSize: 14, color: valueColor), + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 8), + trailing ?? const SizedBox.shrink(), + ], + ), + ); + } + + return DefaultContainer( + padding: EdgeInsets.zero, + child: Column( + children: [ + const SizedBox(height: 5), + Padding( + padding: const EdgeInsets.all(10.0), + child: InkWell( + onTap: () { + showSubSpaceDialog( + context, + communityUuid: device.community!.uuid!, + spaceUuid: device.spaces!.first.uuid!, + subSpaces: subSpaces, + selected: device.subspace!.uuid, + ); + }, + child: infoRow( + label: 'Sub-Space:', + value: deviceInfo.subspace.subspaceName, + valueColor: ColorsManager.textGray, + trailing: const Icon( + Icons.arrow_forward_ios, + size: 16, + color: ColorsManager.greyColor, + ), + ), + ), + ), + const Divider(color: ColorsManager.dividerColor), + Padding( + padding: const EdgeInsets.all(10.0), + child: infoRow( + label: 'Virtual Address:', + value: deviceInfo.productUuid, + valueColor: ColorsManager.blackColor, + trailing: InkWell( + onTap: () { + Clipboard.setData( + ClipboardData(text: device.productUuid ?? ''), + ); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Virtual Address copied to clipboard'), + ), + ); + }, + child: const Icon( + Icons.copy, + size: 16, + color: ColorsManager.greyColor, + ), + ), + ), + ), + const Divider(color: ColorsManager.dividerColor), + Padding( + padding: const EdgeInsets.all(10.0), + child: infoRow( + label: 'MAC Address:', + valueColor: ColorsManager.blackColor, + value: deviceInfo.macAddress, + ), + ), + const SizedBox(height: 5), + ], + ), + ); + } +} diff --git a/lib/pages/device_managment/device_setting/device_settings_panel.dart b/lib/pages/device_managment/device_setting/device_settings_panel.dart new file mode 100644 index 00000000..cebd80b3 --- /dev/null +++ b/lib/pages/device_managment/device_setting/device_settings_panel.dart @@ -0,0 +1,184 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/device_icon_type_helper.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/device_management_content.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/remove_device_widget.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/device_info_model.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/bloc/setting_bloc_state.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/sub_space_model.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:syncrow_web/web_layout/default_container.dart'; + +class DeviceSettingsPanel extends StatelessWidget { + final VoidCallback? onClose; + final AllDevicesModel device; + const DeviceSettingsPanel({super.key, this.onClose, required this.device}); + + @override + Widget build(BuildContext context) { + final sectionTitle = context.theme.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.bold, + color: ColorsManager.grayColor, + ); + return BlocProvider( + create: (context) => SettingDeviceBloc( + deviceId: device.uuid ?? '', + ) + ..add(DeviceSettingInitialInfo()) + ..add(SettingBlocFetchRooms( + communityUuid: device.community!.uuid!, + spaceUuid: device.spaces!.first.uuid!, + )), + child: Builder( + builder: (context) { + return BlocBuilder( + builder: (context, state) { + final _bloc = context.read(); + final iconPath = DeviceIconTypeHelper.getDeviceIconByTypeCode( + device.productType); + final deviceInfo = state is DeviceSettingsUpdate + ? state.deviceInfo ?? DeviceInfoModel.empty() + : DeviceInfoModel.empty(); + final subSpaces = + state is DeviceSettingsUpdate ? state.roomsList ?? [] : []; + return Stack( + children: [ + Container( + width: MediaQuery.of(context).size.width * 0.3, + color: ColorsManager.grey25, + padding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 24), + child: ListView( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + icon: SvgPicture.asset(Assets.closeSettingsIcon), + onPressed: + onClose ?? () => Navigator.of(context).pop(), + ), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Device Settings', + style: + context.theme.textTheme.titleLarge!.copyWith( + fontWeight: FontWeight.bold, + color: ColorsManager.primaryColor, + ), + ), + ], + ), + const SizedBox(height: 24), + DefaultContainer( + child: Row( + children: [ + CircleAvatar( + radius: 40, + backgroundColor: + const Color.fromARGB(177, 213, 213, 213), + child: CircleAvatar( + backgroundColor: ColorsManager.whiteColors, + radius: 36, + child: SvgPicture.asset( + iconPath, + fit: BoxFit.cover, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Device Name:', + style: context.textTheme.bodyMedium! + .copyWith( + color: ColorsManager.grayColor, + ), + ), + TextFormField( + maxLength: 30, + style: const TextStyle( + color: ColorsManager.blackColor, + ), + textAlign: TextAlign.start, + focusNode: _bloc.focusNode, + controller: _bloc.nameController, + enabled: _bloc.editName, + onFieldSubmitted: (value) { + _bloc.add(const ChangeNameEvent( + value: false)); + }, + decoration: const InputDecoration( + border: InputBorder.none, + fillColor: Colors.white10, + counterText: '', + ), + ), + ], + ), + ), + const SizedBox(width: 8), + Visibility( + visible: _bloc.editName != true, + replacement: const SizedBox(), + child: GestureDetector( + onTap: () { + _bloc.add( + const ChangeNameEvent(value: true)); + }, + child: SvgPicture.asset( + Assets.editNameIconSettings, + color: ColorsManager.grayColor, + height: 20, + width: 20, + ), + ), + ) + ], + ), + ), + const SizedBox(height: 32), + Text('Device Management', style: sectionTitle), + DeviceManagementContent( + device: device, + subSpaces: subSpaces.cast(), + deviceInfo: deviceInfo, + ), + const SizedBox(height: 32), + RemoveDeviceWidget(bloc: _bloc), + ], + ), + ), + if (state is DeviceSettingsLoading) + Positioned.fill( + child: Container( + color: Colors.black.withOpacity(0.1), + child: const Center( + child: CircularProgressIndicator( + color: ColorsManager.primaryColor, + ), + ), + ), + ), + ], + ); + }, + ); + }, + ), + ); + } +} diff --git a/lib/pages/device_managment/device_setting/remove_device_widget.dart b/lib/pages/device_managment/device_setting/remove_device_widget.dart new file mode 100644 index 00000000..e65ee125 --- /dev/null +++ b/lib/pages/device_managment/device_setting/remove_device_widget.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:syncrow_web/web_layout/default_container.dart'; + +class RemoveDeviceWidget extends StatelessWidget { + const RemoveDeviceWidget({ + super.key, + required SettingDeviceBloc bloc, + }) : _bloc = bloc; + + final SettingDeviceBloc _bloc; + + @override + Widget build(BuildContext context) { + return SizedBox( + width: double.infinity, + child: InkWell( + onTap: () { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text( + 'Remove Device', + style: context.textTheme.bodyMedium!.copyWith( + fontWeight: FontWeight.w700, + color: ColorsManager.red, + ), + ), + content: Text( + 'Are you sure you want to remove this device?', + style: context.textTheme.bodyMedium!.copyWith( + color: ColorsManager.grayColor, + ), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text( + 'Cancel', + style: context.textTheme.bodyMedium!.copyWith( + color: ColorsManager.grayColor, + ), + ), + ), + TextButton( + onPressed: () { + _bloc.add(SettingBlocDeleteDevice()); + Navigator.of(context).pop(); + }, + child: Text( + 'Remove', + style: context.textTheme.bodyMedium!.copyWith( + color: ColorsManager.red, + ), + ), + ), + ], + ); + }, + ); + }, + child: DefaultContainer( + padding: const EdgeInsets.all(25), + child: Center( + child: Text( + 'Remove Device', + style: context.textTheme.bodyMedium!.copyWith( + fontSize: 14, + color: ColorsManager.red, + fontWeight: FontWeight.w700), + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/device_managment/device_setting/settings_model/device_info_model.dart b/lib/pages/device_managment/device_setting/settings_model/device_info_model.dart new file mode 100644 index 00000000..ce9b6750 --- /dev/null +++ b/lib/pages/device_managment/device_setting/settings_model/device_info_model.dart @@ -0,0 +1,183 @@ +class DeviceInfoModel { + final int activeTime; + final String category; + final String categoryName; + final int createTime; + final String gatewayId; + final String icon; + final String ip; + final String lat; + final String localKey; + final String lon; + final String model; + final String name; + final String nodeId; + final bool online; + final String ownerId; + final String productName; + final bool sub; + final String timeZone; + final int updateTime; + final String uuid; + final String productUuid; + final String productType; + final String permissionType; + final String macAddress; + final Subspace subspace; + + DeviceInfoModel({ + required this.activeTime, + required this.category, + required this.categoryName, + required this.createTime, + required this.gatewayId, + required this.icon, + required this.ip, + required this.lat, + required this.localKey, + required this.lon, + required this.model, + required this.name, + required this.nodeId, + required this.online, + required this.ownerId, + required this.productName, + required this.sub, + required this.timeZone, + required this.updateTime, + required this.uuid, + required this.productUuid, + required this.productType, + required this.permissionType, + required this.macAddress, + required this.subspace, + }); + + factory DeviceInfoModel.fromJson(Map json) { + return DeviceInfoModel( + activeTime: json['activeTime'] as int? ?? 0, + category: json['category'] ?? '', + categoryName: json['categoryName'] as String? ?? '', + createTime: json['createTime'] as int? ?? 0, + gatewayId: json['gatewayId'] as String? ?? '', + icon: json['icon'] as String? ?? '', + ip: json['ip'] as String? ?? '', + lat: json['lat'] as String? ?? '', + localKey: json['localKey'] as String? ?? '', + lon: json['lon'] as String? ?? '', + model: json['model'] as String? ?? '', + name: json['name'] as String? ?? '', + nodeId: json['nodeId'] as String? ?? '', + online: json['online'] as bool? ?? false, + ownerId: json['ownerId'] as String? ?? '', + productName: json['productName'] as String? ?? '', + sub: json['sub'] as bool? ?? false, + timeZone: json['timeZone'] as String? ?? '', + updateTime: json['updateTime'] as int? ?? 0, + uuid: json['uuid'] as String? ?? '', + productUuid: json['productUuid'] as String? ?? '', + productType: json['productType'] as String? ?? '', + permissionType: json['permissionType'] as String? ?? '', + macAddress: json['macAddress'] as String? ?? '', + subspace: + Subspace.fromJson(json['subspace'] as Map? ?? {}), + ); + } + + Map toJson() { + return { + 'activeTime': activeTime, + 'category': category, + 'categoryName': categoryName, + 'createTime': createTime, + 'gatewayId': gatewayId, + 'icon': icon, + 'ip': ip, + 'lat': lat, + 'localKey': localKey, + 'lon': lon, + 'model': model, + 'name': name, + 'nodeId': nodeId, + 'online': online, + 'ownerId': ownerId, + 'productName': productName, + 'sub': sub, + 'timeZone': timeZone, + 'updateTime': updateTime, + 'uuid': uuid, + 'productUuid': productUuid, + 'productType': productType, + 'permissionType': permissionType, + 'macAddress': macAddress, + 'subspace': subspace.toJson(), + }; + } + + static DeviceInfoModel empty() { + return DeviceInfoModel( + activeTime: 0, + category: '', + categoryName: '', + createTime: 0, + gatewayId: '', + icon: '', + ip: '', + lat: '', + localKey: '', + lon: '', + model: '', + name: '', + nodeId: '', + online: false, + ownerId: '', + productName: '', + sub: false, + timeZone: '', + updateTime: 0, + uuid: '', + productUuid: '', + productType: '', + permissionType: '', + macAddress: '', + subspace: Subspace( + uuid: '', + createdAt: '', + updatedAt: '', + subspaceName: '', + ), + ); + } +} + +class Subspace { + final String uuid; + final String createdAt; + final String updatedAt; + final String subspaceName; + + Subspace({ + required this.uuid, + required this.createdAt, + required this.updatedAt, + required this.subspaceName, + }); + + factory Subspace.fromJson(Map json) { + return Subspace( + uuid: json['uuid'] as String? ?? '', + createdAt: json['createdAt'] as String? ?? '', + updatedAt: json['updatedAt'] as String? ?? '', + subspaceName: json['subspaceName'] as String? ?? '', + ); + } + + Map toJson() { + return { + 'uuid': uuid, + 'createdAt': createdAt, + 'updatedAt': updatedAt, + 'subspaceName': subspaceName, + }; + } +} diff --git a/lib/pages/device_managment/device_setting/settings_model/sub_space_model.dart b/lib/pages/device_managment/device_setting/settings_model/sub_space_model.dart new file mode 100644 index 00000000..9d3f4036 --- /dev/null +++ b/lib/pages/device_managment/device_setting/settings_model/sub_space_model.dart @@ -0,0 +1,35 @@ +import 'package:syncrow_web/pages/visitor_password/model/device_model.dart'; + +class SubSpaceModel { + final String? id; + final String? name; + List? devices; + + SubSpaceModel({ + required this.id, + required this.name, + required this.devices, + }); + + Map toJson() { + return { + 'id': id, + 'name': name, + 'devices': devices?.map((device) => device.toJson()).toList(), + }; + } + + factory SubSpaceModel.fromJson(Map json) { + List devices = []; + if (json['devices'] != null) { + for (var device in json['devices']) { + devices.add(DeviceModel.fromJson(device)); + } + } + return SubSpaceModel( + id: json['uuid'] as String? ?? '', + name: json['subspaceName'] as String? ?? '', + devices: devices.isNotEmpty ? devices : null as List?, + ); + } +} diff --git a/lib/pages/device_managment/device_setting/sub_space_dialog.dart b/lib/pages/device_managment/device_setting/sub_space_dialog.dart new file mode 100644 index 00000000..28350d4d --- /dev/null +++ b/lib/pages/device_managment/device_setting/sub_space_dialog.dart @@ -0,0 +1,115 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/sub_space_model.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/subspace_dialog_buttons.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class SubSpaceDialog extends StatefulWidget { + final List subSpaces; + final String? selected; + final void Function(SubSpaceModel?) onConfirmed; + + const SubSpaceDialog({ + Key? key, + required this.subSpaces, + this.selected, + required this.onConfirmed, + }) : super(key: key); + + @override + State createState() => _SubSpaceDialogState(); +} + +class _SubSpaceDialogState extends State { + String? _selectedId; + + @override + void initState() { + super.initState(); + _selectedId = widget.selected; + } + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: ColorsManager.whiteColors, + insetPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 60), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(28), + ), + child: Container( + width: MediaQuery.of(context).size.width * 0.35, + padding: const EdgeInsets.fromLTRB(0, 24, 0, 0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Sub-Space', + style: context.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w700, + color: ColorsManager.blueColor, + fontSize: 20), + ), + const Divider(), + const SizedBox(height: 10), + ...widget.subSpaces.map((space) { + return RadioListTile( + value: space.id!, + groupValue: _selectedId, + onChanged: (value) { + setState(() { + _selectedId = value; + }); + }, + activeColor: Color(0xFF2962FF), + title: Text( + space.name ?? 'Unnamed Sub-Space', + style: context.textTheme.bodyMedium?.copyWith( + fontSize: 15, + color: ColorsManager.grayColor, + fontWeight: FontWeight.w400, + ), + ), + controlAffinity: ListTileControlAffinity.trailing, + contentPadding: const EdgeInsets.symmetric(horizontal: 24), + ); + }).toList(), + const SizedBox(height: 12), + const Divider(height: 1, thickness: 1), + SubSpaceDialogButtons(selectedId: _selectedId, widget: widget), + ], + ), + ), + ); + } +} + +void showSubSpaceDialog( + BuildContext context, { + required List subSpaces, + String? selected, + required String communityUuid, + required String spaceUuid, +}) { + showDialog( + context: context, + barrierDismissible: true, + builder: (ctx) => SubSpaceDialog( + subSpaces: subSpaces, + selected: selected, + onConfirmed: (selectedModel) { + if (selectedModel != null) { + context.read().add( + SettingBlocAssignRoom( + communityUuid: communityUuid, + spaceUuid: spaceUuid, + subSpaceUuid: selectedModel.id ?? '', + ), + ); + } + }, + ), + ); +} diff --git a/lib/pages/device_managment/device_setting/subspace_dialog_buttons.dart b/lib/pages/device_managment/device_setting/subspace_dialog_buttons.dart new file mode 100644 index 00000000..80ece0cb --- /dev/null +++ b/lib/pages/device_managment/device_setting/subspace_dialog_buttons.dart @@ -0,0 +1,114 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/sub_space_model.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/sub_space_dialog.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class SubSpaceDialogButtons extends StatelessWidget { + const SubSpaceDialogButtons({ + super.key, + required String? selectedId, + required this.widget, + }) : _selectedId = selectedId; + + final String? _selectedId; + final SubSpaceDialog widget; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 50, + child: Row( + children: [ + Expanded( + child: Container( + decoration: const BoxDecoration( + border: Border( + right: BorderSide( + color: ColorsManager.dividerColor, + width: 0.5, + ), + ), + ), + child: TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text( + 'Cancel', + style: context.textTheme.bodyMedium?.copyWith( + color: ColorsManager.textGray, + fontSize: 18, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ), + Expanded( + child: Container( + decoration: const BoxDecoration( + border: Border( + left: BorderSide( + color: ColorsManager.dividerColor, + width: 0.5, + ), + ), + ), + child: TextButton( + onPressed: _selectedId == null + ? null + : () { + final selectedModel = widget.subSpaces.firstWhere( + (space) => space.id == _selectedId, + orElse: () => + SubSpaceModel(id: null, name: '', devices: [])); + widget.onConfirmed(selectedModel); + Navigator.of(context).pop(); + }, + child: Text( + 'Confirm', + style: context.textTheme.bodyMedium?.copyWith( + color: ColorsManager.secondaryColor, + fontSize: 14, + fontWeight: FontWeight.w400, + ), + ), + ), + ), + ), + ], + ), + ); + } +} + +void showSubSpaceDialog( + BuildContext context, { + required List subSpaces, + String? selected, + required String communityUuid, + required String spaceUuid, +}) { + showDialog( + context: context, + barrierDismissible: true, + builder: (ctx) => SubSpaceDialog( + subSpaces: subSpaces, + selected: selected, + onConfirmed: (selectedModel) { + if (selectedModel != null) { + context.read().add( + SettingBlocAssignRoom( + communityUuid: communityUuid, + spaceUuid: spaceUuid, + subSpaceUuid: selectedModel.id ?? '', + ), + ); + } + }, + ), + ); +} diff --git a/lib/pages/device_managment/factories/device_bloc_dependencies_factory.dart b/lib/pages/device_managment/factories/device_bloc_dependencies_factory.dart new file mode 100644 index 00000000..1c75c38b --- /dev/null +++ b/lib/pages/device_managment/factories/device_bloc_dependencies_factory.dart @@ -0,0 +1,18 @@ +import 'package:syncrow_web/services/batch_control_devices_service.dart'; +import 'package:syncrow_web/services/control_device_service.dart'; + +abstract final class DeviceBlocDependenciesFactory { + const DeviceBlocDependenciesFactory._(); + + static ControlDeviceService createControlDeviceService() { + return DebouncedControlDeviceService( + decoratee: RemoteControlDeviceService(), + ); + } + + static BatchControlDevicesService createBatchControlDevicesService() { + return DebouncedBatchControlDevicesService( + decoratee: RemoteBatchControlDevicesService(), + ); + } +} diff --git a/lib/pages/device_managment/flush_mounted_presence_sensor/factories/flush_mounted_presence_sensor_bloc_factory.dart b/lib/pages/device_managment/flush_mounted_presence_sensor/factories/flush_mounted_presence_sensor_bloc_factory.dart index 49fb517f..e842f36b 100644 --- a/lib/pages/device_managment/flush_mounted_presence_sensor/factories/flush_mounted_presence_sensor_bloc_factory.dart +++ b/lib/pages/device_managment/flush_mounted_presence_sensor/factories/flush_mounted_presence_sensor_bloc_factory.dart @@ -1,6 +1,5 @@ +import 'package:syncrow_web/pages/device_managment/factories/device_bloc_dependencies_factory.dart'; import 'package:syncrow_web/pages/device_managment/flush_mounted_presence_sensor/bloc/flush_mounted_presence_sensor_bloc.dart'; -import 'package:syncrow_web/services/batch_control_devices_service.dart'; -import 'package:syncrow_web/services/control_device_service.dart'; abstract final class FlushMountedPresenceSensorBlocFactory { const FlushMountedPresenceSensorBlocFactory._(); @@ -10,12 +9,8 @@ abstract final class FlushMountedPresenceSensorBlocFactory { }) { return FlushMountedPresenceSensorBloc( deviceId: deviceId, - controlDeviceService: DebouncedControlDeviceService( - decoratee: RemoteControlDeviceService(), - ), - batchControlDevicesService: DebouncedBatchControlDevicesService( - decoratee: RemoteBatchControlDevicesService(), - ), + controlDeviceService: DeviceBlocDependenciesFactory.createControlDeviceService(), + batchControlDevicesService: DeviceBlocDependenciesFactory.createBatchControlDevicesService(), ); } } diff --git a/lib/pages/device_managment/one_g_glass_switch/bloc/one_gang_glass_switch_bloc.dart b/lib/pages/device_managment/one_g_glass_switch/bloc/one_gang_glass_switch_bloc.dart index 12aeaa88..c1e976ab 100644 --- a/lib/pages/device_managment/one_g_glass_switch/bloc/one_gang_glass_switch_bloc.dart +++ b/lib/pages/device_managment/one_g_glass_switch/bloc/one_gang_glass_switch_bloc.dart @@ -1,11 +1,13 @@ import 'dart:async'; -import 'package:bloc/bloc.dart'; import 'package:firebase_database/firebase_database.dart'; -import 'package:meta/meta.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/models/once_gang_glass_status_model.dart'; +import 'package:syncrow_web/services/batch_control_devices_service.dart'; +import 'package:syncrow_web/services/control_device_service.dart'; import 'package:syncrow_web/services/devices_mang_api.dart'; part 'one_gang_glass_switch_event.dart'; @@ -13,13 +15,16 @@ part 'one_gang_glass_switch_state.dart'; class OneGangGlassSwitchBloc extends Bloc { - OneGangGlassStatusModel deviceStatus; - Timer? _timer; + late OneGangGlassStatusModel deviceStatus; + final String deviceId; + final ControlDeviceService controlDeviceService; + final BatchControlDevicesService batchControlDevicesService; - OneGangGlassSwitchBloc({required String deviceId}) - : deviceStatus = OneGangGlassStatusModel( - uuid: deviceId, switch1: false, countDown: 0), - super(OneGangGlassSwitchInitial()) { + OneGangGlassSwitchBloc({ + required this.deviceId, + required this.controlDeviceService, + required this.batchControlDevicesService, + }) : super(OneGangGlassSwitchInitial()) { on(_onFetchDeviceStatus); on(_onControl); on(_onBatchControl); @@ -28,160 +33,140 @@ class OneGangGlassSwitchBloc on(_onStatusUpdated); } - Future _onFetchDeviceStatus(OneGangGlassSwitchFetchDeviceEvent event, - Emitter emit) async { + Future _onFetchDeviceStatus( + OneGangGlassSwitchFetchDeviceEvent event, + Emitter emit, + ) async { emit(OneGangGlassSwitchLoading()); try { - final status = - await DevicesManagementApi().getDeviceStatus(event.deviceId); - _listenToChanges(event.deviceId); - deviceStatus = - OneGangGlassStatusModel.fromJson(event.deviceId, status.status); + final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); + _listenToChanges(event.deviceId, emit); + deviceStatus = OneGangGlassStatusModel.fromJson(event.deviceId, status.status); emit(OneGangGlassSwitchStatusLoaded(deviceStatus)); } catch (e) { emit(OneGangGlassSwitchError(e.toString())); } } - _listenToChanges(deviceId) { + void _listenToChanges( + String deviceId, + Emitter emit, + ) { try { - DatabaseReference ref = - FirebaseDatabase.instance.ref('device-status/$deviceId'); - Stream stream = ref.onValue; + final ref = FirebaseDatabase.instance.ref('device-status/$deviceId'); + final stream = ref.onValue; stream.listen((DatabaseEvent event) { - Map usersMap = - event.snapshot.value as Map; + final data = event.snapshot.value as Map?; + if (data == null) return; - List statusList = []; - usersMap['status'].forEach((element) { - statusList - .add(Status(code: element['code'], value: element['value'])); - }); - - deviceStatus = OneGangGlassStatusModel.fromJson( - usersMap['productUuid'], statusList); - if (!isClosed) { - add(StatusUpdated(deviceStatus)); + final statusList = []; + if (data['status'] != null) { + for (var element in data['status']) { + statusList.add( + Status( + code: element['code'].toString(), + value: element['value'].toString(), + ), + ); + } + } + if (statusList.isNotEmpty) { + final newStatus = OneGangGlassStatusModel.fromJson(deviceId, statusList); + if (newStatus != deviceStatus) { + deviceStatus = newStatus; + if (!isClosed) { + add(StatusUpdated(deviceStatus)); + } + } } }); - } catch (_) {} + } catch (e) { + emit(OneGangGlassSwitchError('Failed to listen to changes: $e')); + } } void _onStatusUpdated( - StatusUpdated event, Emitter emit) { + StatusUpdated event, + Emitter emit, + ) { + emit(OneGangGlassSwitchLoading()); deviceStatus = event.deviceStatus; emit(OneGangGlassSwitchStatusLoaded(deviceStatus)); } - Future _onControl(OneGangGlassSwitchControl event, - Emitter emit) async { - final oldValue = _getValueByCode(event.code); - + Future _onControl( + OneGangGlassSwitchControl event, + Emitter emit, + ) async { + emit(OneGangGlassSwitchLoading()); _updateLocalValue(event.code, event.value); emit(OneGangGlassSwitchStatusLoaded(deviceStatus)); - await _runDebounce( - deviceId: event.deviceId, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - isBatch: false, - ); - } - - Future _onFactoryReset(OneGangGlassFactoryResetEvent event, - Emitter emit) async { - emit(OneGangGlassSwitchLoading()); try { - final response = await DevicesManagementApi() - .factoryReset(event.factoryReset, event.deviceId); - if (!response) { - emit(OneGangGlassSwitchError('Failed to reset device')); - } else { - emit(OneGangGlassSwitchStatusLoaded(deviceStatus)); - } + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status(code: event.code, value: event.value), + ); } catch (e) { + _updateLocalValue(event.code, !event.value); emit(OneGangGlassSwitchError(e.toString())); } } - Future _onBatchControl(OneGangGlassSwitchBatchControl event, - Emitter emit) async { - final oldValue = _getValueByCode(event.code); - + Future _onBatchControl( + OneGangGlassSwitchBatchControl event, + Emitter emit, + ) async { + emit(OneGangGlassSwitchLoading()); _updateLocalValue(event.code, event.value); emit(OneGangGlassSwitchStatusLoaded(deviceStatus)); - await _runDebounce( - deviceId: event.deviceIds, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - isBatch: true, - ); + try { + await batchControlDevicesService.batchControlDevices( + uuids: event.deviceIds, + code: event.code, + value: event.value, + ); + } catch (e) { + _updateLocalValue(event.code, !event.value); + emit(OneGangGlassSwitchError(e.toString())); + } } Future _onFetchBatchStatus( - OneGangGlassSwitchFetchBatchStatusEvent event, - Emitter emit) async { + OneGangGlassSwitchFetchBatchStatusEvent event, + Emitter emit, + ) async { emit(OneGangGlassSwitchLoading()); try { - final status = - await DevicesManagementApi().getBatchStatus(event.deviceIds); - deviceStatus = OneGangGlassStatusModel.fromJson( - event.deviceIds.first, status.status); + final status = await DevicesManagementApi().getBatchStatus(event.deviceIds); + deviceStatus = + OneGangGlassStatusModel.fromJson(event.deviceIds.first, status.status); emit(OneGangGlassSwitchStatusLoaded(deviceStatus)); } catch (e) { emit(OneGangGlassSwitchError(e.toString())); } } - Future _runDebounce({ - required dynamic deviceId, - required String code, - required bool value, - required bool oldValue, - required Emitter emit, - required bool isBatch, - }) async { - late String id; - if (deviceId is List) { - id = deviceId.first; - } else { - id = deviceId; - } - - if (_timer != null) { - _timer!.cancel(); - } - - _timer = Timer(const Duration(milliseconds: 500), () async { - try { - late bool response; - if (isBatch) { - response = await DevicesManagementApi() - .deviceBatchControl(deviceId, code, value); - } else { - response = await DevicesManagementApi() - .deviceControl(deviceId, Status(code: code, value: value)); - } - - if (!response) { - _revertValueAndEmit(id, code, oldValue, emit); - } - } catch (e) { - _revertValueAndEmit(id, code, oldValue, emit); + Future _onFactoryReset( + OneGangGlassFactoryResetEvent event, + Emitter emit, + ) async { + emit(OneGangGlassSwitchLoading()); + try { + final response = await DevicesManagementApi().factoryReset( + event.factoryReset, + event.deviceId, + ); + if (!response) { + emit(OneGangGlassSwitchError('Failed to reset device')); + } else { + add(OneGangGlassSwitchFetchDeviceEvent(event.deviceId)); } - }); - } - - void _revertValueAndEmit(String deviceId, String code, bool oldValue, - Emitter emit) { - _updateLocalValue(code, oldValue); - emit(OneGangGlassSwitchStatusLoaded(deviceStatus)); + } catch (e) { + emit(OneGangGlassSwitchError(e.toString())); + } } void _updateLocalValue(String code, bool value) { @@ -189,19 +174,4 @@ class OneGangGlassSwitchBloc deviceStatus = deviceStatus.copyWith(switch1: value); } } - - bool _getValueByCode(String code) { - switch (code) { - case 'switch_1': - return deviceStatus.switch1; - default: - return false; - } - } - - @override - Future close() { - _timer?.cancel(); - return super.close(); - } } diff --git a/lib/pages/device_managment/one_g_glass_switch/factories/one_gang_glass_switch_bloc_factory.dart b/lib/pages/device_managment/one_g_glass_switch/factories/one_gang_glass_switch_bloc_factory.dart new file mode 100644 index 00000000..97bcab81 --- /dev/null +++ b/lib/pages/device_managment/one_g_glass_switch/factories/one_gang_glass_switch_bloc_factory.dart @@ -0,0 +1,18 @@ +import 'package:syncrow_web/pages/device_managment/factories/device_bloc_dependencies_factory.dart'; +import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/bloc/one_gang_glass_switch_bloc.dart'; + +abstract final class OneGangGlassSwitchBlocFactory { + const OneGangGlassSwitchBlocFactory._(); + + static OneGangGlassSwitchBloc create({ + required String deviceId, + }) { + return OneGangGlassSwitchBloc( + deviceId: deviceId, + controlDeviceService: + DeviceBlocDependenciesFactory.createControlDeviceService(), + batchControlDevicesService: + DeviceBlocDependenciesFactory.createBatchControlDevicesService(), + ); + } +} diff --git a/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_batch_control_view.dart b/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_batch_control_view.dart index 9b89e876..307e61da 100644 --- a/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_batch_control_view.dart +++ b/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_batch_control_view.dart @@ -2,9 +2,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/bloc/one_gang_glass_switch_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/factories/one_gang_glass_switch_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/models/once_gang_glass_status_model.dart'; import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; -// import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; @@ -16,7 +16,7 @@ class OneGangGlassSwitchBatchControlView extends StatelessWidget with HelperResp @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => OneGangGlassSwitchBloc(deviceId: deviceIds.first) + create: (context) => OneGangGlassSwitchBlocFactory.create(deviceId: deviceIds.first) ..add(OneGangGlassSwitchFetchBatchStatusEvent(deviceIds)), child: BlocBuilder( builder: (context, state) { diff --git a/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart b/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart index 8914b786..997be513 100644 --- a/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart +++ b/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/bloc/one_gang_glass_switch_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/factories/one_gang_glass_switch_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/models/once_gang_glass_status_model.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; @@ -9,13 +10,13 @@ import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_la class OneGangGlassSwitchControlView extends StatelessWidget with HelperResponsiveLayout { final String deviceId; - const OneGangGlassSwitchControlView({required this.deviceId, Key? key}) : super(key: key); + const OneGangGlassSwitchControlView({required this.deviceId, super.key}); @override Widget build(BuildContext context) { return BlocProvider( create: (context) => - OneGangGlassSwitchBloc(deviceId: deviceId)..add(OneGangGlassSwitchFetchDeviceEvent(deviceId)), + OneGangGlassSwitchBlocFactory.create(deviceId: deviceId)..add(OneGangGlassSwitchFetchDeviceEvent(deviceId)), child: BlocBuilder( builder: (context, state) { if (state is OneGangGlassSwitchLoading) { diff --git a/lib/pages/device_managment/one_gang_switch/bloc/wall_light_switch_bloc.dart b/lib/pages/device_managment/one_gang_switch/bloc/wall_light_switch_bloc.dart index c2038330..59eccfe9 100644 --- a/lib/pages/device_managment/one_gang_switch/bloc/wall_light_switch_bloc.dart +++ b/lib/pages/device_managment/one_gang_switch/bloc/wall_light_switch_bloc.dart @@ -6,12 +6,21 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/device_sta import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_event.dart'; import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_state.dart'; import 'package:syncrow_web/pages/device_managment/one_gang_switch/models/wall_light_status_model.dart'; +import 'package:syncrow_web/services/batch_control_devices_service.dart'; +import 'package:syncrow_web/services/control_device_service.dart'; import 'package:syncrow_web/services/devices_mang_api.dart'; -class WallLightSwitchBloc - extends Bloc { - WallLightSwitchBloc({required this.deviceId}) - : super(WallLightSwitchInitial()) { +class WallLightSwitchBloc extends Bloc { + late WallLightStatusModel deviceStatus; + final String deviceId; + final ControlDeviceService controlDeviceService; + final BatchControlDevicesService batchControlDevicesService; + + WallLightSwitchBloc({ + required this.deviceId, + required this.controlDeviceService, + required this.batchControlDevicesService, + }) : super(WallLightSwitchInitial()) { on(_onFetchDeviceStatus); on(_onControl); on(_onFetchBatchStatus); @@ -20,143 +29,114 @@ class WallLightSwitchBloc on(_onStatusUpdated); } - late WallLightStatusModel deviceStatus; - final String deviceId; - Timer? _timer; - - FutureOr _onFetchDeviceStatus(WallLightSwitchFetchDeviceEvent event, - Emitter emit) async { + Future _onFetchDeviceStatus( + WallLightSwitchFetchDeviceEvent event, + Emitter emit, + ) async { emit(WallLightSwitchLoading()); try { - final status = - await DevicesManagementApi().getDeviceStatus(event.deviceId); - - deviceStatus = - WallLightStatusModel.fromJson(event.deviceId, status.status); - _listenToChanges(event.deviceId); + final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); + _listenToChanges(event.deviceId, emit); + deviceStatus = WallLightStatusModel.fromJson(event.deviceId, status.status); emit(WallLightSwitchStatusLoaded(deviceStatus)); } catch (e) { emit(WallLightSwitchError(e.toString())); } } - _listenToChanges(deviceId) { + void _listenToChanges( + String deviceId, + Emitter emit, + ) { try { - DatabaseReference ref = - FirebaseDatabase.instance.ref('device-status/$deviceId'); - Stream stream = ref.onValue; + final ref = FirebaseDatabase.instance.ref('device-status/$deviceId'); + final stream = ref.onValue; stream.listen((DatabaseEvent event) { - Map usersMap = - event.snapshot.value as Map; + final data = event.snapshot.value as Map?; + if (data == null) return; - List statusList = []; - usersMap['status'].forEach((element) { - statusList - .add(Status(code: element['code'], value: element['value'])); - }); - - deviceStatus = - WallLightStatusModel.fromJson(usersMap['productUuid'], statusList); - if (!isClosed) { - add(StatusUpdated(deviceStatus)); + final statusList = []; + if (data['status'] != null) { + for (var element in data['status']) { + statusList.add( + Status( + code: element['code'].toString(), + value: element['value'].toString(), + ), + ); + } + } + if (statusList.isNotEmpty) { + final newStatus = WallLightStatusModel.fromJson(deviceId, statusList); + if (newStatus != deviceStatus) { + deviceStatus = newStatus; + if (!isClosed) { + add(StatusUpdated(deviceStatus)); + } + } } }); - } catch (_) {} + } catch (e) { + emit(WallLightSwitchError('Failed to listen to changes: $e')); + } } void _onStatusUpdated( - StatusUpdated event, Emitter emit) { + StatusUpdated event, + Emitter emit, + ) { + emit(WallLightSwitchLoading()); deviceStatus = event.deviceStatus; emit(WallLightSwitchStatusLoaded(deviceStatus)); } - FutureOr _onControl( - WallLightSwitchControl event, Emitter emit) async { - final oldValue = _getValueByCode(event.code); - + Future _onControl( + WallLightSwitchControl event, + Emitter emit, + ) async { + emit(WallLightSwitchLoading()); _updateLocalValue(event.code, event.value); - emit(WallLightSwitchStatusLoaded(deviceStatus)); - await _runDebounce( - deviceId: event.deviceId, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - isBatch: false, - ); + try { + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status(code: event.code, value: event.value), + ); + } catch (e) { + _updateLocalValue(event.code, !event.value); + emit(WallLightSwitchError(e.toString())); + } } - Future _runDebounce({ - required dynamic deviceId, - required String code, - required bool value, - required bool oldValue, - required Emitter emit, - required bool isBatch, - }) async { - late String id; - - if (deviceId is List) { - id = deviceId.first; - } else { - id = deviceId; - } - - if (_timer != null) { - _timer!.cancel(); - } - - _timer = Timer(const Duration(milliseconds: 500), () async { - try { - late bool response; - - if (isBatch) { - response = await DevicesManagementApi() - .deviceBatchControl(deviceId, code, value); - } else { - response = await DevicesManagementApi() - .deviceControl(deviceId, Status(code: code, value: value)); - } - - if (!response) { - _revertValueAndEmit(id, code, oldValue, emit); - } - } catch (e) { - _revertValueAndEmit(id, code, oldValue, emit); - } - }); - } - - void _revertValueAndEmit(String deviceId, String code, bool oldValue, - Emitter emit) { - _updateLocalValue(code, oldValue); + Future _onBatchControl( + WallLightSwitchBatchControl event, + Emitter emit, + ) async { + emit(WallLightSwitchLoading()); + _updateLocalValue(event.code, event.value); emit(WallLightSwitchStatusLoaded(deviceStatus)); - } - void _updateLocalValue(String code, bool value) { - if (code == 'switch_1') { - deviceStatus = deviceStatus.copyWith(switch1: value); + try { + await batchControlDevicesService.batchControlDevices( + uuids: event.devicesIds, + code: event.code, + value: event.value, + ); + } catch (e) { + _updateLocalValue(event.code, !event.value); + emit(WallLightSwitchError(e.toString())); } } - bool _getValueByCode(String code) { - switch (code) { - case 'switch_1': - return deviceStatus.switch1; - default: - return false; - } - } - - Future _onFetchBatchStatus(WallLightSwitchFetchBatchEvent event, - Emitter emit) async { + Future _onFetchBatchStatus( + WallLightSwitchFetchBatchEvent event, + Emitter emit, + ) async { emit(WallLightSwitchLoading()); try { - final status = - await DevicesManagementApi().getBatchStatus(event.devicesIds); + final status = await DevicesManagementApi().getBatchStatus(event.devicesIds); deviceStatus = WallLightStatusModel.fromJson(event.devicesIds.first, status.status); emit(WallLightSwitchStatusLoaded(deviceStatus)); @@ -165,32 +145,10 @@ class WallLightSwitchBloc } } - @override - Future close() { - _timer?.cancel(); - return super.close(); - } - - FutureOr _onBatchControl(WallLightSwitchBatchControl event, - Emitter emit) async { - final oldValue = _getValueByCode(event.code); - - _updateLocalValue(event.code, event.value); - - emit(WallLightSwitchStatusLoaded(deviceStatus)); - - await _runDebounce( - deviceId: event.devicesIds, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - isBatch: true, - ); - } - - FutureOr _onFactoryReset( - WallLightFactoryReset event, Emitter emit) async { + Future _onFactoryReset( + WallLightFactoryReset event, + Emitter emit, + ) async { emit(WallLightSwitchLoading()); try { final response = await DevicesManagementApi().factoryReset( @@ -198,12 +156,18 @@ class WallLightSwitchBloc event.deviceId, ); if (!response) { - emit(WallLightSwitchError('Failed')); + emit(WallLightSwitchError('Failed to reset device')); } else { - emit(WallLightSwitchStatusLoaded(deviceStatus)); + add(WallLightSwitchFetchDeviceEvent(event.deviceId)); } } catch (e) { emit(WallLightSwitchError(e.toString())); } } + + void _updateLocalValue(String code, bool value) { + if (code == 'switch_1') { + deviceStatus = deviceStatus.copyWith(switch1: value); + } + } } diff --git a/lib/pages/device_managment/one_gang_switch/factories/wall_light_switch_bloc_factory.dart b/lib/pages/device_managment/one_gang_switch/factories/wall_light_switch_bloc_factory.dart new file mode 100644 index 00000000..fbbe13dc --- /dev/null +++ b/lib/pages/device_managment/one_gang_switch/factories/wall_light_switch_bloc_factory.dart @@ -0,0 +1,18 @@ +import 'package:syncrow_web/pages/device_managment/factories/device_bloc_dependencies_factory.dart'; +import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_bloc.dart'; + +abstract final class WallLightSwitchBlocFactory { + const WallLightSwitchBlocFactory._(); + + static WallLightSwitchBloc create({ + required String deviceId, + }) { + return WallLightSwitchBloc( + deviceId: deviceId, + controlDeviceService: + DeviceBlocDependenciesFactory.createControlDeviceService(), + batchControlDevicesService: + DeviceBlocDependenciesFactory.createBatchControlDevicesService(), + ); + } +} diff --git a/lib/pages/device_managment/one_gang_switch/view/wall_light_batch_control.dart b/lib/pages/device_managment/one_gang_switch/view/wall_light_batch_control.dart index 7094b506..7fe57429 100644 --- a/lib/pages/device_managment/one_gang_switch/view/wall_light_batch_control.dart +++ b/lib/pages/device_managment/one_gang_switch/view/wall_light_batch_control.dart @@ -4,9 +4,9 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_re import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_bloc.dart'; import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_event.dart'; import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_state.dart'; +import 'package:syncrow_web/pages/device_managment/one_gang_switch/factories/wall_light_switch_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/one_gang_switch/models/wall_light_status_model.dart'; import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; -// import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; @@ -18,7 +18,7 @@ class WallLightBatchControlView extends StatelessWidget with HelperResponsiveLay @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => WallLightSwitchBloc(deviceId: deviceIds.first) + create: (context) => WallLightSwitchBlocFactory.create(deviceId: deviceIds.first) ..add(WallLightSwitchFetchBatchEvent(deviceIds)), child: BlocBuilder( builder: (context, state) { diff --git a/lib/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart b/lib/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart index a9e6ebbb..f1861c55 100644 --- a/lib/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart +++ b/lib/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart @@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_bloc.dart'; import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_event.dart'; import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_state.dart'; +import 'package:syncrow_web/pages/device_managment/one_gang_switch/factories/wall_light_switch_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/one_gang_switch/models/wall_light_status_model.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; @@ -15,7 +16,7 @@ class WallLightDeviceControl extends StatelessWidget @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => WallLightSwitchBloc(deviceId: deviceId) + create: (context) => WallLightSwitchBlocFactory.create(deviceId: deviceId) ..add(WallLightSwitchFetchDeviceEvent(deviceId)), child: BlocBuilder( builder: (context, state) { diff --git a/lib/pages/device_managment/shared/device_control_dialog.dart b/lib/pages/device_managment/shared/device_control_dialog.dart index c9cd4648..beb3b52c 100644 --- a/lib/pages/device_managment/shared/device_control_dialog.dart +++ b/lib/pages/device_managment/shared/device_control_dialog.dart @@ -157,7 +157,7 @@ class DeviceControlDialog extends StatelessWidget with RouteControlsBasedCode { ), ), const SizedBox(width: 10), - Text( + SelectableText( value, style: TextStyle( fontSize: 16, diff --git a/lib/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_bloc.dart b/lib/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_bloc.dart index 174cd167..766c3163 100644 --- a/lib/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_bloc.dart +++ b/lib/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_bloc.dart @@ -1,11 +1,14 @@ import 'dart:async'; -import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; import 'package:firebase_database/firebase_database.dart'; -import 'package:meta/meta.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; import 'package:syncrow_web/pages/device_managment/three_g_glass_switch/models/three_gang_glass_switch.dart'; +import 'package:syncrow_web/services/batch_control_devices_service.dart'; +import 'package:syncrow_web/services/control_device_service.dart'; import 'package:syncrow_web/services/devices_mang_api.dart'; part 'three_gang_glass_switch_event.dart'; @@ -13,19 +16,16 @@ part 'three_gang_glass_switch_state.dart'; class ThreeGangGlassSwitchBloc extends Bloc { - ThreeGangGlassStatusModel deviceStatus; - Timer? _timer; + late ThreeGangGlassStatusModel deviceStatus; + final String deviceId; + final ControlDeviceService controlDeviceService; + final BatchControlDevicesService batchControlDevicesService; - ThreeGangGlassSwitchBloc({required String deviceId}) - : deviceStatus = ThreeGangGlassStatusModel( - uuid: deviceId, - switch1: false, - countDown1: 0, - switch2: false, - countDown2: 0, - switch3: false, - countDown3: 0), - super(ThreeGangGlassSwitchInitial()) { + ThreeGangGlassSwitchBloc({ + required this.deviceId, + required this.controlDeviceService, + required this.batchControlDevicesService, + }) : super(ThreeGangGlassSwitchInitial()) { on(_onFetchDeviceStatus); on(_onControl); on(_onBatchControl); @@ -34,188 +34,154 @@ class ThreeGangGlassSwitchBloc on(_onStatusUpdated); } - Future _onFetchDeviceStatus(ThreeGangGlassSwitchFetchDeviceEvent event, - Emitter emit) async { + Future _onFetchDeviceStatus( + ThreeGangGlassSwitchFetchDeviceEvent event, + Emitter emit, + ) async { emit(ThreeGangGlassSwitchLoading()); try { - final status = - await DevicesManagementApi().getDeviceStatus(event.deviceId); + final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); + _listenToChanges(event.deviceId, emit); deviceStatus = ThreeGangGlassStatusModel.fromJson(event.deviceId, status.status); - _listenToChanges(event.deviceId); emit(ThreeGangGlassSwitchStatusLoaded(deviceStatus)); } catch (e) { emit(ThreeGangGlassSwitchError(e.toString())); } } - _listenToChanges(deviceId) { + void _listenToChanges( + String deviceId, + Emitter emit, + ) { try { - DatabaseReference ref = - FirebaseDatabase.instance.ref('device-status/$deviceId'); - Stream stream = ref.onValue; + final ref = FirebaseDatabase.instance.ref('device-status/$deviceId'); + final stream = ref.onValue; stream.listen((DatabaseEvent event) { - Map usersMap = - event.snapshot.value as Map; + final data = event.snapshot.value as Map?; + if (data == null) return; - List statusList = []; - usersMap['status'].forEach((element) { - statusList - .add(Status(code: element['code'], value: element['value'])); - }); - - deviceStatus = ThreeGangGlassStatusModel.fromJson( - usersMap['productUuid'], statusList); - if (!isClosed) { - add(StatusUpdated(deviceStatus)); + final statusList = []; + if (data['status'] != null) { + for (var element in data['status']) { + statusList.add( + Status( + code: element['code'].toString(), + value: element['value'].toString(), + ), + ); + } + } + if (statusList.isNotEmpty) { + final newStatus = ThreeGangGlassStatusModel.fromJson(deviceId, statusList); + if (newStatus != deviceStatus) { + deviceStatus = newStatus; + if (!isClosed) { + add(StatusUpdated(deviceStatus)); + } + } } }); - } catch (_) {} + } catch (e) { + emit(ThreeGangGlassSwitchError('Failed to listen to changes: $e')); + } } void _onStatusUpdated( - StatusUpdated event, Emitter emit) { + StatusUpdated event, + Emitter emit, + ) { + emit(ThreeGangGlassSwitchLoading()); deviceStatus = event.deviceStatus; emit(ThreeGangGlassSwitchStatusLoaded(deviceStatus)); } - Future _onControl(ThreeGangGlassSwitchControl event, - Emitter emit) async { - final oldValue = _getValueByCode(event.code); - + Future _onControl( + ThreeGangGlassSwitchControl event, + Emitter emit, + ) async { + emit(ThreeGangGlassSwitchLoading()); _updateLocalValue(event.code, event.value); emit(ThreeGangGlassSwitchStatusLoaded(deviceStatus)); - await _runDebounce( - deviceId: event.deviceId, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - isBatch: false, - ); + try { + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status(code: event.code, value: event.value), + ); + } catch (e) { + _updateLocalValue(event.code, !event.value); + emit(ThreeGangGlassSwitchError(e.toString())); + } } - Future _onBatchControl(ThreeGangGlassSwitchBatchControl event, - Emitter emit) async { - final oldValue = _getValueByCode(event.code); - + Future _onBatchControl( + ThreeGangGlassSwitchBatchControl event, + Emitter emit, + ) async { + emit(ThreeGangGlassSwitchLoading()); _updateLocalValue(event.code, event.value); emit(ThreeGangGlassSwitchBatchStatusLoaded(deviceStatus)); - await _runDebounce( - deviceId: event.deviceIds, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - isBatch: true, - ); + try { + await batchControlDevicesService.batchControlDevices( + uuids: event.deviceIds, + code: event.code, + value: event.value, + ); + } catch (e) { + _updateLocalValue(event.code, !event.value); + emit(ThreeGangGlassSwitchError(e.toString())); + } } Future _onFetchBatchStatus( - ThreeGangGlassSwitchFetchBatchStatusEvent event, - Emitter emit) async { + ThreeGangGlassSwitchFetchBatchStatusEvent event, + Emitter emit, + ) async { emit(ThreeGangGlassSwitchLoading()); try { - final status = - await DevicesManagementApi().getBatchStatus(event.deviceIds); - deviceStatus = ThreeGangGlassStatusModel.fromJson( - event.deviceIds.first, status.status); + final status = await DevicesManagementApi().getBatchStatus(event.deviceIds); + deviceStatus = + ThreeGangGlassStatusModel.fromJson(event.deviceIds.first, status.status); emit(ThreeGangGlassSwitchBatchStatusLoaded(deviceStatus)); } catch (e) { emit(ThreeGangGlassSwitchError(e.toString())); } } - Future _onFactoryReset(ThreeGangGlassFactoryReset event, - Emitter emit) async { + Future _onFactoryReset( + ThreeGangGlassFactoryReset event, + Emitter emit, + ) async { emit(ThreeGangGlassSwitchLoading()); try { - final response = await DevicesManagementApi() - .factoryReset(event.factoryReset, event.deviceId); + final response = await DevicesManagementApi().factoryReset( + event.factoryReset, + event.deviceId, + ); if (!response) { - emit(ThreeGangGlassSwitchError('Failed')); + emit(ThreeGangGlassSwitchError('Failed to reset device')); } else { - emit(ThreeGangGlassSwitchStatusLoaded(deviceStatus)); + add(ThreeGangGlassSwitchFetchDeviceEvent(event.deviceId)); } } catch (e) { emit(ThreeGangGlassSwitchError(e.toString())); } } - Future _runDebounce({ - required dynamic deviceId, - required String code, - required bool value, - required bool oldValue, - required Emitter emit, - required bool isBatch, - }) async { - late String id; - if (deviceId is List) { - id = deviceId.first; - } else { - id = deviceId; - } - - if (_timer != null) { - _timer!.cancel(); - } - - _timer = Timer(const Duration(milliseconds: 500), () async { - try { - late bool response; - if (isBatch) { - response = await DevicesManagementApi() - .deviceBatchControl(deviceId, code, value); - } else { - response = await DevicesManagementApi() - .deviceControl(deviceId, Status(code: code, value: value)); - } - - if (!response) { - _revertValueAndEmit(id, code, oldValue, emit); - } - } catch (e) { - _revertValueAndEmit(id, code, oldValue, emit); - } - }); - } - - void _revertValueAndEmit(String deviceId, String code, bool oldValue, - Emitter emit) { - _updateLocalValue(code, oldValue); - emit(ThreeGangGlassSwitchStatusLoaded(deviceStatus)); - } - void _updateLocalValue(String code, bool value) { - if (code == 'switch_1') { - deviceStatus = deviceStatus.copyWith(switch1: value); - } else if (code == 'switch_2') { - deviceStatus = deviceStatus.copyWith(switch2: value); - } else if (code == 'switch_3') { - deviceStatus = deviceStatus.copyWith(switch3: value); - } - } - - bool _getValueByCode(String code) { switch (code) { case 'switch_1': - return deviceStatus.switch1; + deviceStatus = deviceStatus.copyWith(switch1: value); + break; case 'switch_2': - return deviceStatus.switch2; + deviceStatus = deviceStatus.copyWith(switch2: value); + break; case 'switch_3': - return deviceStatus.switch3; - default: - return false; + deviceStatus = deviceStatus.copyWith(switch3: value); + break; } } - - @override - Future close() { - _timer?.cancel(); - return super.close(); - } } diff --git a/lib/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_event.dart b/lib/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_event.dart index 82b93fba..991de938 100644 --- a/lib/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_event.dart +++ b/lib/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_event.dart @@ -1,7 +1,10 @@ part of 'three_gang_glass_switch_bloc.dart'; @immutable -abstract class ThreeGangGlassSwitchEvent {} +abstract class ThreeGangGlassSwitchEvent extends Equatable { + @override + List get props => []; +} class ThreeGangGlassSwitchFetchDeviceEvent extends ThreeGangGlassSwitchEvent { final String deviceId; @@ -19,6 +22,9 @@ class ThreeGangGlassSwitchControl extends ThreeGangGlassSwitchEvent { required this.code, required this.value, }); + + @override + List get props => [deviceId, code, value]; } class ThreeGangGlassSwitchBatchControl extends ThreeGangGlassSwitchEvent { @@ -31,6 +37,9 @@ class ThreeGangGlassSwitchBatchControl extends ThreeGangGlassSwitchEvent { required this.code, required this.value, }); + + @override + List get props => [deviceIds, code, value]; } class ThreeGangGlassSwitchFetchBatchStatusEvent @@ -38,6 +47,9 @@ class ThreeGangGlassSwitchFetchBatchStatusEvent final List deviceIds; ThreeGangGlassSwitchFetchBatchStatusEvent(this.deviceIds); + + @override + List get props => [deviceIds]; } class ThreeGangGlassFactoryReset extends ThreeGangGlassSwitchEvent { @@ -48,6 +60,9 @@ class ThreeGangGlassFactoryReset extends ThreeGangGlassSwitchEvent { required this.deviceId, required this.factoryReset, }); + + @override + List get props => [deviceId, factoryReset]; } class StatusUpdated extends ThreeGangGlassSwitchEvent { diff --git a/lib/pages/device_managment/three_g_glass_switch/factories/three_gang_glass_switch_bloc_factory.dart b/lib/pages/device_managment/three_g_glass_switch/factories/three_gang_glass_switch_bloc_factory.dart new file mode 100644 index 00000000..9f66773a --- /dev/null +++ b/lib/pages/device_managment/three_g_glass_switch/factories/three_gang_glass_switch_bloc_factory.dart @@ -0,0 +1,18 @@ +import 'package:syncrow_web/pages/device_managment/factories/device_bloc_dependencies_factory.dart'; +import 'package:syncrow_web/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_bloc.dart'; + +abstract final class ThreeGangGlassSwitchBlocFactory { + const ThreeGangGlassSwitchBlocFactory._(); + + static ThreeGangGlassSwitchBloc create({ + required String deviceId, + }) { + return ThreeGangGlassSwitchBloc( + deviceId: deviceId, + controlDeviceService: + DeviceBlocDependenciesFactory.createControlDeviceService(), + batchControlDevicesService: + DeviceBlocDependenciesFactory.createBatchControlDevicesService(), + ); + } +} diff --git a/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_batch_control_view.dart b/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_batch_control_view.dart index 071d6ca0..93fbe53e 100644 --- a/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_batch_control_view.dart +++ b/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_batch_control_view.dart @@ -5,6 +5,7 @@ import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_ // import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/three_g_glass_switch/factories/three_gang_glass_switch_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/three_g_glass_switch/models/three_gang_glass_switch.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; @@ -16,7 +17,7 @@ class ThreeGangGlassSwitchBatchControlView extends StatelessWidget with HelperRe @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => ThreeGangGlassSwitchBloc(deviceId: deviceIds.first) + create: (context) => ThreeGangGlassSwitchBlocFactory.create(deviceId: deviceIds.first) ..add(ThreeGangGlassSwitchFetchBatchStatusEvent(deviceIds)), child: BlocBuilder( builder: (context, state) { diff --git a/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart b/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart index 433e5408..21a81df0 100644 --- a/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart +++ b/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/three_g_glass_switch/factories/three_gang_glass_switch_bloc_factory.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; @@ -16,7 +17,7 @@ class ThreeGangGlassSwitchControlView extends StatelessWidget with HelperRespons Widget build(BuildContext context) { return BlocProvider( create: (context) => - ThreeGangGlassSwitchBloc(deviceId: deviceId)..add(ThreeGangGlassSwitchFetchDeviceEvent(deviceId)), + ThreeGangGlassSwitchBlocFactory.create(deviceId: deviceId)..add(ThreeGangGlassSwitchFetchDeviceEvent(deviceId)), child: BlocBuilder( builder: (context, state) { if (state is ThreeGangGlassSwitchLoading) { diff --git a/lib/pages/device_managment/three_gang_switch/bloc/living_room_bloc.dart b/lib/pages/device_managment/three_gang_switch/bloc/living_room_bloc.dart index a7a03a7f..bec1314c 100644 --- a/lib/pages/device_managment/three_gang_switch/bloc/living_room_bloc.dart +++ b/lib/pages/device_managment/three_gang_switch/bloc/living_room_bloc.dart @@ -1,12 +1,14 @@ -// ignore_for_file: invalid_use_of_visible_for_testing_member - import 'dart:async'; -import 'package:bloc/bloc.dart'; +import 'dart:developer'; + import 'package:equatable/equatable.dart'; import 'package:firebase_database/firebase_database.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; import 'package:syncrow_web/pages/device_managment/three_gang_switch/models/living_room_model.dart'; +import 'package:syncrow_web/services/batch_control_devices_service.dart'; +import 'package:syncrow_web/services/control_device_service.dart'; import 'package:syncrow_web/services/devices_mang_api.dart'; part 'living_room_event.dart'; @@ -15,9 +17,14 @@ part 'living_room_state.dart'; class LivingRoomBloc extends Bloc { late LivingRoomStatusModel deviceStatus; final String deviceId; - Timer? _timer; + final ControlDeviceService controlDeviceService; + final BatchControlDevicesService batchControlDevicesService; - LivingRoomBloc({required this.deviceId}) : super(LivingRoomInitial()) { + LivingRoomBloc({ + required this.deviceId, + required this.controlDeviceService, + required this.batchControlDevicesService, + }) : super(LivingRoomInitial()) { on(_onFetchDeviceStatus); on(_livingRoomControl); on(_livingRoomBatchControl); @@ -26,156 +33,108 @@ class LivingRoomBloc extends Bloc { on(_onStatusUpdated); } - FutureOr _onFetchDeviceStatus(LivingRoomFetchDeviceStatusEvent event, - Emitter emit) async { + Future _onFetchDeviceStatus( + LivingRoomFetchDeviceStatusEvent event, + Emitter emit, + ) async { emit(LivingRoomDeviceStatusLoading()); try { - final status = - await DevicesManagementApi().getDeviceStatus(event.deviceId); - deviceStatus = - LivingRoomStatusModel.fromJson(event.deviceId, status.status); + final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); _listenToChanges(deviceId); + deviceStatus = LivingRoomStatusModel.fromJson(event.deviceId, status.status); emit(LivingRoomDeviceStatusLoaded(deviceStatus)); } catch (e) { emit(LivingRoomDeviceManagementError(e.toString())); } } - FutureOr _livingRoomControl( - LivingRoomControl event, Emitter emit) async { - final oldValue = _getValueByCode(event.code); + void _listenToChanges(String deviceId) { + try { + final ref = FirebaseDatabase.instance.ref('device-status/$deviceId'); + ref.onValue.listen((event) { + final eventsMap = event.snapshot.value as Map; + List statusList = []; + eventsMap['status'].forEach((element) { + statusList.add( + Status(code: element['code'], value: element['value']), + ); + }); + + deviceStatus = LivingRoomStatusModel.fromJson(deviceId, statusList); + add(StatusUpdated(deviceStatus)); + }); + } catch (_) { + log('Error listening to changes'); + } + } + + void _onStatusUpdated( + StatusUpdated event, + Emitter emit, + ) { + deviceStatus = event.deviceStatus; + emit(LivingRoomDeviceStatusLoaded(deviceStatus)); + } + + Future _livingRoomControl( + LivingRoomControl event, + Emitter emit, + ) async { + emit(LivingRoomDeviceStatusLoading()); _updateLocalValue(event.code, event.value); - emit(LivingRoomDeviceStatusLoaded(deviceStatus)); - await _runDebounce( - deviceId: event.deviceId, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - isBatch: false, - ); + try { + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status(code: event.code, value: event.value), + ); + } catch (e) { + _updateLocalValue(event.code, !event.value); + emit(LivingRoomDeviceManagementError(e.toString())); + } } - Future _runDebounce({ - required dynamic deviceId, - required String code, - required dynamic value, - required dynamic oldValue, - required Emitter emit, - required bool isBatch, - }) async { - late String id; - - if (deviceId is List) { - id = deviceId.first; - } else { - id = deviceId; - } - - if (_timer != null) { - _timer!.cancel(); - } - _timer = Timer(const Duration(seconds: 1), () async { - try { - late bool response; - if (isBatch) { - response = await DevicesManagementApi() - .deviceBatchControl(deviceId, code, value); - } else { - response = await DevicesManagementApi() - .deviceControl(deviceId, Status(code: code, value: value)); - } - if (!response) { - _revertValueAndEmit(id, code, oldValue, emit); - } - } catch (e) { - _revertValueAndEmit(id, code, oldValue, emit); - } - }); - } - - void _revertValueAndEmit(String deviceId, String code, dynamic oldValue, - Emitter emit) { - _updateLocalValue(code, oldValue); + Future _livingRoomBatchControl( + LivingRoomBatchControl event, + Emitter emit, + ) async { + emit(LivingRoomDeviceStatusLoading()); + _updateLocalValue(event.code, event.value); emit(LivingRoomDeviceStatusLoaded(deviceStatus)); - } - void _updateLocalValue(String code, dynamic value) { - switch (code) { - case 'switch_1': - if (value is bool) { - deviceStatus = deviceStatus.copyWith(switch1: value); - } - break; - case 'switch_2': - if (value is bool) { - deviceStatus = deviceStatus.copyWith(switch2: value); - } - break; - case 'switch_3': - if (value is bool) { - deviceStatus = deviceStatus.copyWith(switch3: value); - } - break; - default: - break; - } - emit(LivingRoomDeviceStatusLoaded(deviceStatus)); - } - - dynamic _getValueByCode(String code) { - switch (code) { - case 'switch_1': - return deviceStatus.switch1; - case 'switch_2': - return deviceStatus.switch2; - case 'switch_3': - return deviceStatus.switch3; - default: - return null; + try { + await batchControlDevicesService.batchControlDevices( + uuids: event.devicesIds, + code: event.code, + value: event.value, + ); + } catch (e) { + _updateLocalValue(event.code, !event.value); + emit(LivingRoomDeviceManagementError(e.toString())); } } - FutureOr _livingRoomFetchBatchControl( - LivingRoomFetchBatchEvent event, Emitter emit) async { + Future _livingRoomFetchBatchControl( + LivingRoomFetchBatchEvent event, + Emitter emit, + ) async { emit(LivingRoomDeviceStatusLoading()); try { - final status = - await DevicesManagementApi().getBatchStatus(event.devicesIds); + final status = await DevicesManagementApi().getBatchStatus(event.devicesIds); deviceStatus = LivingRoomStatusModel.fromJson(event.devicesIds.first, status.status); - // for (var deviceId in event.devicesIds) { - // _listenToChanges(deviceId); - // } emit(LivingRoomDeviceStatusLoaded(deviceStatus)); } catch (e) { emit(LivingRoomDeviceManagementError(e.toString())); } } - FutureOr _livingRoomBatchControl( - LivingRoomBatchControl event, Emitter emit) async { - final oldValue = _getValueByCode(event.code); - - _updateLocalValue(event.code, event.value); - - emit(LivingRoomDeviceStatusLoaded(deviceStatus)); - - await _runDebounce( - deviceId: event.devicesIds, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - isBatch: true, - ); - } - - FutureOr _livingRoomFactoryReset( - LivingRoomFactoryResetEvent event, Emitter emit) async { + Future _livingRoomFactoryReset( + LivingRoomFactoryResetEvent event, + Emitter emit, + ) async { emit(LivingRoomDeviceStatusLoading()); try { final response = await DevicesManagementApi().factoryReset( @@ -183,42 +142,28 @@ class LivingRoomBloc extends Bloc { event.uuid, ); if (!response) { - emit(const LivingRoomDeviceManagementError('Failed')); + emit(const LivingRoomDeviceManagementError('Failed to reset device')); } else { - emit(LivingRoomDeviceStatusLoaded(deviceStatus)); + add(LivingRoomFetchDeviceStatusEvent(event.uuid)); } } catch (e) { emit(LivingRoomDeviceManagementError(e.toString())); } } - _listenToChanges(deviceId) { - try { - DatabaseReference ref = - FirebaseDatabase.instance.ref('device-status/$deviceId'); - Stream stream = ref.onValue; + void _updateLocalValue(String code, dynamic value) { + if (value is! bool) return; - stream.listen((DatabaseEvent event) { - Map usersMap = - event.snapshot.value as Map; - - List statusList = []; - usersMap['status'].forEach((element) { - statusList - .add(Status(code: element['code'], value: element['value'])); - }); - - deviceStatus = - LivingRoomStatusModel.fromJson(usersMap['productUuid'], statusList); - if (!isClosed) { - add(StatusUpdated(deviceStatus)); - } - }); - } catch (_) {} - } - - void _onStatusUpdated(StatusUpdated event, Emitter emit) { - deviceStatus = event.deviceStatus; - emit(LivingRoomDeviceStatusLoaded(deviceStatus)); + switch (code) { + case 'switch_1': + deviceStatus = deviceStatus.copyWith(switch1: value); + break; + case 'switch_2': + deviceStatus = deviceStatus.copyWith(switch2: value); + break; + case 'switch_3': + deviceStatus = deviceStatus.copyWith(switch3: value); + break; + } } } diff --git a/lib/pages/device_managment/three_gang_switch/factories/living_room_bloc_factory.dart b/lib/pages/device_managment/three_gang_switch/factories/living_room_bloc_factory.dart new file mode 100644 index 00000000..94c2b72f --- /dev/null +++ b/lib/pages/device_managment/three_gang_switch/factories/living_room_bloc_factory.dart @@ -0,0 +1,18 @@ +import 'package:syncrow_web/pages/device_managment/factories/device_bloc_dependencies_factory.dart'; +import 'package:syncrow_web/pages/device_managment/three_gang_switch/bloc/living_room_bloc.dart'; + +abstract final class LivingRoomBlocFactory { + const LivingRoomBlocFactory._(); + + static LivingRoomBloc create({ + required String deviceId, + }) { + return LivingRoomBloc( + deviceId: deviceId, + controlDeviceService: + DeviceBlocDependenciesFactory.createControlDeviceService(), + batchControlDevicesService: + DeviceBlocDependenciesFactory.createBatchControlDevicesService(), + ); + } +} diff --git a/lib/pages/device_managment/three_gang_switch/view/living_room_batch_controls.dart b/lib/pages/device_managment/three_gang_switch/view/living_room_batch_controls.dart index 97c25287..0b1a2f06 100644 --- a/lib/pages/device_managment/three_gang_switch/view/living_room_batch_controls.dart +++ b/lib/pages/device_managment/three_gang_switch/view/living_room_batch_controls.dart @@ -4,6 +4,7 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_re import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; // import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; import 'package:syncrow_web/pages/device_managment/three_gang_switch/bloc/living_room_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/three_gang_switch/factories/living_room_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/three_gang_switch/models/living_room_model.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; @@ -17,7 +18,7 @@ class LivingRoomBatchControlsView extends StatelessWidget with HelperResponsiveL Widget build(BuildContext context) { return BlocProvider( create: (context) => - LivingRoomBloc(deviceId: deviceIds.first)..add(LivingRoomFetchBatchEvent(deviceIds)), + LivingRoomBlocFactory.create(deviceId: deviceIds.first)..add(LivingRoomFetchBatchEvent(deviceIds)), child: BlocBuilder( builder: (context, state) { if (state is LivingRoomDeviceStatusLoading) { diff --git a/lib/pages/device_managment/three_gang_switch/view/living_room_device_control.dart b/lib/pages/device_managment/three_gang_switch/view/living_room_device_control.dart index b7f97776..731b354c 100644 --- a/lib/pages/device_managment/three_gang_switch/view/living_room_device_control.dart +++ b/lib/pages/device_managment/three_gang_switch/view/living_room_device_control.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/three_gang_switch/bloc/living_room_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/three_gang_switch/factories/living_room_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/three_gang_switch/models/living_room_model.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; @@ -14,7 +15,7 @@ class LivingRoomDeviceControlsView extends StatelessWidget @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => LivingRoomBloc(deviceId: deviceId) + create: (context) => LivingRoomBlocFactory.create(deviceId: deviceId) ..add(LivingRoomFetchDeviceStatusEvent(deviceId)), child: BlocBuilder( builder: (context, state) { diff --git a/lib/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_bloc.dart b/lib/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_bloc.dart index 406821da..8f82c198 100644 --- a/lib/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_bloc.dart +++ b/lib/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_bloc.dart @@ -1,26 +1,33 @@ import 'dart:async'; +import 'dart:developer'; + import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; import 'package:firebase_database/firebase_database.dart'; -import 'package:meta/meta.dart'; +import 'package:flutter/foundation.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/models/two_gang_glass_status_model.dart'; +import 'package:syncrow_web/services/batch_control_devices_service.dart'; +import 'package:syncrow_web/services/control_device_service.dart'; import 'package:syncrow_web/services/devices_mang_api.dart'; + part 'two_gang_glass_switch_event.dart'; part 'two_gang_glass_switch_state.dart'; class TwoGangGlassSwitchBloc extends Bloc { - TwoGangGlassStatusModel deviceStatus; - Timer? _timer; - TwoGangGlassSwitchBloc({required String deviceId}) - : deviceStatus = TwoGangGlassStatusModel( - uuid: deviceId, - switch1: false, - countDown1: 0, - switch2: false, - countDown2: 0), - super(TwoGangGlassSwitchInitial()) { + final String deviceId; + final ControlDeviceService controlDeviceService; + final BatchControlDevicesService batchControlDevicesService; + + late TwoGangGlassStatusModel deviceStatus; + + TwoGangGlassSwitchBloc({ + required this.deviceId, + required this.controlDeviceService, + required this.batchControlDevicesService, + }) : super(TwoGangGlassSwitchInitial()) { on(_onFetchDeviceStatus); on(_onControl); on(_onBatchControl); @@ -29,14 +36,14 @@ class TwoGangGlassSwitchBloc on(_onStatusUpdated); } - Future _onFetchDeviceStatus(TwoGangGlassSwitchFetchDeviceEvent event, - Emitter emit) async { + Future _onFetchDeviceStatus( + TwoGangGlassSwitchFetchDeviceEvent event, + Emitter emit, + ) async { emit(TwoGangGlassSwitchLoading()); try { - final status = - await DevicesManagementApi().getDeviceStatus(event.deviceId); - deviceStatus = - TwoGangGlassStatusModel.fromJson(event.deviceId, status.status); + final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); + deviceStatus = TwoGangGlassStatusModel.fromJson(event.deviceId, status.status); _listenToChanges(event.deviceId); emit(TwoGangGlassSwitchStatusLoaded(deviceStatus)); } catch (e) { @@ -46,200 +53,121 @@ class TwoGangGlassSwitchBloc void _listenToChanges(String deviceId) { try { - DatabaseReference ref = - FirebaseDatabase.instance.ref('device-status/$deviceId'); - ref.onValue.listen((DatabaseEvent event) { - if (event.snapshot.value == null) return; + final ref = FirebaseDatabase.instance.ref( + 'device-status/$deviceId', + ); + + ref.onValue.listen((event) { + final eventsMap = event.snapshot.value as Map; - Map data = - event.snapshot.value as Map; List statusList = []; - - data['status'].forEach((element) { - statusList - .add(Status(code: element['code'], value: element['value'])); + eventsMap['status'].forEach((element) { + statusList.add(Status(code: element['code'], value: element['value'])); }); - // Parse the new status and add the event - final updatedStatus = - TwoGangGlassStatusModel.fromJson(data['productUuid'], statusList); - if (!isClosed) { - add(StatusUpdated(updatedStatus)); - } + deviceStatus = TwoGangGlassStatusModel.fromJson(deviceId, statusList); + add(StatusUpdated(deviceStatus)); }); - } catch (e) { - // Handle errors and emit an error state if necessary - if (!isClosed) { - // add(TwoGangGlassSwitchError('Error listening to updates: $e')); - } + } catch (_) { + log( + 'Error listening to changes', + name: 'TwoGangGlassSwitchBloc._listenToChanges', + ); } } - Future _onControl(TwoGangGlassSwitchControl event, - Emitter emit) async { - final oldValue = _getValueByCode(event.code); - + Future _onControl( + TwoGangGlassSwitchControl event, + Emitter emit, + ) async { + emit(TwoGangGlassSwitchLoading()); _updateLocalValue(event.code, event.value); emit(TwoGangGlassSwitchStatusLoaded(deviceStatus)); - await _runDebounce( - deviceId: event.deviceId, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - isBatch: false, - ); + try { + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status(code: event.code, value: event.value), + ); + } catch (e) { + _updateLocalValue(event.code, !event.value); + emit(TwoGangGlassSwitchError(e.toString())); + } } - Future _onBatchControl(TwoGangGlassSwitchBatchControl event, - Emitter emit) async { - final oldValue = _getValueByCode(event.code); - + Future _onBatchControl( + TwoGangGlassSwitchBatchControl event, + Emitter emit, + ) async { + emit(TwoGangGlassSwitchLoading()); _updateLocalValue(event.code, event.value); emit(TwoGangGlassSwitchBatchStatusLoaded(deviceStatus)); - await _runDebounce( - deviceId: event.deviceIds, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - isBatch: true, - ); + try { + await batchControlDevicesService.batchControlDevices( + uuids: event.deviceIds, + code: event.code, + value: event.value, + ); + } catch (e) { + _updateLocalValue(event.code, !event.value); + emit(TwoGangGlassSwitchError(e.toString())); + } } Future _onFetchBatchStatus( - TwoGangGlassSwitchFetchBatchStatusEvent event, - Emitter emit) async { + TwoGangGlassSwitchFetchBatchStatusEvent event, + Emitter emit, + ) async { emit(TwoGangGlassSwitchLoading()); try { - final status = - await DevicesManagementApi().getBatchStatus(event.deviceIds); + final status = await DevicesManagementApi().getBatchStatus(event.deviceIds); deviceStatus = TwoGangGlassStatusModel.fromJson( - event.deviceIds.first, status.status); + event.deviceIds.first, + status.status, + ); emit(TwoGangGlassSwitchBatchStatusLoaded(deviceStatus)); } catch (e) { emit(TwoGangGlassSwitchError(e.toString())); } } - Future _onFactoryReset(TwoGangGlassFactoryReset event, - Emitter emit) async { + Future _onFactoryReset( + TwoGangGlassFactoryReset event, + Emitter emit, + ) async { emit(TwoGangGlassSwitchLoading()); try { - final response = await DevicesManagementApi() - .factoryReset(event.factoryReset, event.deviceId); + final response = await DevicesManagementApi().factoryReset( + event.factoryReset, + event.deviceId, + ); if (!response) { - emit(TwoGangGlassSwitchError('Failed')); + emit(TwoGangGlassSwitchError('Failed to reset device')); } else { - emit(TwoGangGlassSwitchStatusLoaded(deviceStatus)); + add(TwoGangGlassSwitchFetchDeviceEvent(event.deviceId)); } } catch (e) { emit(TwoGangGlassSwitchError(e.toString())); } } - Future _runDebounce({ - required dynamic deviceId, - required String code, - required bool value, - required bool oldValue, - required Emitter emit, - required bool isBatch, - }) async { - late String id; - if (deviceId is List) { - id = deviceId.first; - } else { - id = deviceId; - } - - if (_timer != null) { - _timer!.cancel(); - } - - _timer = Timer(const Duration(milliseconds: 500), () async { - try { - late bool response; - if (isBatch) { - response = await DevicesManagementApi() - .deviceBatchControl(deviceId, code, value); - } else { - response = await DevicesManagementApi() - .deviceControl(deviceId, Status(code: code, value: value)); - } - - if (!response) { - _revertValueAndEmit(id, code, oldValue, emit); - } - } catch (e) { - _revertValueAndEmit(id, code, oldValue, emit); - } - }); - } - - void _revertValueAndEmit(String deviceId, String code, bool oldValue, - Emitter emit) { - _updateLocalValue(code, oldValue); + void _onStatusUpdated( + StatusUpdated event, + Emitter emit, + ) { + deviceStatus = event.deviceStatus; emit(TwoGangGlassSwitchStatusLoaded(deviceStatus)); } void _updateLocalValue(String code, bool value) { - if (code == 'switch_1') { - deviceStatus = deviceStatus.copyWith(switch1: value); - } else if (code == 'switch_2') { - deviceStatus = deviceStatus.copyWith(switch2: value); - } - } - - bool _getValueByCode(String code) { switch (code) { case 'switch_1': - return deviceStatus.switch1; + deviceStatus = deviceStatus.copyWith(switch1: value); + break; case 'switch_2': - return deviceStatus.switch2; - default: - return false; + deviceStatus = deviceStatus.copyWith(switch2: value); + break; } } - - @override - Future close() { - _timer?.cancel(); - return super.close(); - } - - // _listenToChanges(deviceId) { - // try { - // DatabaseReference ref = - // FirebaseDatabase.instance.ref('device-status/$deviceId'); - // Stream stream = ref.onValue; - - // stream.listen((DatabaseEvent event) { - // Map usersMap = - // event.snapshot.value as Map; - - // List statusList = []; - // usersMap['status'].forEach((element) { - // statusList - // .add(Status(code: element['code'], value: element['value'])); - // }); - - // deviceStatus = TwoGangGlassStatusModel.fromJson( - // usersMap['productUuid'], statusList); - // if (!isClosed) { - // add(StatusUpdated(deviceStatus)); - // } - // }); - // } catch (_) {} - // } - - void _onStatusUpdated( - StatusUpdated event, Emitter emit) { - // Update the local deviceStatus with the new status from the event - deviceStatus = event.deviceStatus; - // Emit the new state with the updated status - emit(TwoGangGlassSwitchStatusLoaded(deviceStatus)); - } } diff --git a/lib/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_event.dart b/lib/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_event.dart index 02b61bd0..46444cce 100644 --- a/lib/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_event.dart +++ b/lib/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_event.dart @@ -1,12 +1,17 @@ part of 'two_gang_glass_switch_bloc.dart'; @immutable -abstract class TwoGangGlassSwitchEvent {} +abstract class TwoGangGlassSwitchEvent extends Equatable { + const TwoGangGlassSwitchEvent(); +} class TwoGangGlassSwitchFetchDeviceEvent extends TwoGangGlassSwitchEvent { final String deviceId; - TwoGangGlassSwitchFetchDeviceEvent(this.deviceId); + const TwoGangGlassSwitchFetchDeviceEvent(this.deviceId); + + @override + List get props => [deviceId]; } class TwoGangGlassSwitchControl extends TwoGangGlassSwitchEvent { @@ -14,11 +19,14 @@ class TwoGangGlassSwitchControl extends TwoGangGlassSwitchEvent { final String code; final bool value; - TwoGangGlassSwitchControl({ + const TwoGangGlassSwitchControl({ required this.deviceId, required this.code, required this.value, }); + + @override + List get props => [deviceId, code, value]; } class TwoGangGlassSwitchBatchControl extends TwoGangGlassSwitchEvent { @@ -26,33 +34,43 @@ class TwoGangGlassSwitchBatchControl extends TwoGangGlassSwitchEvent { final String code; final bool value; - TwoGangGlassSwitchBatchControl({ + const TwoGangGlassSwitchBatchControl({ required this.deviceIds, required this.code, required this.value, }); + + @override + List get props => [deviceIds, code, value]; } class TwoGangGlassSwitchFetchBatchStatusEvent extends TwoGangGlassSwitchEvent { final List deviceIds; - TwoGangGlassSwitchFetchBatchStatusEvent(this.deviceIds); + const TwoGangGlassSwitchFetchBatchStatusEvent(this.deviceIds); + + @override + List get props => [deviceIds]; } class TwoGangGlassFactoryReset extends TwoGangGlassSwitchEvent { final String deviceId; final FactoryResetModel factoryReset; - TwoGangGlassFactoryReset({ + const TwoGangGlassFactoryReset({ required this.deviceId, required this.factoryReset, }); + + @override + List get props => [deviceId, factoryReset]; } class StatusUpdated extends TwoGangGlassSwitchEvent { final TwoGangGlassStatusModel deviceStatus; - StatusUpdated(this.deviceStatus); + + const StatusUpdated(this.deviceStatus); + @override List get props => [deviceStatus]; } - diff --git a/lib/pages/device_managment/two_g_glass_switch/factories/two_gang_glass_switch_bloc_factory.dart b/lib/pages/device_managment/two_g_glass_switch/factories/two_gang_glass_switch_bloc_factory.dart new file mode 100644 index 00000000..bd832d8f --- /dev/null +++ b/lib/pages/device_managment/two_g_glass_switch/factories/two_gang_glass_switch_bloc_factory.dart @@ -0,0 +1,18 @@ +import 'package:syncrow_web/pages/device_managment/factories/device_bloc_dependencies_factory.dart'; +import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_bloc.dart'; + +abstract final class TwoGangGlassSwitchBlocFactory { + const TwoGangGlassSwitchBlocFactory._(); + + static TwoGangGlassSwitchBloc create({ + required String deviceId, + }) { + return TwoGangGlassSwitchBloc( + deviceId: deviceId, + controlDeviceService: + DeviceBlocDependenciesFactory.createControlDeviceService(), + batchControlDevicesService: + DeviceBlocDependenciesFactory.createBatchControlDevicesService(), + ); + } +} diff --git a/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_batch_control_view.dart b/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_batch_control_view.dart index c84c1d07..9d120ad6 100644 --- a/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_batch_control_view.dart +++ b/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_batch_control_view.dart @@ -5,6 +5,7 @@ import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_ // import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/factories/two_gang_glass_switch_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/models/two_gang_glass_status_model.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; @@ -16,7 +17,7 @@ class TwoGangGlassSwitchBatchControlView extends StatelessWidget with HelperResp @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => TwoGangGlassSwitchBloc(deviceId: deviceIds.first) + create: (context) => TwoGangGlassSwitchBlocFactory.create(deviceId: deviceIds.first) ..add(TwoGangGlassSwitchFetchBatchStatusEvent(deviceIds)), child: BlocBuilder( builder: (context, state) { diff --git a/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart b/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart index cca794e9..575deeac 100644 --- a/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart +++ b/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/factories/two_gang_glass_switch_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/models/two_gang_glass_status_model.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; @@ -15,7 +16,7 @@ class TwoGangGlassSwitchControlView extends StatelessWidget @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => TwoGangGlassSwitchBloc(deviceId: deviceId) + create: (context) => TwoGangGlassSwitchBlocFactory.create(deviceId: deviceId) ..add(TwoGangGlassSwitchFetchDeviceEvent(deviceId)), child: BlocBuilder( builder: (context, state) { diff --git a/lib/pages/device_managment/two_gang_switch/bloc/two_gang_switch_bloc.dart b/lib/pages/device_managment/two_gang_switch/bloc/two_gang_switch_bloc.dart index ea72e05b..2e3a8633 100644 --- a/lib/pages/device_managment/two_gang_switch/bloc/two_gang_switch_bloc.dart +++ b/lib/pages/device_managment/two_gang_switch/bloc/two_gang_switch_bloc.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:developer'; import 'package:firebase_database/firebase_database.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -6,10 +7,22 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/device_sta import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_event.dart'; import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_state.dart'; import 'package:syncrow_web/pages/device_managment/two_gang_switch/models/two_gang_status_model.dart'; +import 'package:syncrow_web/services/batch_control_devices_service.dart'; +import 'package:syncrow_web/services/control_device_service.dart'; import 'package:syncrow_web/services/devices_mang_api.dart'; class TwoGangSwitchBloc extends Bloc { - TwoGangSwitchBloc({required this.deviceId}) : super(TwoGangSwitchInitial()) { + final String deviceId; + final ControlDeviceService controlDeviceService; + final BatchControlDevicesService batchControlDevicesService; + + late TwoGangStatusModel deviceStatus; + + TwoGangSwitchBloc({ + required this.deviceId, + required this.controlDeviceService, + required this.batchControlDevicesService, + }) : super(TwoGangSwitchInitial()) { on(_onFetchDeviceStatus); on(_onControl); on(_onFetchBatchStatus); @@ -18,16 +31,13 @@ class TwoGangSwitchBloc extends Bloc { on(_onStatusUpdated); } - late TwoGangStatusModel deviceStatus; - final String deviceId; - Timer? _timer; - - FutureOr _onFetchDeviceStatus(TwoGangSwitchFetchDeviceEvent event, - Emitter emit) async { + Future _onFetchDeviceStatus( + TwoGangSwitchFetchDeviceEvent event, + Emitter emit, + ) async { emit(TwoGangSwitchLoading()); try { - final status = - await DevicesManagementApi().getDeviceStatus(event.deviceId); + final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); deviceStatus = TwoGangStatusModel.fromJson(event.deviceId, status.status); _listenToChanges(event.deviceId); emit(TwoGangSwitchStatusLoaded(deviceStatus)); @@ -36,131 +46,91 @@ class TwoGangSwitchBloc extends Bloc { } } - FutureOr _onControl( - TwoGangSwitchControl event, Emitter emit) async { - final oldValue = _getValueByCode(event.code); + void _listenToChanges(String deviceId) { + try { + final ref = FirebaseDatabase.instance.ref('device-status/$deviceId'); + ref.onValue.listen((event) { + final eventsMap = event.snapshot.value as Map; + + List statusList = []; + eventsMap['status'].forEach((element) { + statusList.add( + Status(code: element['code'], value: element['value']), + ); + }); + + deviceStatus = TwoGangStatusModel.fromJson(deviceId, statusList); + add(StatusUpdated(deviceStatus)); + }); + } catch (_) { + log( + 'Error listening to changes', + name: 'TwoGangSwitchBloc._listenToChanges', + ); + } + } + + Future _onControl( + TwoGangSwitchControl event, + Emitter emit, + ) async { + emit(TwoGangSwitchLoading()); _updateLocalValue(event.code, event.value); - emit(TwoGangSwitchStatusLoaded(deviceStatus)); - await _runDebounce( - deviceId: event.deviceId, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - isBatch: false, - ); + try { + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status(code: event.code, value: event.value), + ); + } catch (e) { + _updateLocalValue(event.code, !event.value); + emit(TwoGangSwitchError(e.toString())); + } } - Future _runDebounce({ - required dynamic deviceId, - required String code, - required bool value, - required bool oldValue, - required Emitter emit, - required bool isBatch, - }) async { - late String id; - - if (deviceId is List) { - id = deviceId.first; - } else { - id = deviceId; - } - - if (_timer != null) { - _timer!.cancel(); - } - - _timer = Timer(const Duration(milliseconds: 500), () async { - try { - late bool response; - if (isBatch) { - response = await DevicesManagementApi() - .deviceBatchControl(deviceId, code, value); - } else { - response = await DevicesManagementApi() - .deviceControl(deviceId, Status(code: code, value: value)); - } - - if (!response) { - _revertValueAndEmit(id, code, oldValue, emit); - } - } catch (e) { - _revertValueAndEmit(id, code, oldValue, emit); - } - }); - } - - void _revertValueAndEmit(String deviceId, String code, bool oldValue, - Emitter emit) { - _updateLocalValue(code, oldValue); + Future _onBatchControl( + TwoGangSwitchBatchControl event, + Emitter emit, + ) async { + emit(TwoGangSwitchLoading()); + _updateLocalValue(event.code, event.value); emit(TwoGangSwitchStatusLoaded(deviceStatus)); - } - void _updateLocalValue(String code, bool value) { - if (code == 'switch_1') { - deviceStatus = deviceStatus.copyWith(switch1: value); - } - - if (code == 'switch_2') { - deviceStatus = deviceStatus.copyWith(switch2: value); + try { + await batchControlDevicesService.batchControlDevices( + uuids: event.deviceId, + code: event.code, + value: event.value, + ); + } catch (e) { + _updateLocalValue(event.code, !event.value); + emit(TwoGangSwitchError(e.toString())); } } - bool _getValueByCode(String code) { - switch (code) { - case 'switch_1': - return deviceStatus.switch1; - case 'switch_2': - return deviceStatus.switch2; - default: - return false; - } - } - - Future _onFetchBatchStatus(TwoGangSwitchFetchBatchEvent event, - Emitter emit) async { + Future _onFetchBatchStatus( + TwoGangSwitchFetchBatchEvent event, + Emitter emit, + ) async { emit(TwoGangSwitchLoading()); try { - final status = - await DevicesManagementApi().getBatchStatus(event.devicesIds); - deviceStatus = - TwoGangStatusModel.fromJson(event.devicesIds.first, status.status); + final status = await DevicesManagementApi().getBatchStatus(event.devicesIds); + deviceStatus = TwoGangStatusModel.fromJson( + event.devicesIds.first, + status.status, + ); emit(TwoGangSwitchStatusLoaded(deviceStatus)); } catch (e) { emit(TwoGangSwitchError(e.toString())); } } - @override - Future close() { - _timer?.cancel(); - return super.close(); - } - - FutureOr _onBatchControl( - TwoGangSwitchBatchControl event, Emitter emit) async { - final oldValue = _getValueByCode(event.code); - - _updateLocalValue(event.code, event.value); - - emit(TwoGangSwitchStatusLoaded(deviceStatus)); - - await _runDebounce( - deviceId: event.deviceId, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - isBatch: true, - ); - } - - FutureOr _onFactoryReset( - TwoGangFactoryReset event, Emitter emit) async { + Future _onFactoryReset( + TwoGangFactoryReset event, + Emitter emit, + ) async { emit(TwoGangSwitchLoading()); try { final response = await DevicesManagementApi().factoryReset( @@ -168,42 +138,31 @@ class TwoGangSwitchBloc extends Bloc { event.deviceId, ); if (!response) { - emit(TwoGangSwitchError('Failed')); + emit(TwoGangSwitchError('Failed to reset device')); } else { - emit(TwoGangSwitchStatusLoaded(deviceStatus)); + add(TwoGangSwitchFetchDeviceEvent(event.deviceId)); } } catch (e) { emit(TwoGangSwitchError(e.toString())); } } - _listenToChanges(deviceId) { - try { - DatabaseReference ref = - FirebaseDatabase.instance.ref('device-status/$deviceId'); - Stream stream = ref.onValue; - - stream.listen((DatabaseEvent event) { - Map usersMap = - event.snapshot.value as Map; - - List statusList = []; - usersMap['status'].forEach((element) { - statusList - .add(Status(code: element['code'], value: element['value'])); - }); - - deviceStatus = - TwoGangStatusModel.fromJson(usersMap['productUuid'], statusList); - if (!isClosed) { - add(StatusUpdated(deviceStatus)); - } - }); - } catch (_) {} - } - - void _onStatusUpdated(StatusUpdated event, Emitter emit) { + void _onStatusUpdated( + StatusUpdated event, + Emitter emit, + ) { deviceStatus = event.deviceStatus; emit(TwoGangSwitchStatusLoaded(deviceStatus)); } + + void _updateLocalValue(String code, bool value) { + switch (code) { + case 'switch_1': + deviceStatus = deviceStatus.copyWith(switch1: value); + break; + case 'switch_2': + deviceStatus = deviceStatus.copyWith(switch2: value); + break; + } + } } diff --git a/lib/pages/device_managment/two_gang_switch/factories/two_gang_switch_bloc_factory.dart b/lib/pages/device_managment/two_gang_switch/factories/two_gang_switch_bloc_factory.dart new file mode 100644 index 00000000..37893caf --- /dev/null +++ b/lib/pages/device_managment/two_gang_switch/factories/two_gang_switch_bloc_factory.dart @@ -0,0 +1,18 @@ +import 'package:syncrow_web/pages/device_managment/factories/device_bloc_dependencies_factory.dart'; +import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_bloc.dart'; + +abstract final class TwoGangSwitchBlocFactory { + const TwoGangSwitchBlocFactory._(); + + static TwoGangSwitchBloc create({ + required String deviceId, + }) { + return TwoGangSwitchBloc( + deviceId: deviceId, + controlDeviceService: + DeviceBlocDependenciesFactory.createControlDeviceService(), + batchControlDevicesService: + DeviceBlocDependenciesFactory.createBatchControlDevicesService(), + ); + } +} diff --git a/lib/pages/device_managment/two_gang_switch/models/two_gang_status_model.dart b/lib/pages/device_managment/two_gang_switch/models/two_gang_status_model.dart index 6cec4256..58094a71 100644 --- a/lib/pages/device_managment/two_gang_switch/models/two_gang_status_model.dart +++ b/lib/pages/device_managment/two_gang_switch/models/two_gang_status_model.dart @@ -24,16 +24,16 @@ class TwoGangStatusModel { for (var status in jsonList) { switch (status.code) { case 'switch_1': - switch1 = status.value ?? false; + switch1 = bool.tryParse(status.value.toString()) ?? false; break; case 'countdown_1': - countDown = status.value ?? 0; + countDown = int.tryParse(status.value.toString()) ?? 0; break; case 'switch_2': - switch2 = status.value ?? false; + switch2 = bool.tryParse(status.value.toString()) ?? false; break; case 'countdown_2': - countDown2 = status.value ?? 0; + countDown2 = int.tryParse(status.value.toString()) ?? 0; break; } } diff --git a/lib/pages/device_managment/two_gang_switch/view/wall_light_batch_control.dart b/lib/pages/device_managment/two_gang_switch/view/wall_light_batch_control.dart index b3a39287..e8346cb2 100644 --- a/lib/pages/device_managment/two_gang_switch/view/wall_light_batch_control.dart +++ b/lib/pages/device_managment/two_gang_switch/view/wall_light_batch_control.dart @@ -2,11 +2,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; -// import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_bloc.dart'; import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_event.dart'; import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_state.dart'; +import 'package:syncrow_web/pages/device_managment/two_gang_switch/factories/two_gang_switch_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/two_gang_switch/models/two_gang_status_model.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; @@ -18,7 +18,7 @@ class TwoGangBatchControlView extends StatelessWidget with HelperResponsiveLayou @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => TwoGangSwitchBloc(deviceId: deviceIds.first) + create: (context) => TwoGangSwitchBlocFactory.create(deviceId: deviceIds.first) ..add(TwoGangSwitchFetchBatchEvent(deviceIds)), child: BlocBuilder( builder: (context, state) { diff --git a/lib/pages/device_managment/two_gang_switch/view/wall_light_device_control.dart b/lib/pages/device_managment/two_gang_switch/view/wall_light_device_control.dart index 840d356e..882aac3e 100644 --- a/lib/pages/device_managment/two_gang_switch/view/wall_light_device_control.dart +++ b/lib/pages/device_managment/two_gang_switch/view/wall_light_device_control.dart @@ -4,6 +4,7 @@ import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_bloc.dart'; import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_event.dart'; import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_state.dart'; +import 'package:syncrow_web/pages/device_managment/two_gang_switch/factories/two_gang_switch_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/two_gang_switch/models/two_gang_status_model.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; @@ -15,7 +16,7 @@ class TwoGangDeviceControlView extends StatelessWidget @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => TwoGangSwitchBloc(deviceId: deviceId) + create: (context) => TwoGangSwitchBlocFactory.create(deviceId: deviceId) ..add(TwoGangSwitchFetchDeviceEvent(deviceId)), child: BlocBuilder( builder: (context, state) { diff --git a/lib/pages/device_managment/wall_sensor/bloc/wall_bloc.dart b/lib/pages/device_managment/wall_sensor/bloc/wall_bloc.dart index 3c144142..630a132b 100644 --- a/lib/pages/device_managment/wall_sensor/bloc/wall_bloc.dart +++ b/lib/pages/device_managment/wall_sensor/bloc/wall_bloc.dart @@ -1,18 +1,28 @@ import 'dart:async'; +import 'dart:developer'; + import 'package:firebase_database/firebase_database.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; import 'package:syncrow_web/pages/device_managment/wall_sensor/bloc/wall_event.dart'; import 'package:syncrow_web/pages/device_managment/wall_sensor/bloc/wall_state.dart'; import 'package:syncrow_web/pages/device_managment/wall_sensor/model/wall_sensor_model.dart'; +import 'package:syncrow_web/services/batch_control_devices_service.dart'; +import 'package:syncrow_web/services/control_device_service.dart'; import 'package:syncrow_web/services/devices_mang_api.dart'; class WallSensorBloc extends Bloc { final String deviceId; - late WallSensorModel deviceStatus; - Timer? _timer; + final ControlDeviceService controlDeviceService; + final BatchControlDevicesService batchControlDevicesService; - WallSensorBloc({required this.deviceId}) : super(WallSensorInitialState()) { + late WallSensorModel deviceStatus; + + WallSensorBloc({ + required this.deviceId, + required this.controlDeviceService, + required this.batchControlDevicesService, + }) : super(WallSensorInitialState()) { on(_fetchWallSensorStatus); on(_fetchWallSensorBatchControl); on(_changeValue); @@ -24,28 +34,28 @@ class WallSensorBloc extends Bloc { on(_onRealtimeUpdate); } - void _fetchWallSensorStatus( - WallSensorFetchStatusEvent event, Emitter emit) async { + Future _fetchWallSensorStatus( + WallSensorFetchStatusEvent event, + Emitter emit, + ) async { emit(WallSensorLoadingInitialState()); try { - var response = await DevicesManagementApi().getDeviceStatus(deviceId); + final response = await DevicesManagementApi().getDeviceStatus(deviceId); deviceStatus = WallSensorModel.fromJson(response.status); - emit(WallSensorUpdateState(wallSensorModel: deviceStatus)); _listenToChanges(deviceId); + emit(WallSensorUpdateState(wallSensorModel: deviceStatus)); } catch (e) { emit(WallSensorFailedState(error: e.toString())); - return; } } - // Fetch batch status - FutureOr _fetchWallSensorBatchControl( - WallSensorFetchBatchStatusEvent event, - Emitter emit) async { + Future _fetchWallSensorBatchControl( + WallSensorFetchBatchStatusEvent event, + Emitter emit, + ) async { emit(WallSensorLoadingInitialState()); try { - var response = - await DevicesManagementApi().getBatchStatus(event.devicesIds); + final response = await DevicesManagementApi().getBatchStatus(event.devicesIds); deviceStatus = WallSensorModel.fromJson(response.status); emit(WallSensorUpdateState(wallSensorModel: deviceStatus)); } catch (e) { @@ -54,132 +64,105 @@ class WallSensorBloc extends Bloc { } void _listenToChanges(String deviceId) { - DatabaseReference ref = - FirebaseDatabase.instance.ref('device-status/$deviceId'); - ref.onValue.listen((DatabaseEvent event) { - final data = event.snapshot.value as Map?; - if (data == null) return; + try { + final ref = FirebaseDatabase.instance.ref('device-status/$deviceId'); - final statusList = (data['status'] as List?) - ?.map((e) => Status(code: e['code'], value: e['value'])) - .toList(); + ref.onValue.listen((event) { + final eventsMap = event.snapshot.value as Map; - if (statusList != null) { - final updatedDeviceStatus = WallSensorModel.fromJson(statusList); + List statusList = []; + eventsMap['status'].forEach((element) { + statusList.add( + Status(code: element['code'], value: element['value']), + ); + }); + + deviceStatus = WallSensorModel.fromJson(statusList); if (!isClosed) { - add(WallSensorRealtimeUpdateEvent(updatedDeviceStatus)); + add(WallSensorRealtimeUpdateEvent(deviceStatus)); } - } - }); + }); + } catch (_) { + log( + 'Error listening to changes', + name: 'WallSensorBloc._listenToChanges', + ); + } } - - void _changeValue( - WallSensorChangeValueEvent event, Emitter emit) async { + Future _changeValue( + WallSensorChangeValueEvent event, + Emitter emit, + ) async { emit(WallSensorLoadingNewSate(wallSensorModel: deviceStatus)); - if (event.code == 'far_detection') { - deviceStatus.farDetection = event.value; - } else if (event.code == 'motionless_sensitivity') { - deviceStatus.motionlessSensitivity = event.value; - } else if (event.code == 'motion_sensitivity_value') { - deviceStatus.motionSensitivity = event.value; - } else if (event.code == 'no_one_time') { - deviceStatus.noBodyTime = event.value; - } + _updateLocalValue(event.code, event.value); emit(WallSensorUpdateState(wallSensorModel: deviceStatus)); - await _runDeBouncer( - deviceId: deviceId, - code: event.code, - value: event.value, - isBatch: false, - emit: emit, - ); + + try { + await controlDeviceService.controlDevice( + deviceUuid: deviceId, + status: Status(code: event.code, value: event.value), + ); + } catch (e) { + _updateLocalValue(event.code, event.value == 0 ? 1 : 0); + emit(WallSensorFailedState(error: e.toString())); + } } Future _onBatchControl( - WallSensorBatchControlEvent event, Emitter emit) async { + WallSensorBatchControlEvent event, + Emitter emit, + ) async { emit(WallSensorLoadingNewSate(wallSensorModel: deviceStatus)); - if (event.code == 'far_detection') { - deviceStatus.farDetection = event.value; - } else if (event.code == 'motionless_sensitivity') { - deviceStatus.motionlessSensitivity = event.value; - } else if (event.code == 'motion_sensitivity_value') { - deviceStatus.motionSensitivity = event.value; - } else if (event.code == 'no_one_time') { - deviceStatus.noBodyTime = event.value; - } + _updateLocalValue(event.code, event.value); emit(WallSensorUpdateState(wallSensorModel: deviceStatus)); - await _runDeBouncer( - deviceId: event.deviceIds, - code: event.code, - value: event.value, - emit: emit, - isBatch: true, - ); - } - - _runDeBouncer({ - required dynamic deviceId, - required String code, - required dynamic value, - required Emitter emit, - required bool isBatch, - }) { - if (_timer != null) { - _timer!.cancel(); - } - _timer = Timer(const Duration(seconds: 1), () async { - try { - late bool response; - if (isBatch) { - response = await DevicesManagementApi() - .deviceBatchControl(deviceId, code, value); - } else { - response = await DevicesManagementApi() - .deviceControl(deviceId, Status(code: code, value: value)); - } - - if (!response) { - add(WallSensorFetchStatusEvent()); - } - } catch (_) { - await Future.delayed(const Duration(milliseconds: 500)); - add(WallSensorFetchStatusEvent()); - } - }); - } - - FutureOr _getDeviceReports( - GetDeviceReportsEvent event, Emitter emit) async { - emit(DeviceReportsLoadingState()); - // final from = DateTime.now().subtract(const Duration(days: 30)).millisecondsSinceEpoch; - // final to = DateTime.now().millisecondsSinceEpoch; try { - // await DevicesManagementApi.getDeviceReportsByDate( - // deviceId, event.code, from.toString(), to.toString()) - await DevicesManagementApi.getDeviceReports(deviceId, event.code) - .then((value) { - emit(DeviceReportsState(deviceReport: value, code: event.code)); - }); + await batchControlDevicesService.batchControlDevices( + uuids: event.deviceIds, + code: event.code, + value: event.value, + ); + } catch (e) { + _updateLocalValue(event.code, !event.value); + emit(WallSensorFailedState(error: e.toString())); + } + } + + Future _getDeviceReports( + GetDeviceReportsEvent event, + Emitter emit, + ) async { + emit(DeviceReportsLoadingState()); + try { + final reports = await DevicesManagementApi.getDeviceReports( + deviceId, + event.code, + ); + emit(DeviceReportsState(deviceReport: reports, code: event.code)); } catch (e) { emit(DeviceReportsFailedState(error: e.toString())); - return; } } void _showDescription( - ShowDescriptionEvent event, Emitter emit) { + ShowDescriptionEvent event, + Emitter emit, + ) { emit(WallSensorShowDescriptionState(description: event.description)); } void _backToGridView( - BackToGridViewEvent event, Emitter emit) { + BackToGridViewEvent event, + Emitter emit, + ) { emit(WallSensorUpdateState(wallSensorModel: deviceStatus)); } - FutureOr _onFactoryReset( - WallSensorFactoryResetEvent event, Emitter emit) async { + Future _onFactoryReset( + WallSensorFactoryResetEvent event, + Emitter emit, + ) async { emit(WallSensorLoadingNewSate(wallSensorModel: deviceStatus)); try { final response = await DevicesManagementApi().factoryReset( @@ -187,9 +170,9 @@ class WallSensorBloc extends Bloc { event.deviceId, ); if (!response) { - emit(const WallSensorFailedState(error: 'Failed')); + emit(const WallSensorFailedState(error: 'Failed to reset device')); } else { - emit(WallSensorUpdateState(wallSensorModel: deviceStatus)); + add(WallSensorFetchStatusEvent()); } } catch (e) { emit(WallSensorFailedState(error: e.toString())); @@ -200,7 +183,23 @@ class WallSensorBloc extends Bloc { WallSensorRealtimeUpdateEvent event, Emitter emit, ) { - deviceStatus = event.deviceStatus; - emit(WallSensorUpdateState(wallSensorModel: deviceStatus)); + emit(WallSensorUpdateState(wallSensorModel: event.deviceStatus)); + } + + void _updateLocalValue(String code, dynamic value) { + switch (code) { + case 'far_detection': + deviceStatus.farDetection = value; + break; + case 'motionless_sensitivity': + deviceStatus.motionlessSensitivity = value; + break; + case 'motion_sensitivity_value': + deviceStatus.motionSensitivity = value; + break; + case 'no_one_time': + deviceStatus.noBodyTime = value; + break; + } } } diff --git a/lib/pages/device_managment/wall_sensor/factories/wall_sensor_bloc_factory.dart b/lib/pages/device_managment/wall_sensor/factories/wall_sensor_bloc_factory.dart new file mode 100644 index 00000000..d7811717 --- /dev/null +++ b/lib/pages/device_managment/wall_sensor/factories/wall_sensor_bloc_factory.dart @@ -0,0 +1,18 @@ +import 'package:syncrow_web/pages/device_managment/factories/device_bloc_dependencies_factory.dart'; +import 'package:syncrow_web/pages/device_managment/wall_sensor/bloc/wall_bloc.dart'; + +abstract final class WallSensorBlocFactory { + const WallSensorBlocFactory._(); + + static WallSensorBloc create({ + required String deviceId, + }) { + return WallSensorBloc( + deviceId: deviceId, + controlDeviceService: + DeviceBlocDependenciesFactory.createControlDeviceService(), + batchControlDevicesService: + DeviceBlocDependenciesFactory.createBatchControlDevicesService(), + ); + } +} diff --git a/lib/pages/device_managment/wall_sensor/view/wall_sensor_batch_control.dart b/lib/pages/device_managment/wall_sensor/view/wall_sensor_batch_control.dart index 27169f0e..61108387 100644 --- a/lib/pages/device_managment/wall_sensor/view/wall_sensor_batch_control.dart +++ b/lib/pages/device_managment/wall_sensor/view/wall_sensor_batch_control.dart @@ -7,6 +7,7 @@ import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presen import 'package:syncrow_web/pages/device_managment/wall_sensor/bloc/wall_bloc.dart'; import 'package:syncrow_web/pages/device_managment/wall_sensor/bloc/wall_event.dart'; import 'package:syncrow_web/pages/device_managment/wall_sensor/bloc/wall_state.dart'; +import 'package:syncrow_web/pages/device_managment/wall_sensor/factories/wall_sensor_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/wall_sensor/model/wall_sensor_model.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; @@ -21,7 +22,7 @@ class WallSensorBatchControlView extends StatelessWidget with HelperResponsiveLa final isLarge = isLargeScreenSize(context); final isMedium = isMediumScreenSize(context); return BlocProvider( - create: (context) => WallSensorBloc(deviceId: devicesIds.first) + create: (context) => WallSensorBlocFactory.create(deviceId: devicesIds.first) ..add(WallSensorFetchBatchStatusEvent(devicesIds)), child: BlocBuilder( builder: (context, state) { diff --git a/lib/pages/device_managment/wall_sensor/view/wall_sensor_conrtols.dart b/lib/pages/device_managment/wall_sensor/view/wall_sensor_conrtols.dart index 370edaa5..def8ed93 100644 --- a/lib/pages/device_managment/wall_sensor/view/wall_sensor_conrtols.dart +++ b/lib/pages/device_managment/wall_sensor/view/wall_sensor_conrtols.dart @@ -10,6 +10,7 @@ import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presen import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presence_static_widget.dart'; import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presence_status.dart'; import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presence_update_data.dart'; +import 'package:syncrow_web/pages/device_managment/wall_sensor/factories/wall_sensor_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/wall_sensor/model/wall_sensor_model.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; @@ -26,7 +27,7 @@ class WallSensorControlsView extends StatelessWidget with HelperResponsiveLayout final isMedium = isMediumScreenSize(context); return BlocProvider( create: (context) => - WallSensorBloc(deviceId: device.uuid!)..add(WallSensorFetchStatusEvent()), + WallSensorBlocFactory.create(deviceId: device.uuid!)..add(WallSensorFetchStatusEvent()), child: BlocBuilder( builder: (context, state) { if (state is WallSensorLoadingInitialState || state is DeviceReportsLoadingState) { diff --git a/lib/pages/device_managment/water_heater/bloc/water_heater_bloc.dart b/lib/pages/device_managment/water_heater/bloc/water_heater_bloc.dart index 18a0787f..560a61e1 100644 --- a/lib/pages/device_managment/water_heater/bloc/water_heater_bloc.dart +++ b/lib/pages/device_managment/water_heater/bloc/water_heater_bloc.dart @@ -1,5 +1,3 @@ -// water_heater_bloc.dart - import 'dart:async'; import 'package:bloc/bloc.dart'; @@ -10,6 +8,8 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/device_sta import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_entry.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_model.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; +import 'package:syncrow_web/services/batch_control_devices_service.dart'; +import 'package:syncrow_web/services/control_device_service.dart'; import 'package:syncrow_web/services/devices_mang_api.dart'; import 'package:syncrow_web/utils/format_date_time.dart'; @@ -17,7 +17,17 @@ part 'water_heater_event.dart'; part 'water_heater_state.dart'; class WaterHeaterBloc extends Bloc { - WaterHeaterBloc() : super(WaterHeaterInitial()) { + late WaterHeaterStatusModel deviceStatus; + final String deviceId; + final ControlDeviceService controlDeviceService; + final BatchControlDevicesService batchControlDevicesService; + Timer? _countdownTimer; + + WaterHeaterBloc({ + required this.deviceId, + required this.controlDeviceService, + required this.batchControlDevicesService, + }) : super(WaterHeaterInitial()) { on(_fetchWaterHeaterStatus); on(_controlWaterHeater); on(_batchFetchWaterHeater); @@ -29,7 +39,6 @@ class WaterHeaterBloc extends Bloc { on(_updateSelectedTime); on(_updateSelectedDay); on(_updateFunctionOn); - on(_getSchedule); on(_onAddSchedule); on(_onEditSchedule); @@ -38,11 +47,7 @@ class WaterHeaterBloc extends Bloc { on(_onStatusUpdated); } - late WaterHeaterStatusModel deviceStatus; - Timer? _countdownTimer; - // Timer? _inchingTimer; - - FutureOr _initializeAddSchedule( + void _initializeAddSchedule( InitializeAddScheduleEvent event, Emitter emit, ) { @@ -64,7 +69,7 @@ class WaterHeaterBloc extends Bloc { } } - FutureOr _updateSelectedTime( + void _updateSelectedTime( UpdateSelectedTimeEvent event, Emitter emit, ) { @@ -73,7 +78,7 @@ class WaterHeaterBloc extends Bloc { emit(currentState.copyWith(selectedTime: event.selectedTime)); } - FutureOr _updateSelectedDay( + void _updateSelectedDay( UpdateSelectedDayEvent event, Emitter emit, ) { @@ -84,7 +89,7 @@ class WaterHeaterBloc extends Bloc { selectedDays: updatedDays, selectedTime: currentState.selectedTime)); } - FutureOr _updateFunctionOn( + void _updateFunctionOn( UpdateFunctionOnEvent event, Emitter emit, ) { @@ -93,16 +98,18 @@ class WaterHeaterBloc extends Bloc { functionOn: event.isOn, selectedTime: currentState.selectedTime)); } - FutureOr _updateScheduleEvent( + Future _updateScheduleEvent( UpdateScheduleEvent event, Emitter emit, ) async { final currentState = state; if (currentState is WaterHeaterDeviceStatusLoaded) { if (event.scheduleMode == ScheduleModes.schedule) { - emit(currentState.copyWith( - scheduleMode: ScheduleModes.schedule, - )); + emit( + currentState.copyWith( + scheduleMode: ScheduleModes.schedule, + ), + ); } if (event.scheduleMode == ScheduleModes.countdown) { final countdownRemaining = @@ -116,87 +123,88 @@ class WaterHeaterBloc extends Bloc { countdownRemaining: countdownRemaining, )); - if (!currentState.isCountdownActive! && - countdownRemaining > Duration.zero) { + if (!currentState.isCountdownActive! && countdownRemaining > Duration.zero) { _startCountdownTimer(emit, countdownRemaining); } } else if (event.scheduleMode == ScheduleModes.inching) { - final inchingDuration = - Duration(hours: event.hours, minutes: event.minutes); + final inchingDuration = Duration(hours: event.hours, minutes: event.minutes); - emit(currentState.copyWith( - scheduleMode: ScheduleModes.inching, - inchingHours: inchingDuration.inHours, - inchingMinutes: inchingDuration.inMinutes % 60, - isInchingActive: currentState.isInchingActive, - )); + emit( + currentState.copyWith( + scheduleMode: ScheduleModes.inching, + inchingHours: inchingDuration.inHours, + inchingMinutes: inchingDuration.inMinutes % 60, + isInchingActive: currentState.isInchingActive, + ), + ); } } } - FutureOr _controlWaterHeater( + Future _controlWaterHeater( ToggleWaterHeaterEvent event, Emitter emit, ) async { if (state is WaterHeaterDeviceStatusLoaded) { final currentState = state as WaterHeaterDeviceStatusLoaded; - final oldValue = _getValueByCode(event.code); - _updateLocalValue(event.code, event.value); - emit(currentState.copyWith( - status: deviceStatus, - )); + emit( + currentState.copyWith( + status: deviceStatus, + ), + ); - final success = await _runDebounce( - deviceId: event.deviceId, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - isBatch: false, + final success = await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status( + code: event.code, + value: event.value, + ), ); if (success) { if (event.code == "countdown_1") { final countdownDuration = Duration(seconds: event.value); - emit(currentState.copyWith( - countdownHours: countdownDuration.inHours, - countdownMinutes: countdownDuration.inMinutes % 60, - countdownRemaining: countdownDuration, - isCountdownActive: true, - )); + emit( + currentState.copyWith( + countdownHours: countdownDuration.inHours, + countdownMinutes: countdownDuration.inMinutes % 60, + countdownRemaining: countdownDuration, + isCountdownActive: true, + ), + ); if (countdownDuration.inSeconds > 0) { _startCountdownTimer(emit, countdownDuration); } else { _countdownTimer?.cancel(); - emit(currentState.copyWith( - countdownHours: 0, - countdownMinutes: 0, - countdownRemaining: Duration.zero, - isCountdownActive: false, - )); + emit( + currentState.copyWith( + countdownHours: 0, + countdownMinutes: 0, + countdownRemaining: Duration.zero, + isCountdownActive: false, + ), + ); } } else if (event.code == "switch_inching") { final inchingDuration = Duration(seconds: event.value); - //if (inchingDuration.inSeconds > 0) { - // _startInchingTimer(emit, inchingDuration); - // } else { - emit(currentState.copyWith( - inchingHours: inchingDuration.inHours, - inchingMinutes: inchingDuration.inMinutes % 60, - isInchingActive: true, - )); - // } + emit( + currentState.copyWith( + inchingHours: inchingDuration.inHours, + inchingMinutes: inchingDuration.inMinutes % 60, + isInchingActive: true, + ), + ); } } } } - FutureOr _stopScheduleEvent( + Future _stopScheduleEvent( StopScheduleEvent event, Emitter emit, ) async { @@ -207,25 +215,28 @@ class WaterHeaterBloc extends Bloc { _countdownTimer?.cancel(); if (isCountDown) { - emit(currentState.copyWith( - countdownHours: 0, - countdownMinutes: 0, - countdownRemaining: Duration.zero, - isCountdownActive: false, - )); + emit( + currentState.copyWith( + countdownHours: 0, + countdownMinutes: 0, + countdownRemaining: Duration.zero, + isCountdownActive: false, + ), + ); } else if (currentState.scheduleMode == ScheduleModes.inching) { - emit(currentState.copyWith( - inchingHours: 0, - inchingMinutes: 0, - isInchingActive: false, - )); + emit( + currentState.copyWith( + inchingHours: 0, + inchingMinutes: 0, + isInchingActive: false, + ), + ); } try { final status = await DevicesManagementApi().deviceControl( event.deviceId, - Status( - code: isCountDown ? 'countdown_1' : 'switch_inching', value: 0), + Status(code: isCountDown ? 'countdown_1' : 'switch_inching', value: 0), ); if (!status) { emit(const WaterHeaterFailedState(error: 'Failed to stop schedule.')); @@ -236,17 +247,15 @@ class WaterHeaterBloc extends Bloc { } } - FutureOr _fetchWaterHeaterStatus( + Future _fetchWaterHeaterStatus( WaterHeaterFetchStatusEvent event, Emitter emit, ) async { emit(WaterHeaterLoadingState()); try { - final status = - await DevicesManagementApi().getDeviceStatus(event.deviceId); - deviceStatus = - WaterHeaterStatusModel.fromJson(event.deviceId, status.status); + final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); + deviceStatus = WaterHeaterStatusModel.fromJson(event.deviceId, status.status); if (deviceStatus.scheduleMode == ScheduleModes.countdown) { final countdownRemaining = Duration( @@ -288,7 +297,6 @@ class WaterHeaterBloc extends Bloc { inchingMinutes: deviceStatus.inchingMinutes, isInchingActive: true, )); -//_startInchingTimer(emit, inchingDuration); } else { emit(WaterHeaterDeviceStatusLoaded( deviceStatus, @@ -316,7 +324,7 @@ class WaterHeaterBloc extends Bloc { } } - _listenToChanges(deviceId) { + void _listenToChanges(deviceId) { try { DatabaseReference ref = FirebaseDatabase.instance.ref('device-status/$deviceId'); @@ -328,12 +336,11 @@ class WaterHeaterBloc extends Bloc { List statusList = []; usersMap['status'].forEach((element) { - statusList - .add(Status(code: element['code'], value: element['value'])); + statusList.add(Status(code: element['code'], value: element['value'])); }); - deviceStatus = WaterHeaterStatusModel.fromJson( - usersMap['productUuid'], statusList); + deviceStatus = + WaterHeaterStatusModel.fromJson(usersMap['productUuid'], statusList); if (!isClosed) { add(StatusUpdated(deviceStatus)); } @@ -341,7 +348,10 @@ class WaterHeaterBloc extends Bloc { } catch (_) {} } - void _onStatusUpdated(StatusUpdated event, Emitter emit) { + void _onStatusUpdated( + StatusUpdated event, + Emitter emit, + ) { deviceStatus = event.deviceStatus; emit(WaterHeaterDeviceStatusLoaded(deviceStatus)); } @@ -352,23 +362,13 @@ class WaterHeaterBloc extends Bloc { ) { _countdownTimer?.cancel(); - _countdownTimer = Timer.periodic(const Duration(minutes: 1), (timer) { - add(DecrementCountdownEvent()); - }); + _countdownTimer = Timer.periodic( + const Duration(minutes: 1), + (timer) => add(DecrementCountdownEvent()), + ); } - // void _startInchingTimer( - // Emitter emit, - // Duration inchingDuration, - // ) { - // _inchingTimer?.cancel(); - - // _inchingTimer = Timer.periodic(const Duration(minutes: 1), (timer) { - // add(DecrementInchingEvent()); - // }); - // } - - _onDecrementCountdown( + void _onDecrementCountdown( DecrementCountdownEvent event, Emitter emit, ) { @@ -382,105 +382,29 @@ class WaterHeaterBloc extends Bloc { if (newRemaining <= Duration.zero) { _countdownTimer?.cancel(); - emit(currentState.copyWith( - countdownHours: 0, - countdownMinutes: 0, - isCountdownActive: false, - countdownRemaining: Duration.zero, - )); + emit( + currentState.copyWith( + countdownHours: 0, + countdownMinutes: 0, + isCountdownActive: false, + countdownRemaining: Duration.zero, + ), + ); return; } - int totalSeconds = newRemaining.inSeconds; + final totalSeconds = newRemaining.inSeconds; + final newHours = totalSeconds ~/ 3600; + final newMinutes = (totalSeconds % 3600) ~/ 60; - int newHours = totalSeconds ~/ 3600; - int newMinutes = (totalSeconds % 3600) ~/ 60; - - emit(currentState.copyWith( - countdownHours: newHours, - countdownMinutes: newMinutes, - countdownRemaining: newRemaining, - )); - } - } - } - - // FutureOr _onDecrementInching( - // DecrementInchingEvent event, - // Emitter emit, - // ) { - // if (state is WaterHeaterDeviceStatusLoaded) { - // final currentState = state as WaterHeaterDeviceStatusLoaded; - - // if (currentState.inchingHours > 0 || currentState.inchingMinutes > 0) { - // final newRemaining = Duration( - // hours: currentState.inchingHours, - // minutes: currentState.inchingMinutes, - // ) - - // const Duration(minutes: 1); - - // if (newRemaining <= Duration.zero) { - // _inchingTimer?.cancel(); - // emit(currentState.copyWith( - // inchingHours: 0, - // inchingMinutes: 0, - // isInchingActive: false, - // )); - // } else { - // emit(currentState.copyWith( - // inchingHours: newRemaining.inHours, - // inchingMinutes: newRemaining.inMinutes % 60, - // )); - // } - // } - // } - // } - - Future _runDebounce({ - required dynamic deviceId, - required String code, - required dynamic value, - required dynamic oldValue, - required Emitter emit, - required bool isBatch, - }) async { - try { - late bool status; - await Future.delayed(const Duration(milliseconds: 500)); - - if (isBatch) { - status = await DevicesManagementApi().deviceBatchControl( - deviceId, - code, - value, - ); - } else { - status = await DevicesManagementApi().deviceControl( - deviceId, - Status(code: code, value: value), + emit( + currentState.copyWith( + countdownHours: newHours, + countdownMinutes: newMinutes, + countdownRemaining: newRemaining, + ), ); } - - if (!status) { - _revertValue(code, oldValue, emit.call); - return false; - } else { - return true; - } - } catch (e) { - _revertValue(code, oldValue, emit.call); - return false; - } - } - - void _revertValue(String code, dynamic oldValue, - void Function(WaterHeaterState state) emit) { - _updateLocalValue(code, oldValue); - if (state is WaterHeaterDeviceStatusLoaded) { - final currentState = state as WaterHeaterDeviceStatusLoaded; - emit(currentState.copyWith( - status: deviceStatus, - )); } } @@ -505,14 +429,12 @@ class WaterHeaterBloc extends Bloc { } dynamic _getValueByCode(String code) { - switch (code) { - case 'switch_1': - return deviceStatus.heaterSwitch; - case 'countdown_1': - return deviceStatus.countdownHours * 60 + deviceStatus.countdownMinutes; - default: - return null; - } + return switch (code) { + 'switch_1' => deviceStatus.heaterSwitch, + 'countdown_1' => + (deviceStatus.countdownHours * 60) + deviceStatus.countdownMinutes, + _ => null, + }; } @override @@ -521,13 +443,17 @@ class WaterHeaterBloc extends Bloc { return super.close(); } - FutureOr _getSchedule( - GetSchedulesEvent event, Emitter emit) async { + Future _getSchedule( + GetSchedulesEvent event, + Emitter emit, + ) async { emit(ScheduleLoadingState()); try { - List schedules = await DevicesManagementApi() - .getDeviceSchedules(deviceStatus.uuid, event.category); + final schedules = await DevicesManagementApi().getDeviceSchedules( + deviceStatus.uuid, + event.category, + ); emit(WaterHeaterDeviceStatusLoaded( deviceStatus, @@ -535,7 +461,6 @@ class WaterHeaterBloc extends Bloc { scheduleMode: ScheduleModes.schedule, )); } catch (e) { - //(const WaterHeaterFailedState(error: 'Failed to fetch schedules.')); emit(WaterHeaterDeviceStatusLoaded( deviceStatus, schedules: const [], @@ -543,7 +468,7 @@ class WaterHeaterBloc extends Bloc { } } - FutureOr _onAddSchedule( + Future _onAddSchedule( AddScheduleEvent event, Emitter emit, ) async { @@ -557,8 +482,6 @@ class WaterHeaterBloc extends Bloc { days: ScheduleModel.convertSelectedDaysToStrings(event.selectedDays), ); - // emit(ScheduleLoadingState()); - bool success = await DevicesManagementApi() .addScheduleRecord(newSchedule, currentState.status.uuid); @@ -566,13 +489,14 @@ class WaterHeaterBloc extends Bloc { add(GetSchedulesEvent(category: 'switch_1', uuid: deviceStatus.uuid)); } else { emit(currentState); - //emit(const WaterHeaterFailedState(error: 'Failed to add schedule.')); } } } - FutureOr _onEditSchedule(EditWaterHeaterScheduleEvent event, - Emitter emit) async { + Future _onEditSchedule( + EditWaterHeaterScheduleEvent event, + Emitter emit, + ) async { if (state is WaterHeaterDeviceStatusLoaded) { final currentState = state as WaterHeaterDeviceStatusLoaded; @@ -584,8 +508,6 @@ class WaterHeaterBloc extends Bloc { days: ScheduleModel.convertSelectedDaysToStrings(event.selectedDays), ); - // emit(ScheduleLoadingState()); - bool success = await DevicesManagementApi().editScheduleRecord( currentState.status.uuid, newSchedule, @@ -595,12 +517,11 @@ class WaterHeaterBloc extends Bloc { add(GetSchedulesEvent(category: 'switch_1', uuid: deviceStatus.uuid)); } else { emit(currentState); - //emit(const WaterHeaterFailedState(error: 'Failed to add schedule.')); } } } - FutureOr _onUpdateSchedule( + Future _onUpdateSchedule( UpdateScheduleEntryEvent event, Emitter emit, ) async { @@ -627,20 +548,17 @@ class WaterHeaterBloc extends Bloc { emit(currentState.copyWith(schedules: updatedSchedules)); } else { emit(currentState); - // emit(const WaterHeaterFailedState(error: 'Failed to update schedule.')); } } } - FutureOr _onDeleteSchedule( + Future _onDeleteSchedule( DeleteScheduleEvent event, Emitter emit, ) async { if (state is WaterHeaterDeviceStatusLoaded) { final currentState = state as WaterHeaterDeviceStatusLoaded; - // emit(ScheduleLoadingState()); - bool success = await DevicesManagementApi() .deleteScheduleRecord(currentState.status.uuid, event.scheduleId); @@ -652,20 +570,22 @@ class WaterHeaterBloc extends Bloc { emit(currentState.copyWith(schedules: updatedSchedules)); } else { emit(currentState); - // emit(const WaterHeaterFailedState(error: 'Failed to delete schedule.')); } } } - FutureOr _batchFetchWaterHeater(FetchWaterHeaterBatchStatusEvent event, - Emitter emit) async { + Future _batchFetchWaterHeater( + FetchWaterHeaterBatchStatusEvent event, + Emitter emit, + ) async { emit(WaterHeaterLoadingState()); try { - final status = - await DevicesManagementApi().getBatchStatus(event.devicesUuid); - deviceStatus = WaterHeaterStatusModel.fromJson( - event.devicesUuid.first, status.status); + final status = await DevicesManagementApi().getBatchStatus( + event.devicesUuid, + ); + deviceStatus = + WaterHeaterStatusModel.fromJson(event.devicesUuid.first, status.status); emit(WaterHeaterDeviceStatusLoaded(deviceStatus)); } catch (e) { @@ -673,8 +593,8 @@ class WaterHeaterBloc extends Bloc { } } - FutureOr _batchControlWaterHeater(ControlWaterHeaterBatchEvent event, - Emitter emit) async { + Future _batchControlWaterHeater( + ControlWaterHeaterBatchEvent event, Emitter emit) async { if (state is WaterHeaterDeviceStatusLoaded) { final currentState = state as WaterHeaterDeviceStatusLoaded; @@ -686,13 +606,10 @@ class WaterHeaterBloc extends Bloc { status: deviceStatus, )); - final success = await _runDebounce( - deviceId: event.devicesUuid, + final success = await batchControlDevicesService.batchControlDevices( + uuids: event.devicesUuid, code: event.code, value: event.value, - oldValue: oldValue, - emit: emit, - isBatch: true, ); if (success) { diff --git a/lib/pages/device_managment/water_heater/bloc/water_heater_state.dart b/lib/pages/device_managment/water_heater/bloc/water_heater_state.dart index c2df43c3..974f5f2d 100644 --- a/lib/pages/device_managment/water_heater/bloc/water_heater_state.dart +++ b/lib/pages/device_managment/water_heater/bloc/water_heater_state.dart @@ -1,5 +1,3 @@ -// water_heater_state.dart - part of 'water_heater_bloc.dart'; sealed class WaterHeaterState extends Equatable { diff --git a/lib/pages/device_managment/water_heater/factories/water_heater_bloc_factory.dart b/lib/pages/device_managment/water_heater/factories/water_heater_bloc_factory.dart new file mode 100644 index 00000000..9c0c8ab6 --- /dev/null +++ b/lib/pages/device_managment/water_heater/factories/water_heater_bloc_factory.dart @@ -0,0 +1,18 @@ +import 'package:syncrow_web/pages/device_managment/factories/device_bloc_dependencies_factory.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; + +abstract final class WaterHeaterBlocFactory { + const WaterHeaterBlocFactory._(); + + static WaterHeaterBloc create({ + required String deviceId, + }) { + return WaterHeaterBloc( + deviceId: deviceId, + controlDeviceService: + DeviceBlocDependenciesFactory.createControlDeviceService(), + batchControlDevicesService: + DeviceBlocDependenciesFactory.createBatchControlDevicesService(), + ); + } +} diff --git a/lib/pages/device_managment/water_heater/view/water_heater_batch_control.dart b/lib/pages/device_managment/water_heater/view/water_heater_batch_control.dart index aaab5271..3c8a3858 100644 --- a/lib/pages/device_managment/water_heater/view/water_heater_batch_control.dart +++ b/lib/pages/device_managment/water_heater/view/water_heater_batch_control.dart @@ -1,15 +1,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; - import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; // import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/factories/water_heater_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; -class WaterHEaterBatchControlView extends StatelessWidget with HelperResponsiveLayout { +class WaterHEaterBatchControlView extends StatelessWidget + with HelperResponsiveLayout { const WaterHEaterBatchControlView({super.key, required this.deviceIds}); final List deviceIds; @@ -17,8 +18,9 @@ class WaterHEaterBatchControlView extends StatelessWidget with HelperResponsiveL @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => - WaterHeaterBloc()..add(FetchWaterHeaterBatchStatusEvent(devicesUuid: deviceIds)), + create: (context) => WaterHeaterBlocFactory.create( + deviceId: deviceIds.first, + )..add(FetchWaterHeaterBatchStatusEvent(devicesUuid: deviceIds)), child: BlocBuilder( builder: (context, state) { if (state is WaterHeaterLoadingState) { diff --git a/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart b/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart index 40d3edb5..f1e56136 100644 --- a/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart +++ b/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart @@ -5,6 +5,7 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_mo import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/factories/water_heater_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/widgets/schedual_view.dart'; import 'package:syncrow_web/utils/color_manager.dart'; @@ -21,8 +22,9 @@ class WaterHeaterDeviceControlView extends StatelessWidget @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => - WaterHeaterBloc()..add(WaterHeaterFetchStatusEvent(device.uuid!)), + create: (context) => WaterHeaterBlocFactory.create( + deviceId: device.uuid ?? '', + )..add(WaterHeaterFetchStatusEvent(device.uuid!)), child: BlocBuilder( builder: (context, state) { if (state is WaterHeaterLoadingState) { @@ -33,8 +35,7 @@ class WaterHeaterDeviceControlView extends StatelessWidget state is WaterHeaterBatchFailedState) { return const Center(child: Text('Error fetching status')); } else { - return const SizedBox( - height: 200, child: Center(child: SizedBox())); + return const SizedBox(height: 200, child: Center(child: SizedBox())); } }, )); diff --git a/lib/pages/routines/bloc/routine_bloc/routine_bloc.dart b/lib/pages/routines/bloc/routine_bloc/routine_bloc.dart index 760702d4..ca8aac06 100644 --- a/lib/pages/routines/bloc/routine_bloc/routine_bloc.dart +++ b/lib/pages/routines/bloc/routine_bloc/routine_bloc.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; +import 'package:dio/dio.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/common/bloc/project_manager.dart'; @@ -15,6 +16,7 @@ import 'package:syncrow_web/pages/routines/models/device_functions.dart'; import 'package:syncrow_web/pages/routines/models/routine_details_model.dart'; import 'package:syncrow_web/pages/routines/models/routine_model.dart'; import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; import 'package:syncrow_web/services/devices_mang_api.dart'; import 'package:syncrow_web/services/routines_api.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; @@ -64,7 +66,8 @@ class RoutineBloc extends Bloc { TriggerSwitchTabsEvent event, Emitter emit, ) { - emit(state.copyWith(routineTab: event.isRoutineTab, createRoutineView: false)); + emit(state.copyWith( + routineTab: event.isRoutineTab, createRoutineView: false)); add(ResetRoutineState()); if (event.isRoutineTab) { add(const LoadScenes()); @@ -90,8 +93,8 @@ class RoutineBloc extends Bloc { final updatedIfItems = List>.from(state.ifItems); // Find the index of the item in teh current itemsList - int index = - updatedIfItems.indexWhere((map) => map['uniqueCustomId'] == event.item['uniqueCustomId']); + int index = updatedIfItems.indexWhere( + (map) => map['uniqueCustomId'] == event.item['uniqueCustomId']); // Replace the map if the index is valid if (index != -1) { updatedIfItems[index] = event.item; @@ -100,18 +103,21 @@ class RoutineBloc extends Bloc { } if (event.isTabToRun) { - emit(state.copyWith(ifItems: updatedIfItems, isTabToRun: true, isAutomation: false)); + emit(state.copyWith( + ifItems: updatedIfItems, isTabToRun: true, isAutomation: false)); } else { - emit(state.copyWith(ifItems: updatedIfItems, isTabToRun: false, isAutomation: true)); + emit(state.copyWith( + ifItems: updatedIfItems, isTabToRun: false, isAutomation: true)); } } - void _onAddToThenContainer(AddToThenContainer event, Emitter emit) { + void _onAddToThenContainer( + AddToThenContainer event, Emitter emit) { final currentItems = List>.from(state.thenItems); // Find the index of the item in teh current itemsList - int index = - currentItems.indexWhere((map) => map['uniqueCustomId'] == event.item['uniqueCustomId']); + int index = currentItems.indexWhere( + (map) => map['uniqueCustomId'] == event.item['uniqueCustomId']); // Replace the map if the index is valid if (index != -1) { currentItems[index] = event.item; @@ -122,7 +128,8 @@ class RoutineBloc extends Bloc { emit(state.copyWith(thenItems: currentItems)); } - void _onAddFunctionsToRoutine(AddFunctionToRoutine event, Emitter emit) { + void _onAddFunctionsToRoutine( + AddFunctionToRoutine event, Emitter emit) { try { if (event.functions.isEmpty) return; @@ -157,7 +164,8 @@ class RoutineBloc extends Bloc { // currentSelectedFunctions[event.uniqueCustomId] = List.from(event.functions); // } - currentSelectedFunctions[event.uniqueCustomId] = List.from(event.functions); + currentSelectedFunctions[event.uniqueCustomId] = + List.from(event.functions); emit(state.copyWith(selectedFunctions: currentSelectedFunctions)); } catch (e) { @@ -165,24 +173,30 @@ class RoutineBloc extends Bloc { } } - Future _onLoadScenes(LoadScenes event, Emitter emit) async { + Future _onLoadScenes( + LoadScenes event, Emitter emit) async { emit(state.copyWith(isLoading: true, errorMessage: null)); List scenes = []; try { BuildContext context = NavigationService.navigatorKey.currentContext!; var createRoutineBloc = context.read(); final projectUuid = await ProjectManager.getProjectUUID() ?? ''; - if (createRoutineBloc.selectedSpaceId == '' && createRoutineBloc.selectedCommunityId == '') { + if (createRoutineBloc.selectedSpaceId == '' && + createRoutineBloc.selectedCommunityId == '') { var spaceBloc = context.read(); for (var communityId in spaceBloc.state.selectedCommunities) { - List spacesList = spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? []; + List spacesList = + spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? []; for (var spaceId in spacesList) { - scenes.addAll(await SceneApi.getScenes(spaceId, communityId, projectUuid)); + scenes.addAll( + await SceneApi.getScenes(spaceId, communityId, projectUuid)); } } } else { scenes.addAll(await SceneApi.getScenes( - createRoutineBloc.selectedSpaceId, createRoutineBloc.selectedCommunityId, projectUuid)); + createRoutineBloc.selectedSpaceId, + createRoutineBloc.selectedCommunityId, + projectUuid)); } emit(state.copyWith( @@ -199,7 +213,8 @@ class RoutineBloc extends Bloc { } } - Future _onLoadAutomation(LoadAutomation event, Emitter emit) async { + Future _onLoadAutomation( + LoadAutomation event, Emitter emit) async { emit(state.copyWith(isLoading: true, errorMessage: null)); List automations = []; final projectId = await ProjectManager.getProjectUUID() ?? ''; @@ -207,17 +222,22 @@ class RoutineBloc extends Bloc { BuildContext context = NavigationService.navigatorKey.currentContext!; var createRoutineBloc = context.read(); try { - if (createRoutineBloc.selectedSpaceId == '' && createRoutineBloc.selectedCommunityId == '') { + if (createRoutineBloc.selectedSpaceId == '' && + createRoutineBloc.selectedCommunityId == '') { var spaceBloc = context.read(); for (var communityId in spaceBloc.state.selectedCommunities) { - List spacesList = spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? []; + List spacesList = + spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? []; for (var spaceId in spacesList) { - automations.addAll(await SceneApi.getAutomation(spaceId, communityId, projectId)); + automations.addAll( + await SceneApi.getAutomation(spaceId, communityId, projectId)); } } } else { automations.addAll(await SceneApi.getAutomation( - createRoutineBloc.selectedSpaceId, createRoutineBloc.selectedCommunityId, projectId)); + createRoutineBloc.selectedSpaceId, + createRoutineBloc.selectedCommunityId, + projectId)); } emit(state.copyWith( automations: automations, @@ -233,14 +253,16 @@ class RoutineBloc extends Bloc { } } - FutureOr _onSearchRoutines(SearchRoutines event, Emitter emit) async { + FutureOr _onSearchRoutines( + SearchRoutines event, Emitter emit) async { emit(state.copyWith(isLoading: true, errorMessage: null)); await Future.delayed(const Duration(seconds: 1)); emit(state.copyWith(isLoading: false, errorMessage: null)); emit(state.copyWith(searchText: event.query)); } - FutureOr _onAddSelectedIcon(AddSelectedIcon event, Emitter emit) { + FutureOr _onAddSelectedIcon( + AddSelectedIcon event, Emitter emit) { emit(state.copyWith(selectedIcon: event.icon)); } @@ -254,7 +276,8 @@ class RoutineBloc extends Bloc { return actions.last['deviceId'] == 'delay'; } - Future _onCreateScene(CreateSceneEvent event, Emitter emit) async { + Future _onCreateScene( + CreateSceneEvent event, Emitter emit) async { try { // Check if first action is delay // if (_isFirstActionDelay(state.thenItems)) { @@ -267,7 +290,8 @@ class RoutineBloc extends Bloc { if (_isLastActionDelay(state.thenItems)) { emit(state.copyWith( - errorMessage: 'A delay condition cannot be the only or the last action', + errorMessage: + 'A delay condition cannot be the only or the last action', isLoading: false, )); return; @@ -335,15 +359,18 @@ class RoutineBloc extends Bloc { errorMessage: 'Something went wrong', )); } - } catch (e) { + } on APIException catch (e) { + final errorData = e.message; + String errorMessage = errorData; emit(state.copyWith( isLoading: false, - errorMessage: 'Something went wrong', + errorMessage: errorMessage, )); } } - Future _onCreateAutomation(CreateAutomationEvent event, Emitter emit) async { + Future _onCreateAutomation( + CreateAutomationEvent event, Emitter emit) async { try { final projectUuid = await ProjectManager.getProjectUUID() ?? ''; if (state.routineName == null || state.routineName!.isEmpty) { @@ -365,7 +392,8 @@ class RoutineBloc extends Bloc { if (_isLastActionDelay(state.thenItems)) { emit(state.copyWith( - errorMessage: 'A delay condition cannot be the only or the last action', + errorMessage: + 'A delay condition cannot be the only or the last action', isLoading: false, )); CustomSnackBar.redSnackBar('Cannot have delay as the last action'); @@ -456,7 +484,8 @@ class RoutineBloc extends Bloc { actions: actions, ); - final result = await SceneApi.createAutomation(createAutomationModel, projectUuid); + final result = + await SceneApi.createAutomation(createAutomationModel, projectUuid); if (result['success']) { add(ResetRoutineState()); add(const LoadAutomation()); @@ -468,26 +497,32 @@ class RoutineBloc extends Bloc { )); CustomSnackBar.redSnackBar('Something went wrong'); } - } catch (e) { + } on APIException catch (e) { + final errorData = e.message; + String errorMessage = errorData; emit(state.copyWith( isLoading: false, - errorMessage: 'Something went wrong', + errorMessage: errorMessage, )); - CustomSnackBar.redSnackBar('Something went wrong'); + CustomSnackBar.redSnackBar(errorMessage); } } - FutureOr _onRemoveDragCard(RemoveDragCard event, Emitter emit) { + FutureOr _onRemoveDragCard( + RemoveDragCard event, Emitter emit) { if (event.isFromThen) { final thenItems = List>.from(state.thenItems); - final selectedFunctions = Map>.from(state.selectedFunctions); + final selectedFunctions = + Map>.from(state.selectedFunctions); thenItems.removeAt(event.index); selectedFunctions.remove(event.key); - emit(state.copyWith(thenItems: thenItems, selectedFunctions: selectedFunctions)); + emit(state.copyWith( + thenItems: thenItems, selectedFunctions: selectedFunctions)); } else { final ifItems = List>.from(state.ifItems); - final selectedFunctions = Map>.from(state.selectedFunctions); + final selectedFunctions = + Map>.from(state.selectedFunctions); ifItems.removeAt(event.index); selectedFunctions.remove(event.key); @@ -498,7 +533,8 @@ class RoutineBloc extends Bloc { isAutomation: false, isTabToRun: false)); } else { - emit(state.copyWith(ifItems: ifItems, selectedFunctions: selectedFunctions)); + emit(state.copyWith( + ifItems: ifItems, selectedFunctions: selectedFunctions)); } } } @@ -510,11 +546,13 @@ class RoutineBloc extends Bloc { )); } - FutureOr _onEffectiveTimeEvent(EffectiveTimePeriodEvent event, Emitter emit) { + FutureOr _onEffectiveTimeEvent( + EffectiveTimePeriodEvent event, Emitter emit) { emit(state.copyWith(effectiveTime: event.effectiveTime)); } - FutureOr _onSetRoutineName(SetRoutineName event, Emitter emit) { + FutureOr _onSetRoutineName( + SetRoutineName event, Emitter emit) { emit(state.copyWith( routineName: event.name, )); @@ -641,7 +679,8 @@ class RoutineBloc extends Bloc { // return (thenItems, ifItems, currentFunctions); // } - Future _onGetSceneDetails(GetSceneDetails event, Emitter emit) async { + Future _onGetSceneDetails( + GetSceneDetails event, Emitter emit) async { try { emit(state.copyWith( isLoading: true, @@ -689,10 +728,12 @@ class RoutineBloc extends Bloc { // if (!deviceCards.containsKey(deviceId)) { deviceCards[deviceId] = { 'entityId': action.entityId, - 'deviceId': action.actionExecutor == 'delay' ? 'delay' : action.entityId, - 'uniqueCustomId': action.type == 'automation' || action.actionExecutor == 'delay' - ? action.entityId - : const Uuid().v4(), + 'deviceId': + action.actionExecutor == 'delay' ? 'delay' : action.entityId, + 'uniqueCustomId': + action.type == 'automation' || action.actionExecutor == 'delay' + ? action.entityId + : const Uuid().v4(), 'title': action.actionExecutor == 'delay' ? 'Delay' : action.type == 'automation' @@ -732,7 +773,8 @@ class RoutineBloc extends Bloc { ), ); // emit(state.copyWith(automationActionExecutor: action.actionExecutor)); - } else if (action.executorProperty != null && action.actionExecutor != 'delay') { + } else if (action.executorProperty != null && + action.actionExecutor != 'delay') { final functions = matchingDevice?.functions ?? []; final functionCode = action.executorProperty?.functionCode; for (DeviceFunction function in functions) { @@ -798,7 +840,8 @@ class RoutineBloc extends Bloc { } } - FutureOr _onResetRoutineState(ResetRoutineState event, Emitter emit) { + FutureOr _onResetRoutineState( + ResetRoutineState event, Emitter emit) { emit(state.copyWith( ifItems: [], thenItems: [], @@ -822,7 +865,8 @@ class RoutineBloc extends Bloc { createRoutineView: false)); } - FutureOr _deleteScene(DeleteScene event, Emitter emit) async { + FutureOr _deleteScene( + DeleteScene event, Emitter emit) async { try { final projectId = await ProjectManager.getProjectUUID() ?? ''; @@ -831,7 +875,8 @@ class RoutineBloc extends Bloc { var spaceBloc = context.read(); if (state.isTabToRun) { await SceneApi.deleteScene( - unitUuid: spaceBloc.state.selectedSpaces[0], sceneId: state.sceneId ?? ''); + unitUuid: spaceBloc.state.selectedSpaces[0], + sceneId: state.sceneId ?? ''); } else { await SceneApi.deleteAutomation( unitUuid: spaceBloc.state.selectedSpaces[0], @@ -854,11 +899,14 @@ class RoutineBloc extends Bloc { add(const LoadAutomation()); add(ResetRoutineState()); emit(state.copyWith(isLoading: false, createRoutineView: false)); - } catch (e) { + } on APIException catch (e) { + final errorData = e.message; + String errorMessage = errorData; emit(state.copyWith( isLoading: false, - errorMessage: 'Failed to delete scene', + errorMessage: errorMessage, )); + CustomSnackBar.redSnackBar(errorMessage); } } @@ -876,7 +924,8 @@ class RoutineBloc extends Bloc { // } // } - FutureOr _fetchDevices(FetchDevicesInRoutine event, Emitter emit) async { + FutureOr _fetchDevices( + FetchDevicesInRoutine event, Emitter emit) async { emit(state.copyWith(isLoading: true)); try { final projectUuid = await ProjectManager.getProjectUUID() ?? ''; @@ -885,17 +934,21 @@ class RoutineBloc extends Bloc { var createRoutineBloc = context.read(); var spaceBloc = context.read(); - if (createRoutineBloc.selectedSpaceId == '' && createRoutineBloc.selectedCommunityId == '') { + if (createRoutineBloc.selectedSpaceId == '' && + createRoutineBloc.selectedCommunityId == '') { for (var communityId in spaceBloc.state.selectedCommunities) { - List spacesList = spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? []; + List spacesList = + spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? []; for (var spaceId in spacesList) { - devices.addAll( - await DevicesManagementApi().fetchDevices(communityId, spaceId, projectUuid)); + devices.addAll(await DevicesManagementApi() + .fetchDevices(communityId, spaceId, projectUuid)); } } } else { devices.addAll(await DevicesManagementApi().fetchDevices( - createRoutineBloc.selectedCommunityId, createRoutineBloc.selectedSpaceId, projectUuid)); + createRoutineBloc.selectedCommunityId, + createRoutineBloc.selectedSpaceId, + projectUuid)); } emit(state.copyWith(isLoading: false, devices: devices)); @@ -904,7 +957,8 @@ class RoutineBloc extends Bloc { } } - FutureOr _onUpdateScene(UpdateScene event, Emitter emit) async { + FutureOr _onUpdateScene( + UpdateScene event, Emitter emit) async { try { // Check if first action is delay // if (_isFirstActionDelay(state.thenItems)) { @@ -918,7 +972,8 @@ class RoutineBloc extends Bloc { if (_isLastActionDelay(state.thenItems)) { emit(state.copyWith( - errorMessage: 'A delay condition cannot be the only or the last action', + errorMessage: + 'A delay condition cannot be the only or the last action', isLoading: false, )); return; @@ -971,7 +1026,8 @@ class RoutineBloc extends Bloc { actions: actions, ); - final result = await SceneApi.updateScene(createSceneModel, state.sceneId ?? ''); + final result = + await SceneApi.updateScene(createSceneModel, state.sceneId ?? ''); if (result['success']) { add(ResetRoutineState()); add(const LoadScenes()); @@ -990,7 +1046,8 @@ class RoutineBloc extends Bloc { } } - FutureOr _onUpdateAutomation(UpdateAutomation event, Emitter emit) async { + FutureOr _onUpdateAutomation( + UpdateAutomation event, Emitter emit) async { try { if (state.routineName == null || state.routineName!.isEmpty) { emit(state.copyWith( @@ -1114,10 +1171,11 @@ class RoutineBloc extends Bloc { errorMessage: result['message'], )); } - } catch (e) { + } on APIException catch (e) { + final errorData = e.message; emit(state.copyWith( isLoading: false, - errorMessage: 'Something went wrong', + errorMessage: errorData, )); } } @@ -1214,7 +1272,8 @@ class RoutineBloc extends Bloc { // if (!deviceThenCards.containsKey(deviceId)) { deviceThenCards[deviceId] = { 'entityId': action.entityId, - 'deviceId': action.actionExecutor == 'delay' ? 'delay' : action.entityId, + 'deviceId': + action.actionExecutor == 'delay' ? 'delay' : action.entityId, 'uniqueCustomId': const Uuid().v4(), 'title': action.actionExecutor == 'delay' ? 'Delay' @@ -1249,7 +1308,8 @@ class RoutineBloc extends Bloc { updatedFunctions[uniqueCustomId] = []; } - if (action.executorProperty != null && action.actionExecutor != 'delay') { + if (action.executorProperty != null && + action.actionExecutor != 'delay') { final functions = matchingDevice.functions; final functionCode = action.executorProperty!.functionCode; for (var function in functions) { @@ -1291,10 +1351,14 @@ class RoutineBloc extends Bloc { } } - final ifItems = deviceIfCards.values.where((card) => card['type'] == 'condition').toList(); + final ifItems = deviceIfCards.values + .where((card) => card['type'] == 'condition') + .toList(); final thenItems = deviceThenCards.values .where((card) => - card['type'] == 'action' || card['type'] == 'automation' || card['type'] == 'scene') + card['type'] == 'action' || + card['type'] == 'automation' || + card['type'] == 'scene') .toList(); emit(state.copyWith( @@ -1316,7 +1380,8 @@ class RoutineBloc extends Bloc { } } - Future _onSceneTrigger(SceneTrigger event, Emitter emit) async { + Future _onSceneTrigger( + SceneTrigger event, Emitter emit) async { emit(state.copyWith(loadingSceneId: event.sceneId)); try { @@ -1358,24 +1423,29 @@ class RoutineBloc extends Bloc { if (success) { final updatedAutomations = await SceneApi.getAutomationByUnitId( - event.automationStatusUpdate.spaceUuid, event.communityId, projectId); + event.automationStatusUpdate.spaceUuid, + event.communityId, + projectId); // Remove from loading set safely - final updatedLoadingIds = {...state.loadingAutomationIds!}..remove(event.automationId); + final updatedLoadingIds = {...state.loadingAutomationIds!} + ..remove(event.automationId); emit(state.copyWith( automations: updatedAutomations, loadingAutomationIds: updatedLoadingIds, )); } else { - final updatedLoadingIds = {...state.loadingAutomationIds!}..remove(event.automationId); + final updatedLoadingIds = {...state.loadingAutomationIds!} + ..remove(event.automationId); emit(state.copyWith( loadingAutomationIds: updatedLoadingIds, errorMessage: 'Update failed', )); } } catch (e) { - final updatedLoadingIds = {...state.loadingAutomationIds!}..remove(event.automationId); + final updatedLoadingIds = {...state.loadingAutomationIds!} + ..remove(event.automationId); emit(state.copyWith( loadingAutomationIds: updatedLoadingIds, errorMessage: 'Update error: ${e.toString()}', diff --git a/lib/pages/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/pages/space_tree/view/space_tree_view.dart b/lib/pages/space_tree/view/space_tree_view.dart index fadcdc0c..c60474f8 100644 --- a/lib/pages/space_tree/view/space_tree_view.dart +++ b/lib/pages/space_tree/view/space_tree_view.dart @@ -48,7 +48,8 @@ class _SpaceTreeViewState extends State { @override Widget build(BuildContext context) { - return BlocBuilder(builder: (context, state) { + return BlocBuilder( + builder: (context, state) { final communities = state.searchQuery.isNotEmpty ? state.filteredCommunity : state.communityList; @@ -132,104 +133,118 @@ class _SpaceTreeViewState extends State { ) else CustomSearchBar( - onSearchChanged: (query) => context.read().add( - SearchQueryEvent(query), - ), + onSearchChanged: (query) => + context.read().add( + SearchQueryEvent(query), + ), ), const SizedBox(height: 16), Expanded( child: state.isSearching ? const Center(child: CircularProgressIndicator()) - : SidebarCommunitiesList( - onScrollToEnd: () { - if (!state.paginationIsLoading) { - context.read().add( - PaginationEvent( - state.paginationModel, - state.communityList, - ), - ); - } - }, - scrollController: _scrollController, - communities: communities, - itemBuilder: (context, index) { - return CustomExpansionTileSpaceTree( - title: communities[index].name, - isSelected: state.selectedCommunities - .contains(communities[index].uuid), - isSoldCheck: state.selectedCommunities - .contains(communities[index].uuid), - onExpansionChanged: () => - context.read().add( - OnCommunityExpanded( - communities[index].uuid, - ), - ), - isExpanded: state.expandedCommunities.contains( - communities[index].uuid, + : communities.isEmpty + ? Center( + child: Text( + 'No communities found', + style: context.textTheme.bodyMedium?.copyWith( + color: ColorsManager.textGray, + ), ), - onItemSelected: () { - widget.onSelect(); - context.read().add( - OnCommunitySelected( - communities[index].uuid, - communities[index].spaces, - ), - ); - }, - children: communities[index].spaces.map( - (space) { - return CustomExpansionTileSpaceTree( - title: space.name, - isExpanded: - state.expandedSpaces.contains(space.uuid), - onItemSelected: () { - final isParentSelected = _isParentSelected( - state, - communities[index], - space, + ) + : SidebarCommunitiesList( + onScrollToEnd: () { + if (!state.paginationIsLoading) { + context.read().add( + PaginationEvent( + state.paginationModel, + state.communityList, + ), ); - if (widget - .shouldDisableDeselectingChildrenOfSelectedParent && - isParentSelected) { - return; - } - widget.onSelect(); + } + }, + scrollController: _scrollController, + communities: communities, + itemBuilder: (context, index) { + return CustomExpansionTileSpaceTree( + title: communities[index].name, + isSelected: state.selectedCommunities + .contains(communities[index].uuid), + isSoldCheck: state.selectedCommunities + .contains(communities[index].uuid), + onExpansionChanged: () => context.read().add( - OnSpaceSelected( - communities[index], - space.uuid ?? '', - space.children, + OnCommunityExpanded( + communities[index].uuid, ), + ), + isExpanded: + state.expandedCommunities.contains( + communities[index].uuid, + ), + onItemSelected: () { + widget.onSelect(); + context.read().add( + OnCommunitySelected( + communities[index].uuid, + communities[index].spaces, + ), + ); + }, + children: communities[index].spaces.map( + (space) { + return CustomExpansionTileSpaceTree( + title: space.name, + isExpanded: state.expandedSpaces + .contains(space.uuid), + onItemSelected: () { + final isParentSelected = + _isParentSelected( + state, + communities[index], + space, ); + if (widget + .shouldDisableDeselectingChildrenOfSelectedParent && + isParentSelected) { + return; + } + widget.onSelect(); + context.read().add( + OnSpaceSelected( + communities[index], + space.uuid ?? '', + space.children, + ), + ); + }, + onExpansionChanged: () => + context.read().add( + OnSpaceExpanded( + communities[index].uuid, + space.uuid ?? '', + ), + ), + isSelected: state.selectedSpaces + .contains(space.uuid) || + state.soldCheck + .contains(space.uuid), + isSoldCheck: state.soldCheck + .contains(space.uuid), + children: _buildNestedSpaces( + context, + state, + space, + communities[index], + ), + ); }, - onExpansionChanged: () => - context.read().add( - OnSpaceExpanded( - communities[index].uuid, - space.uuid ?? '', - ), - ), - isSelected: state.selectedSpaces - .contains(space.uuid) || - state.soldCheck.contains(space.uuid), - isSoldCheck: - state.soldCheck.contains(space.uuid), - children: _buildNestedSpaces( - context, - state, - space, - communities[index], - ), - ); - }, - ).toList(), - ); - }, - ), + ).toList(), + ); + }, + ), ), - if (state.paginationIsLoading) const CircularProgressIndicator(), + if (state.paginationIsLoading) + const CircularProgressIndicator(), ], ), ); diff --git a/lib/services/api/api_exception.dart b/lib/services/api/api_exception.dart new file mode 100644 index 00000000..89d969d3 --- /dev/null +++ b/lib/services/api/api_exception.dart @@ -0,0 +1,10 @@ +class APIException implements Exception { + final String message; + + APIException(this.message); + + @override + String toString() { + return message; + } +} diff --git a/lib/services/auth_api.dart b/lib/services/auth_api.dart index 190eb624..18d951c1 100644 --- a/lib/services/auth_api.dart +++ b/lib/services/auth_api.dart @@ -1,18 +1,26 @@ +import 'package:dio/dio.dart'; import 'package:syncrow_web/pages/auth/model/region_model.dart'; import 'package:syncrow_web/pages/auth/model/token.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/utils/constants/api_const.dart'; class AuthenticationAPI { static Future loginWithEmail({required var model}) async { - final response = await HTTPService().post( - path: ApiEndpoints.login, - body: model.toJson(), - showServerMessage: true, - expectedResponseModel: (json) { - return Token.fromJson(json['data']); - }); - return response; + try { + final response = await HTTPService().post( + path: ApiEndpoints.login, + body: model.toJson(), + showServerMessage: true, + expectedResponseModel: (json) { + return Token.fromJson(json['data']); + }); + return response; + } on DioException catch (e) { + final message = e.response?.data['error']['message'] ?? + 'An error occurred while logging in'; + throw APIException(message); + } } static Future forgetPassword({ @@ -20,12 +28,18 @@ class AuthenticationAPI { required var password, required var otpCode, }) async { - final response = await HTTPService().post( - path: ApiEndpoints.forgetPassword, - body: {"email": email, "password": password, "otpCode": otpCode}, - showServerMessage: true, - expectedResponseModel: (json) {}); - return response; + try { + final response = await HTTPService().post( + path: ApiEndpoints.forgetPassword, + body: {"email": email, "password": password, "otpCode": otpCode}, + showServerMessage: true, + expectedResponseModel: (json) {}); + return response; + } on DioException catch (e) { + final message = e.response?.data['error']['message'] ?? + 'An error occurred while resetting the password'; + throw APIException(message); + } } static Future sendOtp({required String email}) async { @@ -39,19 +53,26 @@ class AuthenticationAPI { return response; } - static Future verifyOtp({required String email, required String otpCode}) async { - final response = await HTTPService().post( - path: ApiEndpoints.verifyOtp, - body: {"email": email, "type": "PASSWORD", "otpCode": otpCode}, - showServerMessage: true, - expectedResponseModel: (json) { - if (json['message'] == 'Otp Verified Successfully') { - return true; - } else { - return false; - } - }); - return response; + static Future verifyOtp( + {required String email, required String otpCode}) async { + try { + final response = await HTTPService().post( + path: ApiEndpoints.verifyOtp, + body: {"email": email, "type": "PASSWORD", "otpCode": otpCode}, + showServerMessage: true, + expectedResponseModel: (json) { + if (json['message'] == 'Otp Verified Successfully') { + return true; + } else { + return false; + } + }); + return response; + } on APIException catch (e) { + throw APIException(e.message); + } catch (e) { + throw APIException('An error occurred while verifying the OTP'); + } } static Future> fetchRegion() async { @@ -59,7 +80,9 @@ class AuthenticationAPI { path: ApiEndpoints.getRegion, showServerMessage: true, expectedResponseModel: (json) { - return (json as List).map((zone) => RegionModel.fromJson(zone)).toList(); + return (json as List) + .map((zone) => RegionModel.fromJson(zone)) + .toList(); }); return response; } diff --git a/lib/services/devices_mang_api.dart b/lib/services/devices_mang_api.dart index b4de6326..6f60e34f 100644 --- a/lib/services/devices_mang_api.dart +++ b/lib/services/devices_mang_api.dart @@ -91,7 +91,8 @@ class DevicesManagementApi { } } - Future deviceBatchControl(List uuids, String code, dynamic value) async { + Future deviceBatchControl( + List uuids, String code, dynamic value) async { try { final body = { 'devicesUuid': uuids, @@ -116,7 +117,8 @@ class DevicesManagementApi { } } - static Future> getDevicesByGatewayId(String gatewayId) async { + static Future> getDevicesByGatewayId( + String gatewayId) async { final response = await HTTPService().get( path: ApiEndpoints.gatewayApi.replaceAll('{gatewayUuid}', gatewayId), showServerMessage: false, @@ -150,7 +152,9 @@ class DevicesManagementApi { String code, ) async { final response = await HTTPService().get( - path: ApiEndpoints.getDeviceLogs.replaceAll('{uuid}', uuid).replaceAll('{code}', code), + path: ApiEndpoints.getDeviceLogs + .replaceAll('{uuid}', uuid) + .replaceAll('{code}', code), showServerMessage: false, expectedResponseModel: (json) { return DeviceReport.fromJson(json['data']); @@ -223,7 +227,8 @@ class DevicesManagementApi { } } - Future addScheduleRecord(ScheduleEntry sendSchedule, String uuid) async { + Future addScheduleRecord( + ScheduleEntry sendSchedule, String uuid) async { try { final response = await HTTPService().post( path: ApiEndpoints.scheduleByDeviceId.replaceAll('{deviceUuid}', uuid), @@ -240,7 +245,8 @@ class DevicesManagementApi { } } - Future> getDeviceSchedules(String uuid, String category) async { + Future> getDeviceSchedules( + String uuid, String category) async { try { final response = await HTTPService().get( path: ApiEndpoints.getScheduleByDeviceId @@ -263,7 +269,9 @@ class DevicesManagementApi { } Future updateScheduleRecord( - {required bool enable, required String uuid, required String scheduleId}) async { + {required bool enable, + required String uuid, + required String scheduleId}) async { try { final response = await HTTPService().put( path: ApiEndpoints.updateScheduleByDeviceId @@ -284,7 +292,8 @@ class DevicesManagementApi { } } - Future editScheduleRecord(String uuid, ScheduleEntry newSchedule) async { + Future editScheduleRecord( + String uuid, ScheduleEntry newSchedule) async { try { final response = await HTTPService().put( path: ApiEndpoints.scheduleByDeviceId.replaceAll('{deviceUuid}', uuid), @@ -335,4 +344,46 @@ class DevicesManagementApi { return false; } } + + static Future> putDeviceName( + {required String deviceId, required String deviceName}) async { + try { + final response = await HTTPService().put( + path: ApiEndpoints.deviceByUuid.replaceAll('{deviceUuid}', deviceId), + body: {"deviceName": deviceName}, + expectedResponseModel: (json) { + return json['data']; + }, + ); + return response; + } catch (e) { + rethrow; + } + } + + static Future getDeviceInfo(String deviceId) async { + final response = await HTTPService().get( + path: ApiEndpoints.deviceByUuid.replaceAll('{deviceUuid}', deviceId), + showServerMessage: false, + expectedResponseModel: (json) { + return json['data'] as Map; + }); + return response; + } + + static Future resetDevice({ + String? devicesUuid, + }) async { + final response = await HTTPService().post( + path: ApiEndpoints.resetDevice.replaceAll('{deviceUuid}', devicesUuid!), + showServerMessage: false, + body: { + "devicesUuid": [devicesUuid] + }, + expectedResponseModel: (json) { + return json; + }, + ); + return response; + } } diff --git a/lib/services/routines_api.dart b/lib/services/routines_api.dart index eaa09e27..bdc46ac1 100644 --- a/lib/services/routines_api.dart +++ b/lib/services/routines_api.dart @@ -1,3 +1,4 @@ +import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/routines/bloc/automation_scene_trigger_bloc/automation_status_update.dart'; import 'package:syncrow_web/pages/routines/models/create_scene_and_autoamtion/create_automation_model.dart'; @@ -5,6 +6,7 @@ import 'package:syncrow_web/pages/routines/models/create_scene_and_autoamtion/cr import 'package:syncrow_web/pages/routines/models/icon_model.dart'; import 'package:syncrow_web/pages/routines/models/routine_details_model.dart'; import 'package:syncrow_web/pages/routines/models/routine_model.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/utils/constants/api_const.dart'; @@ -26,9 +28,10 @@ class SceneApi { ); debugPrint('create scene response: $response'); return response; - } catch (e) { - debugPrint(e.toString()); - rethrow; + } on DioException catch (e) { + String errorMessage = + e.response?.data['error']['message'][0] ?? 'something went wrong'; + throw APIException(errorMessage); } } @@ -48,9 +51,10 @@ class SceneApi { ); debugPrint('create automation response: $response'); return response; - } catch (e) { - debugPrint(e.toString()); - rethrow; + } on DioException catch (e) { + String errorMessage = + e.response?.data['error']['message'][0] ?? 'something went wrong'; + throw APIException(errorMessage); } } @@ -165,8 +169,10 @@ class SceneApi { }, ); return response; - } catch (e) { - rethrow; + } on DioException catch (e) { + String errorMessage = + e.response?.data['error']['message'][0] ?? 'something went wrong'; + throw APIException(errorMessage); } } @@ -185,8 +191,10 @@ class SceneApi { }, ); return response; - } catch (e) { - rethrow; + } on DioException catch (e) { + String errorMessage = + e.response?.data['error']['message'][0] ?? 'something went wrong'; + throw APIException(errorMessage); } } @@ -217,8 +225,10 @@ class SceneApi { expectedResponseModel: (json) => json['statusCode'] == 200, ); return response; - } catch (e) { - rethrow; + } on DioException catch (e) { + String errorMessage = + e.response?.data['error']['message'][0] ?? 'something went wrong'; + throw APIException(errorMessage); } } @@ -236,8 +246,10 @@ class SceneApi { expectedResponseModel: (json) => json['statusCode'] == 200, ); return response; - } catch (e) { - rethrow; + } on DioException catch (e) { + String errorMessage = + e.response?.data['error']['message'][0] ?? 'something went wrong'; + throw APIException(errorMessage); } } diff --git a/lib/services/space_mana_api.dart b/lib/services/space_mana_api.dart index 19e219b6..8f8d1d07 100644 --- a/lib/services/space_mana_api.dart +++ b/lib/services/space_mana_api.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/sub_space_model.dart'; import 'package:syncrow_web/pages/space_tree/model/pagination_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/create_subspace_model.dart'; @@ -12,14 +13,16 @@ import 'package:syncrow_web/utils/constants/api_const.dart'; class CommunitySpaceManagementApi { // Community Management APIs - Future> fetchCommunities(String projectId, {int page = 1}) async { + Future> fetchCommunities(String projectId, + {int page = 1}) async { try { List allCommunities = []; bool hasNext = true; while (hasNext) { await HTTPService().get( - path: ApiEndpoints.getCommunityList.replaceAll('{projectId}', projectId), + path: ApiEndpoints.getCommunityList + .replaceAll('{projectId}', projectId), queryParameters: { 'page': page, }, @@ -55,8 +58,14 @@ class CommunitySpaceManagementApi { try { bool hasNext = false; await HTTPService().get( - path: ApiEndpoints.getCommunityList.replaceAll('{projectId}', projectId), - queryParameters: {'page': page, 'includeSpaces': true, 'size': 25, 'search': search}, + path: + ApiEndpoints.getCommunityList.replaceAll('{projectId}', projectId), + queryParameters: { + 'page': page, + 'includeSpaces': true, + 'size': 25, + 'search': search + }, expectedResponseModel: (json) { try { List jsonData = json['data'] ?? []; @@ -68,7 +77,10 @@ class CommunitySpaceManagementApi { page = currentPage + 1; paginationModel = PaginationModel( - pageNum: page, hasNext: hasNext, size: 25, communities: communityList); + pageNum: page, + hasNext: hasNext, + size: 25, + communities: communityList); return paginationModel; } catch (_) { hasNext = false; @@ -83,7 +95,8 @@ class CommunitySpaceManagementApi { Future getCommunityById(String communityId) async { try { final response = await HTTPService().get( - path: ApiEndpoints.getCommunityById.replaceAll('{communityId}', communityId), + path: ApiEndpoints.getCommunityById + .replaceAll('{communityId}', communityId), expectedResponseModel: (json) { return CommunityModel.fromJson(json['data']); }, @@ -95,7 +108,8 @@ class CommunitySpaceManagementApi { } } - Future createCommunity(String name, String description, String projectId) async { + Future createCommunity( + String name, String description, String projectId) async { try { final response = await HTTPService().post( path: ApiEndpoints.createCommunity.replaceAll('{projectId}', projectId), @@ -114,7 +128,8 @@ class CommunitySpaceManagementApi { } } - Future updateCommunity(String communityId, String name, String projectId) async { + Future updateCommunity( + String communityId, String name, String projectId) async { try { final response = await HTTPService().put( path: ApiEndpoints.updateCommunity @@ -151,7 +166,8 @@ class CommunitySpaceManagementApi { } } - Future fetchSpaces(String communityId, String projectId) async { + Future fetchSpaces( + String communityId, String projectId) async { try { final response = await HTTPService().get( path: ApiEndpoints.listSpaces @@ -177,7 +193,8 @@ class CommunitySpaceManagementApi { } } - Future getSpace(String communityId, String spaceId, String projectId) async { + Future getSpace( + String communityId, String spaceId, String projectId) async { try { final response = await HTTPService().get( path: ApiEndpoints.getSpace @@ -289,7 +306,8 @@ class CommunitySpaceManagementApi { } } - Future deleteSpace(String communityId, String spaceId, String projectId) async { + Future deleteSpace( + String communityId, String spaceId, String projectId) async { try { final response = await HTTPService().delete( path: ApiEndpoints.deleteSpace @@ -307,15 +325,17 @@ class CommunitySpaceManagementApi { } } - Future> getSpaceHierarchy(String communityId, String projectId) async { + Future> getSpaceHierarchy( + String communityId, String projectId) async { try { final response = await HTTPService().get( path: ApiEndpoints.getSpaceHierarchy .replaceAll('{communityId}', communityId) .replaceAll('{projectId}', projectId), expectedResponseModel: (json) { - final spaceModels = - (json['data'] as List).map((spaceJson) => SpaceModel.fromJson(spaceJson)).toList(); + final spaceModels = (json['data'] as List) + .map((spaceJson) => SpaceModel.fromJson(spaceJson)) + .toList(); return spaceModels; }, @@ -327,15 +347,17 @@ class CommunitySpaceManagementApi { } } - Future> getSpaceOnlyWithDevices({String? communityId, String? projectId}) async { + Future> getSpaceOnlyWithDevices( + {String? communityId, String? projectId}) async { try { final response = await HTTPService().get( path: ApiEndpoints.spaceOnlyWithDevices .replaceAll('{communityId}', communityId!) .replaceAll('{projectId}', projectId!), expectedResponseModel: (json) { - final spaceModels = - (json['data'] as List).map((spaceJson) => SpaceModel.fromJson(spaceJson)).toList(); + final spaceModels = (json['data'] as List) + .map((spaceJson) => SpaceModel.fromJson(spaceJson)) + .toList(); return spaceModels; }, ); @@ -345,4 +367,59 @@ class CommunitySpaceManagementApi { return []; } } + + static Future> getSubSpaceBySpaceId( + {required String communityId, + required String spaceId, + required String projectId}) async { + try { + final path = ApiEndpoints.listSubspace + .replaceFirst('{communityUuid}', communityId) + .replaceFirst('{spaceUuid}', spaceId) + .replaceAll('{projectUuid}', projectId); + + final response = await HTTPService().get( + path: path, + queryParameters: {"page": 1, "pageSize": 10}, + showServerMessage: false, + expectedResponseModel: (json) { + List rooms = []; + if (json['data'] != null) { + for (var subspace in json['data']) { + rooms.add(SubSpaceModel.fromJson(subspace)); + } + } + return rooms; + }, + ); + + return response; + } catch (error, stackTrace) { + return []; + } + } + + static Future> assignDeviceToRoom( + {required String communityId, + required String spaceId, + required String subSpaceId, + required String deviceId, + required String projectId}) async { + try { + final response = await HTTPService().post( + path: ApiEndpoints.assignDeviceToRoom + .replaceAll('{projectUuid}', projectId) + .replaceAll('{communityUuid}', communityId) + .replaceAll('{spaceUuid}', spaceId) + .replaceAll('{subSpaceUuid}', subSpaceId) + .replaceAll('{deviceUuid}', deviceId), + expectedResponseModel: (json) { + return json; + }, + ); + return response; + } catch (e) { + rethrow; + } + } } diff --git a/lib/utils/color_manager.dart b/lib/utils/color_manager.dart index 41ceb29a..50170ed9 100644 --- a/lib/utils/color_manager.dart +++ b/lib/utils/color_manager.dart @@ -83,4 +83,7 @@ abstract class ColorsManager { static const Color maxPurpleDot = Color(0xFF5F00BD); static const Color minBlue = Color(0xFF93AAFD); static const Color minBlueDot = Color(0xFF023DFE); + static const Color grey25 = Color(0xFFF9F9F9); + + } diff --git a/lib/utils/constants/api_const.dart b/lib/utils/constants/api_const.dart index 454ec46d..411e72a5 100644 --- a/lib/utils/constants/api_const.dart +++ b/lib/utils/constants/api_const.dart @@ -60,9 +60,12 @@ abstract class ApiEndpoints { '/devices/{uuid}/report-logs?code={code}&startTime={startTime}&endTime={endTime}'; static const String scheduleByDeviceId = '/schedule/{deviceUuid}'; - static const String getScheduleByDeviceId = '/schedule/{deviceUuid}?category={category}'; - static const String deleteScheduleByDeviceId = '/schedule/{deviceUuid}/{scheduleUuid}'; - static const String updateScheduleByDeviceId = '/schedule/enable/{deviceUuid}'; + static const String getScheduleByDeviceId = + '/schedule/{deviceUuid}?category={category}'; + static const String deleteScheduleByDeviceId = + '/schedule/{deviceUuid}/{scheduleUuid}'; + static const String updateScheduleByDeviceId = + '/schedule/enable/{deviceUuid}'; static const String factoryReset = '/devices/batch'; //product @@ -124,4 +127,13 @@ abstract class ApiEndpoints { '/projects/{projectId}/communities/{communityId}/spaces/{unitUuid}/automations'; static const String spaceOnlyWithDevices = '/projects/{projectId}/communities/{communityId}/spaces?onlyWithDevices=true'; + + static const String listSubspace = + '/projects/{projectUuid}/communities/{communityUuid}/spaces/{spaceUuid}/subspaces'; + static const String deviceByUuid = '/devices/{deviceUuid}'; + + static const String resetDevice = '/factory/reset/{deviceUuid}'; + + static const String assignDeviceToRoom = + '/projects/{projectUuid}/communities/{communityUuid}/spaces/{spaceUuid}/subspaces/{subSpaceUuid}/devices/{deviceUuid}'; } diff --git a/lib/utils/constants/assets.dart b/lib/utils/constants/assets.dart index 6dcf5f83..5eb0eb05 100644 --- a/lib/utils/constants/assets.dart +++ b/lib/utils/constants/assets.dart @@ -452,6 +452,13 @@ 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'; diff --git a/lib/web_layout/default_container.dart b/lib/web_layout/default_container.dart new file mode 100644 index 00000000..e0a71b04 --- /dev/null +++ b/lib/web_layout/default_container.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; + +class DefaultContainer extends StatelessWidget { + const DefaultContainer({ + super.key, + required this.child, + this.height, + this.width, + this.color, + this.boxConstraints, + this.margin, + this.padding, + this.onTap, + this.borderRadius, + }); + + final double? height; + final double? width; + final Widget child; + final BoxConstraints? boxConstraints; + final EdgeInsets? margin; + final EdgeInsets? padding; + final Color? color; + final Function()? onTap; + final BorderRadius? borderRadius; + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(20), + child: Container( + height: height, + width: width, + margin: margin ?? const EdgeInsets.only(right: 3, bottom: 3), + constraints: boxConstraints, + decoration: BoxDecoration( + color: color ?? Colors.white, + borderRadius: borderRadius ?? BorderRadius.circular(20), + ), + padding: padding ?? const EdgeInsets.all(10), + child: child, + ), + ); + } +}