mirror of
https://github.com/SyncrowIOT/web.git
synced 2025-07-10 15:17:31 +00:00
Prepared for aqi distribution API Integration.
This commit is contained in:
@ -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];
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
final AirQualityDistributionStatus status;
|
||||||
List<Object?> get props => [status, chartData, errorMessage];
|
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
|
||||||
|
List<Object?> get props => [status, chartData, errorMessage, selectedAqiType];
|
||||||
}
|
}
|
||||||
|
@ -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(
|
||||||
|
@ -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,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
@ -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),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -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';
|
||||||
@ -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));
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -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,19 +26,9 @@ class RangeOfAqiChartBox extends StatelessWidget {
|
|||||||
AnalyticsErrorWidget(state.errorMessage),
|
AnalyticsErrorWidget(state.errorMessage),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
],
|
],
|
||||||
GestureDetector(
|
RangeOfAqiChartTitle(
|
||||||
onTap: () {
|
|
||||||
context.read<RangeOfAqiBloc>().add(LoadRangeOfAqiEvent(
|
|
||||||
GetRangeOfAqiParam(
|
|
||||||
spaceUuid: '123',
|
|
||||||
date: DateTime.now().subtract(const Duration(days: 30)),
|
|
||||||
),
|
|
||||||
));
|
|
||||||
},
|
|
||||||
child: RangeOfAqiChartTitle(
|
|
||||||
isLoading: state.status == RangeOfAqiStatus.loading,
|
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),
|
||||||
|
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user