SP-1723-FE-Integrate-Charts-with-API-s-for-AQI-sensor.

This commit is contained in:
Faris Armoush
2025-06-16 10:59:51 +03:00
parent 5f20d52e57
commit 6ff9c602f1
12 changed files with 57 additions and 178 deletions

View File

@ -15,7 +15,9 @@ class AirQualityDataModel extends Equatable {
return AirQualityDataModel( return AirQualityDataModel(
date: DateTime.parse(json['date'] as String), date: DateTime.parse(json['date'] as String),
data: (json['data'] as List<dynamic>) data: (json['data'] as List<dynamic>)
.map((e) => AirQualityPercentageData.fromJson(e as Map<String, dynamic>)) .map(
(e) => AirQualityPercentageData.fromJson(e as Map<String, dynamic>),
)
.toList(), .toList(),
); );
} }
@ -23,9 +25,9 @@ class AirQualityDataModel extends Equatable {
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),
'moderate': ColorsManager.moderateYellow.withValues(alpha: 0.7), 'moderate': ColorsManager.moderateYellow.withValues(alpha: 0.7),
'poor': ColorsManager.poorOrange.withValues(alpha: 0.7), 'unhealthy_sensitive': ColorsManager.poorOrange.withValues(alpha: 0.7),
'unhealthy': ColorsManager.unhealthyRed.withValues(alpha: 0.7), 'unhealthy': ColorsManager.unhealthyRed.withValues(alpha: 0.7),
'severe': ColorsManager.severePink.withValues(alpha: 0.7), 'very_unhealthy': ColorsManager.severePink.withValues(alpha: 0.7),
'hazardous': ColorsManager.hazardousPurple.withValues(alpha: 0.7), 'hazardous': ColorsManager.hazardousPurple.withValues(alpha: 0.7),
}; };
@ -36,22 +38,19 @@ class AirQualityDataModel extends Equatable {
class AirQualityPercentageData extends Equatable { class AirQualityPercentageData extends Equatable {
const AirQualityPercentageData({ const AirQualityPercentageData({
required this.type, required this.type,
required this.name,
required this.percentage, required this.percentage,
}); });
final String type; final String type;
final String name;
final double percentage; final double percentage;
factory AirQualityPercentageData.fromJson(Map<String, dynamic> json) { factory AirQualityPercentageData.fromJson(Map<String, dynamic> json) {
return AirQualityPercentageData( return AirQualityPercentageData(
type: json['type'] as String? ?? '', type: json['type'] as String? ?? '',
name: json['name'] as String? ?? '',
percentage: (json['percentage'] as num?)?.toDouble() ?? 0, percentage: (json['percentage'] as num?)?.toDouble() ?? 0,
); );
} }
@override @override
List<Object?> get props => [type, name, percentage]; List<Object?> get props => [type, percentage];
} }

View File

@ -3,6 +3,7 @@ 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/device_location/device_location_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/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';
@ -22,6 +23,7 @@ abstract final class FetchAirQualityDataHelper {
bool shouldFetchAnalyticsDevices = true, bool shouldFetchAnalyticsDevices = true,
}) { }) {
final date = context.read<AnalyticsDatePickerBloc>().state.monthlyDate; final date = context.read<AnalyticsDatePickerBloc>().state.monthlyDate;
final aqiType = context.read<AirQualityDistributionBloc>().state.selectedAqiType;
loadAnalyticsDevices( loadAnalyticsDevices(
context, context,
communityUuid: communityUuid, communityUuid: communityUuid,
@ -36,6 +38,7 @@ abstract final class FetchAirQualityDataHelper {
context, context,
spaceUuid: spaceUuid, spaceUuid: spaceUuid,
date: date, date: date,
aqiType: aqiType,
); );
} }
@ -104,10 +107,15 @@ 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<AirQualityDistributionBloc>().add( context.read<AirQualityDistributionBloc>().add(
LoadAirQualityDistribution( LoadAirQualityDistribution(
GetAirQualityDistributionParam(spaceUuid: spaceUuid, date: date), GetAirQualityDistributionParam(
spaceUuid: spaceUuid,
date: date,
aqiType: aqiType,
),
), ),
); );
} }

View File

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

View File

@ -34,14 +34,14 @@ class AqiDistributionChartTitle extends StatelessWidget {
child: AqiTypeDropdown( child: AqiTypeDropdown(
onChanged: (value) { onChanged: (value) {
if (value != null) { if (value != null) {
try {
final bloc = context.read<AirQualityDistributionBloc>(); final bloc = context.read<AirQualityDistributionBloc>();
try {
final param = _makeLoadAqiDistributionParam(context, value); final param = _makeLoadAqiDistributionParam(context, value);
bloc bloc.add(LoadAirQualityDistribution(param));
..add(UpdateAqiTypeEvent(value))
..add(LoadAirQualityDistribution(param));
} catch (_) { } catch (_) {
return; return;
} finally {
bloc.add(UpdateAqiTypeEvent(value));
} }
} }
}, },

View File

@ -16,7 +16,7 @@ import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/real
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/total_energy_consumption/total_energy_consumption_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/total_energy_consumption/total_energy_consumption_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy/occupancy_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy/occupancy_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy_heat_map/occupancy_heat_map_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy_heat_map/occupancy_heat_map_bloc.dart';
import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/fake_air_quality_distribution_service.dart'; import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/remote_air_quality_distribution_service.dart';
import 'package:syncrow_web/pages/analytics/services/analytics_devices/analytics_devices_service_delagate.dart'; import 'package:syncrow_web/pages/analytics/services/analytics_devices/analytics_devices_service_delagate.dart';
import 'package:syncrow_web/pages/analytics/services/analytics_devices/remote_energy_management_analytics_devices_service.dart'; import 'package:syncrow_web/pages/analytics/services/analytics_devices/remote_energy_management_analytics_devices_service.dart';
import 'package:syncrow_web/pages/analytics/services/analytics_devices/remote_occupancy_analytics_devices_service.dart'; import 'package:syncrow_web/pages/analytics/services/analytics_devices/remote_occupancy_analytics_devices_service.dart';
@ -109,7 +109,7 @@ class _AnalyticsPageState extends State<AnalyticsPage> {
), ),
BlocProvider( BlocProvider(
create: (context) => AirQualityDistributionBloc( create: (context) => AirQualityDistributionBloc(
FakeAirQualityDistributionService(), RemoteAirQualityDistributionService(_httpService),
), ),
), ),
BlocProvider( BlocProvider(

View File

@ -20,7 +20,7 @@ class AnalyticsDateFilterButton extends StatefulWidget {
final void Function(DateTime)? onDateSelected; final void Function(DateTime)? onDateSelected;
final DatePickerType datePickerType; final DatePickerType datePickerType;
static final _color = ColorsManager.blackColor.withValues(alpha: 0.8); static final Color _color = ColorsManager.blackColor.withValues(alpha: 0.8);
@override @override
State<AnalyticsDateFilterButton> createState() => State<AnalyticsDateFilterButton> createState() =>
@ -60,10 +60,9 @@ class _AnalyticsDateFilterButtonState extends State<AnalyticsDateFilterButton> {
), ),
), ),
onPressed: () { onPressed: () {
showDialog( showDialog<void>(
context: context, context: context,
builder: (_) { builder: (_) => switch (widget.datePickerType) {
return switch (widget.datePickerType) {
DatePickerType.month => MonthPickerWidget( DatePickerType.month => MonthPickerWidget(
selectedDate: widget.selectedDate, selectedDate: widget.selectedDate,
onDateSelected: (value) { onDateSelected: (value) {
@ -76,7 +75,6 @@ class _AnalyticsDateFilterButtonState extends State<AnalyticsDateFilterButton> {
widget.onDateSelected?.call(value); widget.onDateSelected?.call(value);
}, },
), ),
};
}, },
); );
}, },

View File

@ -1,9 +1,14 @@
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart';
class GetAirQualityDistributionParam { class GetAirQualityDistributionParam {
final DateTime date; final DateTime date;
final String spaceUuid; final String spaceUuid;
final AqiType aqiType;
const GetAirQualityDistributionParam({ const GetAirQualityDistributionParam(
{
required this.date, required this.date,
required this.spaceUuid, required this.spaceUuid,
required this.aqiType,
}); });
} }

View File

@ -1,95 +0,0 @@
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<List<AirQualityDataModel>> 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<double> _redistributePercentages(
List<double> originalValues,
List<bool> 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<double> _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();
}
}

View File

@ -3,7 +3,8 @@ import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_
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';
import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/services/api/http_service.dart';
class RemoteAirQualityDistributionService implements AirQualityDistributionService { final class RemoteAirQualityDistributionService
implements AirQualityDistributionService {
RemoteAirQualityDistributionService(this._httpService); RemoteAirQualityDistributionService(this._httpService);
final HTTPService _httpService; final HTTPService _httpService;
@ -14,10 +15,10 @@ class RemoteAirQualityDistributionService implements AirQualityDistributionServi
) async { ) async {
try { try {
final response = await _httpService.get( final response = await _httpService.get(
path: 'endpoint', path: '/aqi/distribution/space/${param.spaceUuid}',
queryParameters: { queryParameters: {
'spaceUuid': param.spaceUuid, 'monthDate': _formatDate(param.date),
'date': param.date.toIso8601String(), 'pollutantType': param.aqiType.code,
}, },
expectedResponseModel: (data) { expectedResponseModel: (data) {
final json = data as Map<String, dynamic>? ?? {}; final json = data as Map<String, dynamic>? ?? {};
@ -33,4 +34,8 @@ class RemoteAirQualityDistributionService implements AirQualityDistributionServi
throw Exception('Failed to load energy consumption per phase: $e'); throw Exception('Failed to load energy consumption per phase: $e');
} }
} }
static String _formatDate(DateTime date) {
return '${date.year}-${date.month.toString().padLeft(2, '0')}';
}
} }

View File

@ -26,15 +26,15 @@ class DeviceLocationDetailsServiceDecorator implements DeviceLocationService {
if (data != null) { if (data != null) {
final addressData = data['address'] as Map<String, dynamic>; final addressData = data['address'] as Map<String, dynamic>;
return deviceLocationInfo.copyWith( return deviceLocationInfo.copyWith(
city: addressData['city'], city: addressData['city'] as String?,
country: addressData['country_code'].toString().toUpperCase(), country: addressData['country_code']?.toString().toUpperCase(),
address: addressData['state'], address: addressData['state'] as String?,
); );
} }
return deviceLocationInfo; return deviceLocationInfo;
} catch (e) { } catch (e) {
throw Exception('Failed to load device location info: ${e.toString()}'); throw Exception('Failed to load device location info: $e');
} }
} }
} }

View File

@ -1,15 +0,0 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter_linux/flutter_linux.h>
// Registers Flutter plugins.
void fl_register_plugins(FlPluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_

View File

@ -1,26 +0,0 @@
//
// Generated file. Do not edit.
//
import FlutterMacOS
import Foundation
import firebase_analytics
import firebase_core
import firebase_crashlytics
import firebase_database
import flutter_secure_storage_macos
import path_provider_foundation
import shared_preferences_foundation
import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FirebaseAnalyticsPlugin"))
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
FLTFirebaseCrashlyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCrashlyticsPlugin"))
FLTFirebaseDatabasePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseDatabasePlugin"))
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
}