SP-1593 Implemented the agreed upon api contract.

This commit is contained in:
Faris Armoush
2025-06-02 14:26:47 +03:00
parent 066f967cd1
commit 57b6f01177
11 changed files with 170 additions and 65 deletions

View File

@ -1,18 +1,31 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
class RangeOfAqi extends Equatable { class RangeOfAqi extends Equatable {
final double min;
final double avg;
final double max;
final DateTime date; final DateTime date;
final List<RangeOfAqiValue> data;
const RangeOfAqi({ const RangeOfAqi({
required this.min, required this.data,
required this.avg,
required this.max,
required this.date, required this.date,
}); });
@override @override
List<Object?> get props => [min, avg, max, date]; List<Object?> get props => [data, date];
}
class RangeOfAqiValue extends Equatable {
final String type;
final double min;
final double average;
final double max;
const RangeOfAqiValue({
required this.type,
required this.min,
required this.average,
required this.max,
});
@override
List<Object?> get props => [type, min, average, max];
} }

View File

@ -1,6 +1,7 @@
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/analytics/models/range_of_aqi.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/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/pages/analytics/services/range_of_aqi/range_of_aqi_service.dart';
@ -11,6 +12,7 @@ class RangeOfAqiBloc extends Bloc<RangeOfAqiEvent, RangeOfAqiState> {
RangeOfAqiBloc(this._rangeOfAqiService) : super(const RangeOfAqiState()) { RangeOfAqiBloc(this._rangeOfAqiService) : super(const RangeOfAqiState()) {
on<LoadRangeOfAqiEvent>(_onLoadRangeOfAqiEvent); on<LoadRangeOfAqiEvent>(_onLoadRangeOfAqiEvent);
on<ClearRangeOfAqiEvent>(_onClearRangeOfAqiEvent); on<ClearRangeOfAqiEvent>(_onClearRangeOfAqiEvent);
on<UpdateAqiTypeEvent>(_onUpdateAqiTypeEvent);
} }
final RangeOfAqiService _rangeOfAqiService; final RangeOfAqiService _rangeOfAqiService;
@ -20,19 +22,55 @@ class RangeOfAqiBloc extends Bloc<RangeOfAqiEvent, RangeOfAqiState> {
Emitter<RangeOfAqiState> emit, Emitter<RangeOfAqiState> emit,
) async { ) async {
emit( emit(
RangeOfAqiState( state.copyWith(status: RangeOfAqiStatus.loading),
status: RangeOfAqiStatus.loading,
rangeOfAqi: state.rangeOfAqi,
),
); );
try { try {
final rangeOfAqi = await _rangeOfAqiService.load(event.param); 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) { } catch (e) {
emit(RangeOfAqiState(status: RangeOfAqiStatus.failure, errorMessage: '$e')); emit(
state.copyWith(
status: RangeOfAqiStatus.failure,
errorMessage: '$e',
),
);
} }
} }
void _onUpdateAqiTypeEvent(
UpdateAqiTypeEvent event,
Emitter<RangeOfAqiState> emit,
) {
emit(
state.copyWith(
selectedAqiType: event.aqiType,
filteredRangeOfAqi: _arrangeChartDataByType(state.rangeOfAqi, event.aqiType),
),
);
}
List<RangeOfAqi> _arrangeChartDataByType(
List<RangeOfAqi> 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( void _onClearRangeOfAqiEvent(
ClearRangeOfAqiEvent event, ClearRangeOfAqiEvent event,
Emitter<RangeOfAqiState> emit, Emitter<RangeOfAqiState> emit,

View File

@ -16,6 +16,15 @@ class LoadRangeOfAqiEvent extends RangeOfAqiEvent {
List<Object> get props => [param]; List<Object> get props => [param];
} }
class UpdateAqiTypeEvent extends RangeOfAqiEvent {
const UpdateAqiTypeEvent(this.aqiType);
final AqiType aqiType;
@override
List<Object> get props => [aqiType];
}
class ClearRangeOfAqiEvent extends RangeOfAqiEvent { class ClearRangeOfAqiEvent extends RangeOfAqiEvent {
const ClearRangeOfAqiEvent(); const ClearRangeOfAqiEvent();
} }

View File

@ -5,14 +5,35 @@ enum RangeOfAqiStatus { initial, loading, loaded, failure }
final class RangeOfAqiState extends Equatable { final class RangeOfAqiState extends Equatable {
const RangeOfAqiState({ const RangeOfAqiState({
this.rangeOfAqi = const [], this.rangeOfAqi = const [],
this.filteredRangeOfAqi = const [],
this.status = RangeOfAqiStatus.initial, this.status = RangeOfAqiStatus.initial,
this.errorMessage, this.errorMessage,
this.selectedAqiType = AqiType.aqi,
}); });
final RangeOfAqiStatus status; final RangeOfAqiStatus status;
final List<RangeOfAqi> rangeOfAqi; final List<RangeOfAqi> rangeOfAqi;
final List<RangeOfAqi> filteredRangeOfAqi;
final String? errorMessage; final String? errorMessage;
final AqiType selectedAqiType;
RangeOfAqiState copyWith({
RangeOfAqiStatus? status,
List<RangeOfAqi>? rangeOfAqi,
List<RangeOfAqi>? 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 @override
List<Object?> get props => [status, rangeOfAqi, errorMessage]; List<Object?> get props =>
[status, rangeOfAqi, filteredRangeOfAqi, errorMessage, selectedAqiType];
} }

View File

@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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/air_quality_distribution/air_quality_distribution_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/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_date_picker_bloc/analytics_date_picker_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart';
@ -28,7 +27,6 @@ abstract final class FetchAirQualityDataHelper {
context, context,
spaceUuid: spaceUuid, spaceUuid: spaceUuid,
date: date, date: date,
aqiType: AqiType.aqi,
); );
loadAirQualityDistribution( loadAirQualityDistribution(
context, context,
@ -76,14 +74,12 @@ abstract final class FetchAirQualityDataHelper {
BuildContext context, { BuildContext context, {
required String spaceUuid, required String spaceUuid,
required DateTime date, required DateTime date,
required AqiType aqiType,
}) { }) {
context.read<RangeOfAqiBloc>().add( context.read<RangeOfAqiBloc>().add(
LoadRangeOfAqiEvent( LoadRangeOfAqiEvent(
GetRangeOfAqiParam( GetRangeOfAqiParam(
date: date, date: date,
spaceUuid: spaceUuid, spaceUuid: spaceUuid,
aqiType: aqiType,
), ),
), ),
); );

View File

@ -3,17 +3,18 @@ import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart';
enum AqiType { enum AqiType {
aqi('AQI', ''), aqi('AQI', '', 'aqi'),
pm25('PM2.5', 'µg/m³'), pm25('PM2.5', 'µg/m³', 'pm25'),
pm10('PM10', 'µg/m³'), pm10('PM10', 'µg/m³', 'pm10'),
hcho('HCHO', 'mg/m³'), hcho('HCHO', 'mg/m³', 'hcho'),
tvoc('TVOC', 'µg/m³'), tvoc('TVOC', 'µg/m³', 'tvoc'),
co2('CO2', 'ppm'); co2('CO2', 'ppm', 'co2');
const AqiType(this.value, this.unit); const AqiType(this.value, this.unit, this.code);
final String value; final String value;
final String unit; final String unit;
final String code;
} }
class AqiTypeDropdown extends StatefulWidget { class AqiTypeDropdown extends StatefulWidget {

View File

@ -13,23 +13,37 @@ class RangeOfAqiChart extends StatelessWidget {
required this.chartData, required this.chartData,
}); });
List<(List<double> values, Color color, Color? dotColor)> get _lines => [ List<(List<double> values, Color color, Color? dotColor)> get _lines {
( final sortedData = List<RangeOfAqi>.from(chartData)
chartData.map((e) => e.max).toList(), ..sort((a, b) => a.date.compareTo(b.date));
ColorsManager.maxPurple,
ColorsManager.maxPurpleDot, return [
), (
( sortedData.map((e) {
chartData.map((e) => e.avg).toList(), final value = e.data.firstOrNull;
Colors.white, return value?.max ?? 0;
null, }).toList(),
), ColorsManager.maxPurple,
( ColorsManager.maxPurpleDot,
chartData.map((e) => e.min).toList(), ),
ColorsManager.minBlue, (
ColorsManager.minBlueDot, 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart';
import 'package:syncrow_web/pages/analytics/params/get_range_of_aqi_param.dart';
import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart'; import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart';
import 'package:syncrow_web/utils/style.dart'; import 'package:syncrow_web/utils/style.dart';
@ -26,13 +27,23 @@ class RangeOfAqiChartBox extends StatelessWidget {
AnalyticsErrorWidget(state.errorMessage), AnalyticsErrorWidget(state.errorMessage),
const SizedBox(height: 10), const SizedBox(height: 10),
], ],
RangeOfAqiChartTitle( GestureDetector(
isLoading: state.status == RangeOfAqiStatus.loading, onTap: () {
context.read<RangeOfAqiBloc>().add(LoadRangeOfAqiEvent(
GetRangeOfAqiParam(
spaceUuid: '123',
date: DateTime.now().subtract(const Duration(days: 30)),
),
));
},
child: RangeOfAqiChartTitle(
isLoading: state.status == RangeOfAqiStatus.loading,
),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
const Divider(), const Divider(),
const SizedBox(height: 20), const SizedBox(height: 20),
Expanded(child: RangeOfAqiChart(chartData: state.rangeOfAqi)), Expanded(child: RangeOfAqiChart(chartData: state.filteredRangeOfAqi)),
], ],
), ),
); );

View File

@ -1,15 +1,18 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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/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/analytics/widgets/chart_informative_cell.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart';
import 'package:syncrow_web/pages/analytics/widgets/charts_loading_widget.dart'; import 'package:syncrow_web/pages/analytics/widgets/charts_loading_widget.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart'; import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
class RangeOfAqiChartTitle extends StatelessWidget { class RangeOfAqiChartTitle extends StatelessWidget {
const RangeOfAqiChartTitle({required this.isLoading, super.key}); const RangeOfAqiChartTitle({
required this.isLoading,
super.key,
});
final bool isLoading; final bool isLoading;
static const List<(Color color, String title, bool hasBorder)> _colors = [ static const List<(Color color, String title, bool hasBorder)> _colors = [
@ -59,19 +62,16 @@ class RangeOfAqiChartTitle extends StatelessWidget {
child: FittedBox( child: FittedBox(
fit: BoxFit.scaleDown, fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerEnd, alignment: AlignmentDirectional.centerEnd,
child: AqiTypeDropdown( child: AqiTypeDropdown(
onChanged: (value) { onChanged: (value) {
final spaceTreeState = context.read<SpaceTreeBloc>().state; final spaceTreeState = context.read<SpaceTreeBloc>().state;
final spaceUuid = spaceTreeState.selectedSpaces.firstOrNull; final spaceUuid = spaceTreeState.selectedSpaces.firstOrNull;
if (spaceUuid == null) return; // if (spaceUuid == null) return;
FetchAirQualityDataHelper.loadRangeOfAqi( if (value != null) {
context, context.read<RangeOfAqiBloc>().add(UpdateAqiTypeEvent(value));
spaceUuid: spaceUuid, }
date: context.read<AnalyticsDatePickerBloc>().state.monthlyDate,
aqiType: value ?? AqiType.aqi,
);
}, },
), ),
), ),

View File

@ -1,16 +1,12 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart';
class GetRangeOfAqiParam extends Equatable { class GetRangeOfAqiParam extends Equatable {
final DateTime date; final DateTime date;
final String spaceUuid; final String spaceUuid;
final AqiType aqiType;
const GetRangeOfAqiParam( const GetRangeOfAqiParam({
{
required this.date, required this.date,
required this.spaceUuid, required this.spaceUuid,
required this.aqiType,
}); });
@override @override

View File

@ -1,4 +1,5 @@
import 'package:syncrow_web/pages/analytics/models/range_of_aqi.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/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/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 avg = (min + avgDelta).clamp(0.0, 301.0);
final max = (avg + maxDelta).clamp(0.0, 301.0); final max = (avg + maxDelta).clamp(0.0, 301.0);
return RangeOfAqi( return RangeOfAqi(
min: min, data: [
avg: avg, RangeOfAqiValue(type: AqiType.aqi.code, min: min, average: avg, max: max),
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, date: date,
); );
}); });