Prepared for aqi distribution API Integration.

This commit is contained in:
Faris Armoush
2025-06-02 16:13:58 +03:00
parent 7bc9079212
commit 8e11749ed7
13 changed files with 296 additions and 213 deletions

View File

@ -1,24 +1,24 @@
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
class AirQualityDataModel { class AirQualityDataModel extends Equatable {
const AirQualityDataModel({ const AirQualityDataModel({
required this.date, required this.date,
this.good, required this.data,
this.moderate,
this.poor,
this.unhealthy,
this.severe,
this.hazardous,
}); });
final DateTime date; final DateTime date;
final double? good; final List<AirQualityPercentageData> data;
final double? moderate;
final double? poor; factory AirQualityDataModel.fromJson(Map<String, dynamic> json) {
final double? unhealthy; return AirQualityDataModel(
final double? severe; date: DateTime.parse(json['date'] as String),
final double? hazardous; data: (json['data'] as List<dynamic>)
.map((e) => AirQualityPercentageData.fromJson(e as Map<String, dynamic>))
.toList(),
);
}
static final Map<String, Color> metricColors = { static final Map<String, Color> metricColors = {
'good': ColorsManager.goodGreen.withValues(alpha: 0.7), 'good': ColorsManager.goodGreen.withValues(alpha: 0.7),
@ -28,4 +28,30 @@ class AirQualityDataModel {
'severe': ColorsManager.severePink.withValues(alpha: 0.7), 'severe': ColorsManager.severePink.withValues(alpha: 0.7),
'hazardous': ColorsManager.hazardousPurple.withValues(alpha: 0.7), 'hazardous': ColorsManager.hazardousPurple.withValues(alpha: 0.7),
}; };
@override
List<Object?> 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<String, dynamic> json) {
return AirQualityPercentageData(
type: json['type'] as String? ?? '',
name: json['name'] as String? ?? '',
percentage: (json['percentage'] as num?)?.toDouble() ?? 0,
);
}
@override
List<Object?> get props => [type, name, percentage];
} }

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/air_quality_data_model.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/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/pages/analytics/services/air_quality_distribution/air_quality_distribution_service.dart';
@ -9,13 +10,14 @@ part 'air_quality_distribution_state.dart';
class AirQualityDistributionBloc class AirQualityDistributionBloc
extends Bloc<AirQualityDistributionEvent, AirQualityDistributionState> { extends Bloc<AirQualityDistributionEvent, AirQualityDistributionState> {
final AirQualityDistributionService _service; final AirQualityDistributionService _aqiDistributionService;
AirQualityDistributionBloc( AirQualityDistributionBloc(
this._service, this._aqiDistributionService,
) : super(const AirQualityDistributionState()) { ) : super(const AirQualityDistributionState()) {
on<LoadAirQualityDistribution>(_onLoadAirQualityDistribution); on<LoadAirQualityDistribution>(_onLoadAirQualityDistribution);
on<ClearAirQualityDistribution>(_onClearAirQualityDistribution); on<ClearAirQualityDistribution>(_onClearAirQualityDistribution);
on<UpdateAqiTypeEvent>(_onUpdateAqiTypeEvent);
} }
Future<void> _onLoadAirQualityDistribution( Future<void> _onLoadAirQualityDistribution(
@ -23,16 +25,15 @@ class AirQualityDistributionBloc
Emitter<AirQualityDistributionState> emit, Emitter<AirQualityDistributionState> emit,
) async { ) async {
try { try {
emit( emit(state.copyWith(status: AirQualityDistributionStatus.loading));
const AirQualityDistributionState( final result = await _aqiDistributionService.getAirQualityDistribution(
status: AirQualityDistributionStatus.loading, event.param,
),
); );
final result = await _service.getAirQualityDistribution(event.param);
emit( emit(
AirQualityDistributionState( state.copyWith(
status: AirQualityDistributionStatus.success, status: AirQualityDistributionStatus.success,
chartData: result, chartData: result,
filteredChartData: _arrangeChartDataByType(result, state.selectedAqiType),
), ),
); );
} catch (e) { } catch (e) {
@ -40,6 +41,7 @@ class AirQualityDistributionBloc
AirQualityDistributionState( AirQualityDistributionState(
status: AirQualityDistributionStatus.failure, status: AirQualityDistributionStatus.failure,
errorMessage: e.toString(), errorMessage: e.toString(),
selectedAqiType: state.selectedAqiType,
), ),
); );
} }
@ -51,4 +53,29 @@ class AirQualityDistributionBloc
) async { ) async {
emit(const AirQualityDistributionState()); emit(const AirQualityDistributionState());
} }
void _onUpdateAqiTypeEvent(
UpdateAqiTypeEvent event,
Emitter<AirQualityDistributionState> emit,
) {
emit(
state.copyWith(
selectedAqiType: event.aqiType,
filteredChartData: _arrangeChartDataByType(state.chartData, event.aqiType),
),
);
}
List<AirQualityDataModel> _arrangeChartDataByType(
List<AirQualityDataModel> 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();
}
} }

View File

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

View File

@ -8,16 +8,36 @@ enum AirQualityDistributionStatus {
} }
class AirQualityDistributionState extends Equatable { class AirQualityDistributionState extends Equatable {
final AirQualityDistributionStatus status;
final List<AirQualityDataModel> chartData;
final String? errorMessage;
const AirQualityDistributionState({ const AirQualityDistributionState({
this.status = AirQualityDistributionStatus.initial, this.status = AirQualityDistributionStatus.initial,
this.chartData = const [], this.chartData = const [],
this.filteredChartData = const [],
this.errorMessage, this.errorMessage,
this.selectedAqiType = AqiType.aqi,
}); });
final AirQualityDistributionStatus status;
final List<AirQualityDataModel> chartData;
final List<AirQualityDataModel> filteredChartData;
final String? errorMessage;
final AqiType selectedAqiType;
AirQualityDistributionState copyWith({
AirQualityDistributionStatus? status,
List<AirQualityDataModel>? chartData,
List<AirQualityDataModel>? 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 @override
List<Object?> get props => [status, chartData, errorMessage]; List<Object?> get props => [status, chartData, errorMessage, selectedAqiType];
} }

View File

@ -14,8 +14,10 @@ abstract final class FetchAirQualityDataHelper {
static void loadAirQualityData( static void loadAirQualityData(
BuildContext context, { BuildContext context, {
required DateTime date,
required String communityUuid, required String communityUuid,
required String spaceUuid, required String spaceUuid,
bool shouldFetchAnalyticsDevices = true,
}) { }) {
final date = context.read<AnalyticsDatePickerBloc>().state.monthlyDate; final date = context.read<AnalyticsDatePickerBloc>().state.monthlyDate;
loadAnalyticsDevices( loadAnalyticsDevices(

View File

@ -16,6 +16,11 @@ class AqiDistributionChart extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final sortedData = List<AirQualityDataModel>.from(chartData)
..sort(
(a, b) => a.date.compareTo(b.date),
);
return BarChart( return BarChart(
BarChartData( BarChartData(
maxY: 100.1, maxY: 100.1,
@ -25,45 +30,29 @@ class AqiDistributionChart extends StatelessWidget {
borderData: EnergyManagementChartsHelper.borderData(), borderData: EnergyManagementChartsHelper.borderData(),
barTouchData: _barTouchData(context), barTouchData: _barTouchData(context),
titlesData: _titlesData(context), titlesData: _titlesData(context),
barGroups: _buildBarGroups(), barGroups: _buildBarGroups(sortedData),
), ),
duration: Duration.zero, duration: Duration.zero,
); );
} }
List<BarChartGroupData> _buildBarGroups() { List<BarChartGroupData> _buildBarGroups(List<AirQualityDataModel> sortedData) {
return List.generate(chartData.length, (index) { return List.generate(sortedData.length, (index) {
final data = chartData[index]; final data = sortedData[index];
final stackItems = <BarChartRodData>[]; final stackItems = <BarChartRodData>[];
double currentY = 0; double currentY = 0;
bool isFirstElement = true; bool isFirstElement = true;
if (data.good != null) { // Sort data by type to ensure consistent order
stackItems.add( final sortedPercentageData = List<AirQualityPercentageData>.from(data.data)
BarChartRodData( ..sort((a, b) => a.type.compareTo(b.type));
fromY: currentY,
toY: currentY + data.good!,
color: AirQualityDataModel.metricColors['good']!,
borderRadius: isFirstElement
? const BorderRadius.only(
topLeft: Radius.circular(22),
topRight: Radius.circular(22),
)
// ignore: dead_code
: _barBorderRadius,
width: _barWidth,
),
);
currentY += data.good! + _rodStackItemsSpacing;
isFirstElement = false;
}
if (data.moderate != null) { for (final percentageData in sortedPercentageData) {
stackItems.add( stackItems.add(
BarChartRodData( BarChartRodData(
fromY: currentY, fromY: currentY,
toY: currentY + data.moderate!, toY: currentY + percentageData.percentage ,
color: AirQualityDataModel.metricColors['moderate']!, color: AirQualityDataModel.metricColors[percentageData.name]!,
borderRadius: isFirstElement borderRadius: isFirstElement
? const BorderRadius.only( ? const BorderRadius.only(
topLeft: Radius.circular(22), topLeft: Radius.circular(22),
@ -73,83 +62,7 @@ class AqiDistributionChart extends StatelessWidget {
width: _barWidth, width: _barWidth,
), ),
); );
currentY += data.moderate! + _rodStackItemsSpacing; currentY += percentageData.percentage + _rodStackItemsSpacing;
isFirstElement = false;
}
if (data.poor != null) {
stackItems.add(
BarChartRodData(
fromY: currentY,
toY: currentY + data.poor!,
color: AirQualityDataModel.metricColors['poor']!,
borderRadius: isFirstElement
? const BorderRadius.only(
topLeft: Radius.circular(22),
topRight: Radius.circular(22),
)
: _barBorderRadius,
width: _barWidth,
),
);
currentY += data.poor! + _rodStackItemsSpacing;
isFirstElement = false;
}
if (data.unhealthy != null) {
stackItems.add(
BarChartRodData(
fromY: currentY,
toY: currentY + data.unhealthy!,
color: AirQualityDataModel.metricColors['unhealthy']!,
borderRadius: isFirstElement
? const BorderRadius.only(
topLeft: Radius.circular(22),
topRight: Radius.circular(22),
)
: _barBorderRadius,
width: _barWidth,
),
);
currentY += data.unhealthy! + _rodStackItemsSpacing;
isFirstElement = false;
}
if (data.severe != null) {
stackItems.add(
BarChartRodData(
fromY: currentY,
toY: currentY + data.severe!,
color: AirQualityDataModel.metricColors['severe']!,
borderRadius: isFirstElement
? const BorderRadius.only(
topLeft: Radius.circular(22),
topRight: Radius.circular(22),
)
: _barBorderRadius,
width: _barWidth,
),
);
currentY += data.severe! + _rodStackItemsSpacing;
isFirstElement = false;
}
if (data.hazardous != null) {
stackItems.add(
BarChartRodData(
fromY: currentY,
toY: currentY + data.hazardous!,
color: AirQualityDataModel.metricColors['hazardous']!,
borderRadius: isFirstElement
? const BorderRadius.only(
topLeft: Radius.circular(22),
topRight: Radius.circular(22),
)
: _barBorderRadius,
width: _barWidth,
),
);
currentY += data.hazardous! + _rodStackItemsSpacing;
isFirstElement = false; isFirstElement = false;
} }
@ -180,44 +93,14 @@ class AqiDistributionChart extends StatelessWidget {
fontSize: 12, fontSize: 12,
); );
if (data.good != null) { // Sort data by type to ensure consistent order
children.add(TextSpan( final sortedPercentageData = List<AirQualityPercentageData>.from(data.data)
text: '\nGOOD: ${data.good!.toStringAsFixed(1)}%', ..sort((a, b) => a.type.compareTo(b.type));
style: textStyle,
));
}
if (data.moderate != null) { for (final percentageData in sortedPercentageData) {
children.add(TextSpan( children.add(TextSpan(
text: '\nMODERATE: ${data.moderate!.toStringAsFixed(1)}%', text:
style: textStyle, '\n${percentageData.type.toUpperCase()}: ${percentageData.percentage.toStringAsFixed(1)}%',
));
}
if (data.poor != null) {
children.add(TextSpan(
text: '\nPOOR: ${data.poor!.toStringAsFixed(1)}%',
style: textStyle,
));
}
if (data.unhealthy != null) {
children.add(TextSpan(
text: '\nUNHEALTHY: ${data.unhealthy!.toStringAsFixed(1)}%',
style: textStyle,
));
}
if (data.severe != null) {
children.add(TextSpan(
text: '\nSEVERE: ${data.severe!.toStringAsFixed(1)}%',
style: textStyle,
));
}
if (data.hazardous != null) {
children.add(TextSpan(
text: '\nHAZARDOUS: ${data.hazardous!.toStringAsFixed(1)}%',
style: textStyle, style: textStyle,
)); ));
} }

View File

@ -32,7 +32,9 @@ class AqiDistributionChartBox extends StatelessWidget {
const SizedBox(height: 10), const SizedBox(height: 10),
const Divider(), const Divider(),
const SizedBox(height: 20), const SizedBox(height: 20),
Expanded(child: AqiDistributionChart(chartData: state.chartData)), Expanded(
child: AqiDistributionChart(chartData: state.filteredChartData),
),
], ],
), ),
); );

View File

@ -1,4 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart';
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/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';
@ -16,7 +18,7 @@ class AqiDistributionChartTitle extends StatelessWidget {
const Expanded( const Expanded(
flex: 3, flex: 3,
child: FittedBox( child: FittedBox(
fit: BoxFit.scaleDown, fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerStart, alignment: AlignmentDirectional.centerStart,
child: ChartTitle( child: ChartTitle(
title: Text('Distribution over Air Quality Index'), title: Text('Distribution over Air Quality Index'),
@ -27,7 +29,13 @@ class AqiDistributionChartTitle extends StatelessWidget {
alignment: AlignmentDirectional.centerEnd, alignment: AlignmentDirectional.centerEnd,
fit: BoxFit.scaleDown, fit: BoxFit.scaleDown,
child: AqiTypeDropdown( child: AqiTypeDropdown(
onChanged: (value) {}, onChanged: (value) {
if (value != null) {
context
.read<AirQualityDistributionBloc>()
.add(UpdateAqiTypeEvent(value));
}
},
), ),
), ),
], ],

View File

@ -3,7 +3,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/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';
@ -27,18 +26,8 @@ class RangeOfAqiChartBox extends StatelessWidget {
AnalyticsErrorWidget(state.errorMessage), AnalyticsErrorWidget(state.errorMessage),
const SizedBox(height: 10), const SizedBox(height: 10),
], ],
GestureDetector( RangeOfAqiChartTitle(
onTap: () { isLoading: state.status == RangeOfAqiStatus.loading,
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(),

View File

@ -1,6 +1,7 @@
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/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/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_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart'; import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart';
@ -39,6 +40,7 @@ final class AirQualityDataLoadingStrategy implements AnalyticsDataLoadingStrateg
context, context,
communityUuid: community.uuid, communityUuid: community.uuid,
spaceUuid: space.uuid ?? '', spaceUuid: space.uuid ?? '',
date: context.read<AnalyticsDatePickerBloc>().state.monthlyDate,
); );
} }

View File

@ -1,5 +1,6 @@
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/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_tab/analytics_tab_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'; import 'package:syncrow_web/pages/analytics/modules/analytics/enums/analytics_page_tab.dart';
@ -56,33 +57,16 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget {
const Spacer(), const Spacer(),
Visibility( Visibility(
key: ValueKey(selectedTab), key: ValueKey(selectedTab),
visible: selectedTab == AnalyticsPageTab.energyManagement, visible: selectedTab == AnalyticsPageTab.energyManagement ||
selectedTab == AnalyticsPageTab.airQuality,
child: Expanded( child: Expanded(
flex: 2, flex: 2,
child: FittedBox( child: FittedBox(
fit: BoxFit.scaleDown, fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerEnd, alignment: AlignmentDirectional.centerEnd,
child: AnalyticsDateFilterButton( child: AnalyticsDateFilterButton(
onDateSelected: (DateTime value) { onDateSelected: (value) {
context.read<AnalyticsDatePickerBloc>().add( _onDateChanged(context, value, selectedTab);
UpdateAnalyticsDatePickerEvent(montlyDate: value),
);
final spaceTreeState =
context.read<SpaceTreeBloc>().state;
if (spaceTreeState.selectedSpaces.isNotEmpty) {
FetchEnergyManagementDataHelper
.loadEnergyManagementData(
context,
shouldFetchAnalyticsDevices: false,
selectedDate: value,
communityId:
spaceTreeState.selectedCommunities.firstOrNull ??
'',
spaceId:
spaceTreeState.selectedSpaces.firstOrNull ?? '',
);
}
}, },
selectedDate: context selectedDate: context
.watch<AnalyticsDatePickerBloc>() .watch<AnalyticsDatePickerBloc>()
@ -112,4 +96,73 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget {
child: child, child: child,
); );
} }
void _onDateChanged(
BuildContext context,
DateTime date,
AnalyticsPageTab selectedTab,
) {
context.read<AnalyticsDatePickerBloc>().add(
UpdateAnalyticsDatePickerEvent(montlyDate: date),
);
final spaceTreeState = context.read<SpaceTreeBloc>().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<AnalyticsDatePickerBloc>().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,
);
}
} }

View File

@ -1,6 +1,7 @@
import 'dart:math'; import 'dart:math';
import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.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/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/pages/analytics/services/air_quality_distribution/air_quality_distribution_service.dart';
@ -19,30 +20,56 @@ class FakeAirQualityDistributionService implements AirQualityDistributionService
final values = _generateRandomPercentages(); final values = _generateRandomPercentages();
final nullMask = List.generate(6, (_) => _shouldBeNull()); final nullMask = List.generate(6, (_) => _shouldBeNull());
// If all values are null, force at least one to be non-null
if (nullMask.every((isNull) => isNull)) { if (nullMask.every((isNull) => isNull)) {
nullMask[_random.nextInt(6)] = false; nullMask[_random.nextInt(6)] = false;
} }
// Redistribute percentages among non-null values
final nonNullValues = _redistributePercentages(values, nullMask); final nonNullValues = _redistributePercentages(values, nullMask);
return AirQualityDataModel( return AirQualityDataModel(
date: date, date: date,
good: nullMask[0] ? null : nonNullValues[0], data: [
moderate: nullMask[1] ? null : nonNullValues[1], AirQualityPercentageData(
poor: nullMask[2] ? null : nonNullValues[2], type: AqiType.aqi.code,
unhealthy: nullMask[3] ? null : nonNullValues[3], percentage: nonNullValues[0],
severe: nullMask[4] ? null : nonNullValues[4], name: 'good',
hazardous: nullMask[5] ? null : nonNullValues[5], ),
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<double> _redistributePercentages( List<double> _redistributePercentages(
List<double> originalValues, List<bool> nullMask) { List<double> originalValues,
// Calculate total of non-null values List<bool> nullMask,
) {
double nonNullSum = 0; double nonNullSum = 0;
for (int i = 0; i < originalValues.length; i++) { for (int i = 0; i < originalValues.length; i++) {
if (!nullMask[i]) { if (!nullMask[i]) {
@ -50,7 +77,6 @@ class FakeAirQualityDistributionService implements AirQualityDistributionService
} }
} }
// Redistribute percentages to maintain 100% total
return List.generate(originalValues.length, (i) { return List.generate(originalValues.length, (i) {
if (nullMask[i]) return 0; if (nullMask[i]) return 0;
return (originalValues[i] / nonNullSum * 100).roundToDouble(); return (originalValues[i] / nonNullSum * 100).roundToDouble();

View File

@ -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<List<AirQualityDataModel>> 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<String, dynamic>? ?? {};
final mappedData = json['data'] as List<dynamic>? ?? [];
return mappedData.map((e) {
final jsonData = e as Map<String, dynamic>;
return AirQualityDataModel.fromJson(jsonData);
}).toList();
},
);
return response;
} catch (e) {
throw Exception('Failed to load energy consumption per phase: $e');
}
}
}