Sp 1594 device location api integration (#216)

<!--
  Thanks for contributing!

Provide a description of your changes below and a general summary in the
title

Please look at the following checklist to ensure that your PR can be
accepted quickly:
-->

## Jira Ticket
[SP-1594](https://syncrow.atlassian.net/browse/SP-1594)

## Description

1. Implemented `Bloc` and `Services` to integrate the device location
into the side panel of the AQI Analytics module.
2. Fixed bugs in side panel caused by `Expanded` widgets.

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [x]  New feature (non-breaking change which adds functionality)
- [x] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [ ]  Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] 🧹 Code refactor
- [ ]  Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore 


[SP-1594]:
https://syncrow.atlassian.net/browse/SP-1594?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
This commit is contained in:
Faris Armoush
2025-06-04 16:41:08 +03:00
committed by GitHub
15 changed files with 392 additions and 81 deletions

View File

@ -0,0 +1,50 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/analytics/models/device_location_info.dart';
import 'package:syncrow_web/pages/analytics/params/get_device_location_data_param.dart';
import 'package:syncrow_web/pages/analytics/services/device_location/device_location_service.dart';
part 'device_location_event.dart';
part 'device_location_state.dart';
class DeviceLocationBloc extends Bloc<DeviceLocationEvent, DeviceLocationState> {
DeviceLocationBloc(
this._deviceLocationService,
) : super(const DeviceLocationState()) {
on<LoadDeviceLocationEvent>(_onLoadDeviceLocation);
on<ClearDeviceLocationEvent>(_onClearDeviceLocation);
}
final DeviceLocationService _deviceLocationService;
Future<void> _onLoadDeviceLocation(
LoadDeviceLocationEvent event,
Emitter<DeviceLocationState> emit,
) async {
emit(const DeviceLocationState(status: DeviceLocationStatus.loading));
try {
final locationInfo = await _deviceLocationService.get(event.param);
emit(
DeviceLocationState(
status: DeviceLocationStatus.success,
locationInfo: locationInfo,
),
);
} catch (e) {
emit(
DeviceLocationState(
status: DeviceLocationStatus.failure,
errorMessage: e.toString(),
),
);
}
}
void _onClearDeviceLocation(
ClearDeviceLocationEvent event,
Emitter<DeviceLocationState> emit,
) {
emit(const DeviceLocationState());
}
}

View File

@ -0,0 +1,21 @@
part of 'device_location_bloc.dart';
sealed class DeviceLocationEvent extends Equatable {
const DeviceLocationEvent();
@override
List<Object?> get props => [];
}
final class LoadDeviceLocationEvent extends DeviceLocationEvent {
const LoadDeviceLocationEvent(this.param);
final GetDeviceLocationDataParam param;
@override
List<Object?> get props => [param];
}
final class ClearDeviceLocationEvent extends DeviceLocationEvent {
const ClearDeviceLocationEvent();
}

View File

@ -0,0 +1,18 @@
part of 'device_location_bloc.dart';
enum DeviceLocationStatus { initial, loading, success, failure }
final class DeviceLocationState extends Equatable {
const DeviceLocationState({
this.status = DeviceLocationStatus.initial,
this.locationInfo,
this.errorMessage,
});
final DeviceLocationStatus status;
final DeviceLocationInfo? locationInfo;
final String? errorMessage;
@override
List<Object?> get props => [status, locationInfo, errorMessage];
}

View File

@ -1,12 +1,14 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/device_location/device_location_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart';
import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart';
import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart';
import 'package:syncrow_web/pages/analytics/params/get_device_location_data_param.dart';
import 'package:syncrow_web/pages/analytics/params/get_range_of_aqi_param.dart';
abstract final class FetchAirQualityDataHelper {
@ -48,6 +50,8 @@ abstract final class FetchAirQualityDataHelper {
const ClearAirQualityDistribution(),
);
context.read<RangeOfAqiBloc>().add(const ClearRangeOfAqiEvent());
context.read<DeviceLocationBloc>().add(const ClearDeviceLocationEvent());
}
static void loadAnalyticsDevices(
@ -61,12 +65,21 @@ abstract final class FetchAirQualityDataHelper {
communityUuid: communityUuid,
spaceUuid: spaceUuid,
deviceTypes: ['AQI'],
requestType: AnalyticsDeviceRequestType.energyManagement,
requestType: AnalyticsDeviceRequestType.occupancy,
),
onSuccess: (device) {
context.read<RealtimeDeviceChangesBloc>()
..add(const RealtimeDeviceChangesClosed())
..add(RealtimeDeviceChangesStarted(device.uuid));
context.read<DeviceLocationBloc>().add(
LoadDeviceLocationEvent(
GetDeviceLocationDataParam(
latitude: device.latitude ?? 0,
longitude: device.longitude ?? 0,
),
),
);
},
),
);

View File

@ -72,55 +72,54 @@ class AqiDeviceInfo extends StatelessWidget {
return Container(
decoration: secondarySection.copyWith(boxShadow: const []),
padding: const EdgeInsetsDirectional.all(20),
child: Expanded(
child: Column(
spacing: 6,
children: [
const AirQualityEndSideLiveIndicator(),
AirQualityEndSideGaugeAndInfo(
aqiLevel: status
.firstWhere(
(e) => e.code == 'air_quality_index',
orElse: () => Status(code: 'air_quality_index', value: ''),
)
.value
.toString(),
temperature: int.parse(tempValue),
humidity: int.parse(humidityValue),
),
const SizedBox(height: 20),
AqiSubValueWidget(
range: (0, 999),
label: AqiType.pm25.value,
value: pm25Value,
unit: AqiType.pm25.unit,
),
AqiSubValueWidget(
range: (0, 999),
label: AqiType.pm10.value,
value: pm10Value,
unit: AqiType.pm10.unit,
),
AqiSubValueWidget(
range: (0, 5),
label: AqiType.hcho.value,
value: ch2oValue,
unit: AqiType.hcho.unit,
),
AqiSubValueWidget(
range: (0, 999),
label: AqiType.tvoc.value,
value: tvocValue,
unit: AqiType.tvoc.unit,
),
AqiSubValueWidget(
range: (0, 5000),
label: AqiType.co2.value,
value: co2Value,
unit: AqiType.co2.unit,
),
],
),
child: Column(
spacing: 6,
mainAxisSize: MainAxisSize.max,
children: [
const AirQualityEndSideLiveIndicator(),
AirQualityEndSideGaugeAndInfo(
aqiLevel: status
.firstWhere(
(e) => e.code == 'air_quality_index',
orElse: () => Status(code: 'air_quality_index', value: ''),
)
.value
.toString(),
temperature: int.parse(tempValue),
humidity: int.parse(humidityValue),
),
const SizedBox(height: 20),
AqiSubValueWidget(
range: (0, 999),
label: AqiType.pm25.value,
value: pm25Value,
unit: AqiType.pm25.unit,
),
AqiSubValueWidget(
range: (0, 999),
label: AqiType.pm10.value,
value: pm10Value,
unit: AqiType.pm10.unit,
),
AqiSubValueWidget(
range: (0, 5),
label: AqiType.hcho.value,
value: ch2oValue,
unit: AqiType.hcho.unit,
),
AqiSubValueWidget(
range: (0, 999),
label: AqiType.tvoc.value,
value: tvocValue,
unit: AqiType.tvoc.unit,
),
AqiSubValueWidget(
range: (0, 5000),
label: AqiType.co2.value,
value: co2Value,
unit: AqiType.co2.unit,
),
],
),
);
},

View File

@ -6,7 +6,34 @@ import 'package:syncrow_web/utils/extension/build_context_x.dart';
import 'package:syncrow_web/utils/style.dart';
class AqiLocation extends StatelessWidget {
const AqiLocation({super.key});
const AqiLocation({
required this.city,
required this.country,
required this.address,
super.key,
});
final String? city;
final String? country;
final String? address;
String _getFormattedLocation() {
if (city == null && country == null && address == null) {
return 'N/A';
}
final parts = <String>[];
if (city != null) parts.add(city!);
if (address != null) parts.add(address!);
final locationPart = parts.join(', ');
if (country != null) {
return locationPart.isEmpty ? country! : '$locationPart - $country';
}
return locationPart;
}
@override
Widget build(BuildContext context) {
@ -24,7 +51,7 @@ class AqiLocation extends StatelessWidget {
_buildLocationPin(),
Expanded(
child: Text(
'Business Bay, Dubai - UAE',
_getFormattedLocation(),
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.textPrimaryColor,
fontWeight: FontWeight.w400,

View File

@ -1,4 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_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/widgets/aqi_location.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_location_info_cell.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
@ -9,37 +11,46 @@ class AqiLocationInfo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
decoration: secondarySection.copyWith(boxShadow: const []),
padding: const EdgeInsetsDirectional.all(20),
child: const Column(
spacing: 8,
children: [
AqiLocation(),
Expanded(
child: Row(
spacing: 8,
children: [
AqiLocationInfoCell(
label: 'Temperature',
value: ' 25°',
svgPath: Assets.aqiTemperature,
return BlocBuilder<DeviceLocationBloc, DeviceLocationState>(
builder: (context, state) {
final info = state.locationInfo;
return Container(
decoration: secondarySection.copyWith(boxShadow: const []),
padding: const EdgeInsetsDirectional.all(20),
child: Column(
spacing: 8,
children: [
AqiLocation(
city: info?.city,
country: info?.country,
address: info?.address,
),
Expanded(
child: Row(
spacing: 8,
children: [
AqiLocationInfoCell(
label: 'Temperature',
value: ' ${info?.temperature?.roundToDouble() ?? '--'}°',
svgPath: Assets.aqiTemperature,
),
AqiLocationInfoCell(
label: 'Humidity',
value: '${info?.humidity?.roundToDouble() ?? '--'}%',
svgPath: Assets.aqiHumidity,
),
AqiLocationInfoCell(
label: 'Air Quality',
value: ' ${info?.airQuality?.roundToDouble() ?? '--'}',
svgPath: Assets.aqiAirQuality,
),
],
),
AqiLocationInfoCell(
label: 'Humidity',
value: '25%',
svgPath: Assets.aqiHumidity,
),
AqiLocationInfoCell(
label: 'Air Quality',
value: ' 120',
svgPath: Assets.aqiAirQuality,
),
],
),
),
],
),
],
),
);
},
);
}
}

View File

@ -1,6 +1,8 @@
import 'package:dio/dio.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/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/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';
@ -18,6 +20,8 @@ import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/fa
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_occupancy_analytics_devices_service.dart';
import 'package:syncrow_web/pages/analytics/services/device_location/device_location_details_service_decorator.dart';
import 'package:syncrow_web/pages/analytics/services/device_location/remote_device_location_service.dart';
import 'package:syncrow_web/pages/analytics/services/energy_consumption_by_phases/remote_energy_consumption_by_phases_service.dart';
import 'package:syncrow_web/pages/analytics/services/energy_consumption_per_device/remote_energy_consumption_per_device_service.dart';
import 'package:syncrow_web/pages/analytics/services/occupacy/remote_occupancy_service.dart';
@ -108,6 +112,18 @@ class _AnalyticsPageState extends State<AnalyticsPage> {
FakeAirQualityDistributionService(),
),
),
BlocProvider(
create: (context) => DeviceLocationBloc(
DeviceLocationDetailsServiceDecorator(
RemoteDeviceLocationService(_httpService),
Dio(
BaseOptions(
baseUrl: 'https://nominatim.openstreetmap.org/',
),
),
),
),
),
],
child: const AnalyticsPageForm(),
);