Compare commits

..
Author SHA1 Message Date
mohammad f7f3843fa7 Enhance device management UI with improved padding and SVG icon integration 2025-06-10 16:29:14 +03:00
Faris Armoush c6b55cb28b 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
2025-06-04 16:41:08 +03:00
Faris Armoush bfd8e964f7 SP-1665-FE-Return-a-readable-error-when-a-connection-error-Exception-… (#233)
…occurs-on-the-charts.

<!--
  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-1665](https://syncrow.atlassian.net/browse/SP-1665)

## Description

Returns a readable error when an analytics api fails.

## Type of Change

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

- [x] ✨ New feature (non-breaking change which adds functionality)
- [ ] 🛠️ 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-1665]:
https://syncrow.atlassian.net/browse/SP-1665?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-04 16:40:25 +03:00
Faris Armoush 08725201d5 Dev ci/cd enhancements (#234)
<!--
  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:
-->

## Description

Pulled CI/CD changes from `main` branch into `dev` branch.

## Type of Change

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

- [ ] ✨ New feature (non-breaking change which adds functionality)
- [ ] 🛠️ 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
- [x] 🗑️ Chore
2025-06-04 16:10:00 +03:00
Faris Armoush 7fe34c61b2 correctly fetch energy management data using spaceUuid. (#235)
<!--
  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-1495](https://syncrow.atlassian.net/browse/SP-1495)

## Description

correctly fetch energy management data using `spaceUuid`.

## Type of Change

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

- [x] 🛠️ Bug fix (non-breaking change which fixes an issue)

[SP-1495]:
https://syncrow.atlassian.net/browse/SP-1495?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-04 16:09:10 +03:00
Faris Armoush 69c23525ba Merge branch 'dev' of https://github.com/SyncrowIOT/web into SP-1665-FE-Return-a-readable-error-when-a-connection-error-Exception-occurs-on-the-charts 2025-06-04 14:49:48 +03:00
Faris Armoush 3e32968209 Update pull_request_template.md 2025-06-04 14:47:35 +03:00
Faris Armoush beb5239c4f enhanced ci/cd by not running the deply jobs on the PR itself, and now we only deploy when we merged a PR to dev or main, and created a separate GitHub action that only builds and install dependencies, which only runs on the PR itself. 2025-06-04 14:47:35 +03:00
Faris Armoush 3a98f71ff3 SP-1665-FE-Return-a-readable-error-when-a-connection-error-Exception-occurs-on-the-charts. 2025-06-04 14:42:41 +03:00
Faris Armoush 24a7f3ac2a SP-1594-device-location-api-integration. 2025-06-04 13:06:27 +03:00
Faris Armoush 79b974ee6c re-injected AirQualityDistributionBloc into AnalyticsPage. 2025-06-04 09:36:41 +03:00
Faris Armoush 651ac6785e removed open weather api keys from .env files. 2025-06-04 09:32:56 +03:00
Faris Armoush 9fa59ce78b Merge branch 'dev' of https://github.com/SyncrowIOT/web into SP-1594-device-location-api-integration 2025-06-04 09:29:49 +03:00
Faris Armoush e2c44ba85f injected the remote and reverse geocoder dependenies into DeviceLocationBloc. 2025-06-04 09:28:50 +03:00
Faris Armoush 1edeb664aa Connected data coming from DeviceLocationBloc into the respective widgets. 2025-06-04 09:28:16 +03:00
Faris Armoush 25a55ad820 made GetDeviceLocationDataParam.toJson method have the correct keys for the API. 2025-06-04 09:27:46 +03:00
Faris Armoush e48fc8b82c loads and clears DeviceLocationBloc. 2025-06-04 09:27:21 +03:00
Faris Armoush 8d999f118c Connected RemoteDeviceLocationService to the new BE API, instead of directly fetching the data from OpenWeather Api's. 2025-06-04 09:18:28 +03:00
Faris Armoush bcb6e49a01 Deleted FakeDeviceLocationService class, since it is no longer needed. 2025-06-04 09:17:38 +03:00
Faris Armoush b0ed844893 made events and state class finals, to better document that they shouldn't be extended. 2025-06-01 14:37:22 +03:00
Faris Armoush 3d183528c5 Fixed thrown exceptions because of Expanded widgets. 2025-06-01 10:57:49 +03:00
Faris Armoush 2c4da63266 Injected DeviceLocationBloc into AnalyticsPage. 2025-06-01 10:50:51 +03:00
Faris Armoush 4ebe65f820 Merge branch 'dev' of https://github.com/SyncrowIOT/web into SP-1594-device-location-api-integration 2025-06-01 09:51:32 +03:00
Faris Armoush 5654d66b60 Created a remote implementation for DeviceLocationService. 2025-06-01 09:51:08 +03:00
Faris Armoush b6879035f0 Implemented geocoding functionality to retrieve and manage device location data using the newly added geocoding package. 2025-05-29 15:47:34 +03:00
Faris Armoush 8ad048e18d Added geocoding: ^4.0.0 package. 2025-05-29 15:47:24 +03:00
Faris Armoush d92b699a2b Created a bloc for loading and managing the state of device location data. 2025-05-29 15:40:44 +03:00
Faris Armoush 6ffb677c33 Created an interface and its fake implementation for loading device location data. 2025-05-29 15:40:15 +03:00
Faris Armoush e7476a084d Created a model class for loading device location data. 2025-05-29 15:39:51 +03:00
Faris Armoush 511acc186f Created a param class for loading device location data. 2025-05-29 15:39:44 +03:00
36 changed files with 680 additions and 172 deletions
+2 -6
View File
@@ -7,11 +7,7 @@
--> -->
## Jira Ticket ## Jira Ticket
<!-- Add your Jira ticket number as a link (e.g., [PROJ-123](https://jira.company.com/browse/PROJ-123)) --> [SP-0000](https://syncrow.atlassian.net/browse/SP-0000)
## Status
**READY/IN DEVELOPMENT/HOLD**
## Description ## Description
@@ -27,4 +23,4 @@
- [ ] 🧹 Code refactor - [ ] 🧹 Code refactor
- [ ] ✅ Build configuration change - [ ] ✅ Build configuration change
- [ ] 📝 Documentation - [ ] 📝 Documentation
- [ ] 🗑️ Chore - [ ] 🗑️ Chore
@@ -4,10 +4,6 @@ on:
push: push:
branches: branches:
- main - main
pull_request:
types: [opened, synchronize, reopened, closed]
branches:
- main
jobs: jobs:
build_and_deploy_job: build_and_deploy_job:
@@ -4,18 +4,12 @@ on:
push: push:
branches: branches:
- dev - dev
pull_request:
types: [opened, synchronize, reopened, closed]
branches:
- dev
jobs: jobs:
build_and_deploy_job: build_and_deploy_job:
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Build and Deploy Job name: Build and Deploy Job
steps: steps:
- name: Checkout Code - name: Checkout Code
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
+29
View File
@@ -0,0 +1,29 @@
name: Pull Request Check
on:
pull_request:
branches:
- dev
- main
jobs:
setup_flutter:
runs-on: ubuntu-latest
name: Setup Flutter and Dependencies
steps:
- name: Checkout Code
uses: actions/checkout@v3
with:
submodules: true
lfs: false
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.27.3'
- name: Install dependencies
run: flutter pub get
- name: Run Flutter Build
run: flutter build web --web-renderer canvaskit -t lib/main_dev.dart
@@ -8,6 +8,8 @@ class AnalyticsDevice {
this.isActive, this.isActive,
this.productDevice, this.productDevice,
this.spaceUuid, this.spaceUuid,
this.latitude,
this.longitude,
}); });
final String uuid; final String uuid;
@@ -18,6 +20,8 @@ class AnalyticsDevice {
final bool? isActive; final bool? isActive;
final ProductDevice? productDevice; final ProductDevice? productDevice;
final String? spaceUuid; final String? spaceUuid;
final double? latitude;
final double? longitude;
factory AnalyticsDevice.fromJson(Map<String, dynamic> json) { factory AnalyticsDevice.fromJson(Map<String, dynamic> json) {
return AnalyticsDevice( return AnalyticsDevice(
@@ -35,6 +39,8 @@ class AnalyticsDevice {
? ProductDevice.fromJson(json['productDevice'] as Map<String, dynamic>) ? ProductDevice.fromJson(json['productDevice'] as Map<String, dynamic>)
: null, : null,
spaceUuid: json['spaceUuid'] as String?, spaceUuid: json['spaceUuid'] as String?,
latitude: json['lat'] != null ? double.parse(json['lat'] as String) : null,
longitude: json['lon'] != null ? double.parse(json['lon'] as String) : null,
); );
} }
} }
@@ -0,0 +1,55 @@
import 'package:equatable/equatable.dart';
class DeviceLocationInfo extends Equatable {
const DeviceLocationInfo({
this.airQuality,
this.humidity,
this.city,
this.country,
this.address,
this.temperature,
});
final double? airQuality;
final double? humidity;
final String? city;
final String? country;
final String? address;
final double? temperature;
factory DeviceLocationInfo.fromJson(Map<String, dynamic> json) {
return DeviceLocationInfo(
airQuality: json['aqi'] as double?,
humidity: json['humidity'] as double?,
temperature: json['temperature'] as double?,
);
}
DeviceLocationInfo copyWith({
double? airQuality,
double? humidity,
String? city,
String? country,
String? address,
double? temperature,
}) {
return DeviceLocationInfo(
airQuality: airQuality ?? this.airQuality,
humidity: humidity ?? this.humidity,
city: city ?? this.city,
country: country ?? this.country,
address: address ?? this.address,
temperature: temperature ?? this.temperature,
);
}
@override
List<Object?> get props => [
airQuality,
humidity,
city,
country,
address,
temperature,
];
}
@@ -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());
}
}
@@ -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();
}
@@ -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];
}
@@ -1,12 +1,14 @@
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/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/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/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';
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/params/get_analytics_devices_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'; import 'package:syncrow_web/pages/analytics/params/get_range_of_aqi_param.dart';
abstract final class FetchAirQualityDataHelper { abstract final class FetchAirQualityDataHelper {
@@ -48,6 +50,8 @@ abstract final class FetchAirQualityDataHelper {
const ClearAirQualityDistribution(), const ClearAirQualityDistribution(),
); );
context.read<RangeOfAqiBloc>().add(const ClearRangeOfAqiEvent()); context.read<RangeOfAqiBloc>().add(const ClearRangeOfAqiEvent());
context.read<DeviceLocationBloc>().add(const ClearDeviceLocationEvent());
} }
static void loadAnalyticsDevices( static void loadAnalyticsDevices(
@@ -61,12 +65,21 @@ abstract final class FetchAirQualityDataHelper {
communityUuid: communityUuid, communityUuid: communityUuid,
spaceUuid: spaceUuid, spaceUuid: spaceUuid,
deviceTypes: ['AQI'], deviceTypes: ['AQI'],
requestType: AnalyticsDeviceRequestType.energyManagement, requestType: AnalyticsDeviceRequestType.occupancy,
), ),
onSuccess: (device) { onSuccess: (device) {
context.read<RealtimeDeviceChangesBloc>() context.read<RealtimeDeviceChangesBloc>()
..add(const RealtimeDeviceChangesClosed()) ..add(const RealtimeDeviceChangesClosed())
..add(RealtimeDeviceChangesStarted(device.uuid)); ..add(RealtimeDeviceChangesStarted(device.uuid));
context.read<DeviceLocationBloc>().add(
LoadDeviceLocationEvent(
GetDeviceLocationDataParam(
latitude: device.latitude ?? 0,
longitude: device.longitude ?? 0,
),
),
);
}, },
), ),
); );
@@ -72,55 +72,54 @@ class AqiDeviceInfo extends StatelessWidget {
return Container( return Container(
decoration: secondarySection.copyWith(boxShadow: const []), decoration: secondarySection.copyWith(boxShadow: const []),
padding: const EdgeInsetsDirectional.all(20), padding: const EdgeInsetsDirectional.all(20),
child: Expanded( child: Column(
child: Column( spacing: 6,
spacing: 6, mainAxisSize: MainAxisSize.max,
children: [ children: [
const AirQualityEndSideLiveIndicator(), const AirQualityEndSideLiveIndicator(),
AirQualityEndSideGaugeAndInfo( AirQualityEndSideGaugeAndInfo(
aqiLevel: status aqiLevel: status
.firstWhere( .firstWhere(
(e) => e.code == 'air_quality_index', (e) => e.code == 'air_quality_index',
orElse: () => Status(code: 'air_quality_index', value: ''), orElse: () => Status(code: 'air_quality_index', value: ''),
) )
.value .value
.toString(), .toString(),
temperature: int.parse(tempValue), temperature: int.parse(tempValue),
humidity: int.parse(humidityValue), humidity: int.parse(humidityValue),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
AqiSubValueWidget( AqiSubValueWidget(
range: (0, 999), range: (0, 999),
label: AqiType.pm25.value, label: AqiType.pm25.value,
value: pm25Value, value: pm25Value,
unit: AqiType.pm25.unit, unit: AqiType.pm25.unit,
), ),
AqiSubValueWidget( AqiSubValueWidget(
range: (0, 999), range: (0, 999),
label: AqiType.pm10.value, label: AqiType.pm10.value,
value: pm10Value, value: pm10Value,
unit: AqiType.pm10.unit, unit: AqiType.pm10.unit,
), ),
AqiSubValueWidget( AqiSubValueWidget(
range: (0, 5), range: (0, 5),
label: AqiType.hcho.value, label: AqiType.hcho.value,
value: ch2oValue, value: ch2oValue,
unit: AqiType.hcho.unit, unit: AqiType.hcho.unit,
), ),
AqiSubValueWidget( AqiSubValueWidget(
range: (0, 999), range: (0, 999),
label: AqiType.tvoc.value, label: AqiType.tvoc.value,
value: tvocValue, value: tvocValue,
unit: AqiType.tvoc.unit, unit: AqiType.tvoc.unit,
), ),
AqiSubValueWidget( AqiSubValueWidget(
range: (0, 5000), range: (0, 5000),
label: AqiType.co2.value, label: AqiType.co2.value,
value: co2Value, value: co2Value,
unit: AqiType.co2.unit, unit: AqiType.co2.unit,
), ),
], ],
),
), ),
); );
}, },
@@ -6,7 +6,34 @@ import 'package:syncrow_web/utils/extension/build_context_x.dart';
import 'package:syncrow_web/utils/style.dart'; import 'package:syncrow_web/utils/style.dart';
class AqiLocation extends StatelessWidget { 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -24,7 +51,7 @@ class AqiLocation extends StatelessWidget {
_buildLocationPin(), _buildLocationPin(),
Expanded( Expanded(
child: Text( child: Text(
'Business Bay, Dubai - UAE', _getFormattedLocation(),
style: context.textTheme.bodySmall?.copyWith( style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.textPrimaryColor, color: ColorsManager.textPrimaryColor,
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
@@ -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/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.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_location_info_cell.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_location_info_cell.dart';
import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/constants/assets.dart';
@@ -9,37 +11,46 @@ class AqiLocationInfo extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return BlocBuilder<DeviceLocationBloc, DeviceLocationState>(
decoration: secondarySection.copyWith(boxShadow: const []), builder: (context, state) {
padding: const EdgeInsetsDirectional.all(20), final info = state.locationInfo;
child: const Column( return Container(
spacing: 8, decoration: secondarySection.copyWith(boxShadow: const []),
children: [ padding: const EdgeInsetsDirectional.all(20),
AqiLocation(), child: Column(
Expanded( spacing: 8,
child: Row( children: [
spacing: 8, AqiLocation(
children: [ city: info?.city,
AqiLocationInfoCell( country: info?.country,
label: 'Temperature', address: info?.address,
value: ' 25°', ),
svgPath: Assets.aqiTemperature, 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,
),
],
),
), ),
], );
), },
); );
} }
} }
@@ -3,6 +3,7 @@ import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/analytics/models/analytics_device.dart'; import 'package:syncrow_web/pages/analytics/models/analytics_device.dart';
import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart'; import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart';
import 'package:syncrow_web/pages/analytics/services/analytics_devices/analytics_devices_service.dart'; import 'package:syncrow_web/pages/analytics/services/analytics_devices/analytics_devices_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
part 'analytics_devices_event.dart'; part 'analytics_devices_event.dart';
part 'analytics_devices_state.dart'; part 'analytics_devices_state.dart';
@@ -36,6 +37,13 @@ class AnalyticsDevicesBloc
if (devices.isNotEmpty) { if (devices.isNotEmpty) {
event.onSuccess(devices.first); event.onSuccess(devices.first);
} }
} on APIException catch (e) {
emit(
AnalyticsDevicesState(
status: AnalyticsDevicesStatus.failure,
errorMessage: e.message,
),
);
} catch (e) { } catch (e) {
emit( emit(
AnalyticsDevicesState( AnalyticsDevicesState(
@@ -1,6 +1,8 @@
import 'package:dio/dio.dart';
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/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/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/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';
@@ -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/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';
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_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/energy_consumption_per_device/remote_energy_consumption_per_device_service.dart';
import 'package:syncrow_web/pages/analytics/services/occupacy/remote_occupancy_service.dart'; import 'package:syncrow_web/pages/analytics/services/occupacy/remote_occupancy_service.dart';
@@ -108,6 +112,18 @@ class _AnalyticsPageState extends State<AnalyticsPage> {
FakeAirQualityDistributionService(), FakeAirQualityDistributionService(),
), ),
), ),
BlocProvider(
create: (context) => DeviceLocationBloc(
DeviceLocationDetailsServiceDecorator(
RemoteDeviceLocationService(_httpService),
Dio(
BaseOptions(
baseUrl: 'https://nominatim.openstreetmap.org/',
),
),
),
),
),
], ],
child: const AnalyticsPageForm(), child: const AnalyticsPageForm(),
); );
@@ -3,6 +3,7 @@ import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/analytics/models/phases_energy_consumption.dart'; import 'package:syncrow_web/pages/analytics/models/phases_energy_consumption.dart';
import 'package:syncrow_web/pages/analytics/params/get_energy_consumption_by_phases_param.dart'; import 'package:syncrow_web/pages/analytics/params/get_energy_consumption_by_phases_param.dart';
import 'package:syncrow_web/pages/analytics/services/energy_consumption_by_phases/energy_consumption_by_phases_service.dart'; import 'package:syncrow_web/pages/analytics/services/energy_consumption_by_phases/energy_consumption_by_phases_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
part 'energy_consumption_by_phases_event.dart'; part 'energy_consumption_by_phases_event.dart';
part 'energy_consumption_by_phases_state.dart'; part 'energy_consumption_by_phases_state.dart';
@@ -31,6 +32,13 @@ class EnergyConsumptionByPhasesBloc
chartData: chartData, chartData: chartData,
), ),
); );
} on APIException catch (e) {
emit(
state.copyWith(
status: EnergyConsumptionByPhasesStatus.failure,
errorMessage: e.message,
),
);
} catch (e) { } catch (e) {
emit( emit(
state.copyWith( state.copyWith(
@@ -3,6 +3,7 @@ import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/analytics/models/device_energy_data_model.dart'; import 'package:syncrow_web/pages/analytics/models/device_energy_data_model.dart';
import 'package:syncrow_web/pages/analytics/params/get_energy_consumption_per_device_param.dart'; import 'package:syncrow_web/pages/analytics/params/get_energy_consumption_per_device_param.dart';
import 'package:syncrow_web/pages/analytics/services/energy_consumption_per_device/energy_consumption_per_device_service.dart'; import 'package:syncrow_web/pages/analytics/services/energy_consumption_per_device/energy_consumption_per_device_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
part 'energy_consumption_per_device_event.dart'; part 'energy_consumption_per_device_event.dart';
part 'energy_consumption_per_device_state.dart'; part 'energy_consumption_per_device_state.dart';
@@ -13,7 +14,8 @@ class EnergyConsumptionPerDeviceBloc
this._energyConsumptionPerDeviceService, this._energyConsumptionPerDeviceService,
) : super(const EnergyConsumptionPerDeviceState()) { ) : super(const EnergyConsumptionPerDeviceState()) {
on<LoadEnergyConsumptionPerDeviceEvent>(_onLoadEnergyConsumptionPerDeviceEvent); on<LoadEnergyConsumptionPerDeviceEvent>(_onLoadEnergyConsumptionPerDeviceEvent);
on<ClearEnergyConsumptionPerDeviceEvent>(_onClearEnergyConsumptionPerDeviceEvent); on<ClearEnergyConsumptionPerDeviceEvent>(
_onClearEnergyConsumptionPerDeviceEvent);
} }
final EnergyConsumptionPerDeviceService _energyConsumptionPerDeviceService; final EnergyConsumptionPerDeviceService _energyConsumptionPerDeviceService;
@@ -31,6 +33,13 @@ class EnergyConsumptionPerDeviceBloc
chartData: chartData, chartData: chartData,
), ),
); );
} on APIException catch (e) {
emit(
state.copyWith(
status: EnergyConsumptionPerDeviceStatus.failure,
errorMessage: e.message,
),
);
} catch (e) { } catch (e) {
emit( emit(
state.copyWith( state.copyWith(
@@ -3,6 +3,7 @@ import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/analytics/services/power_clamp_info/power_clamp_info_service.dart'; import 'package:syncrow_web/pages/analytics/services/power_clamp_info/power_clamp_info_service.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
import 'package:syncrow_web/pages/device_managment/power_clamp/models/power_clamp_model.dart'; import 'package:syncrow_web/pages/device_managment/power_clamp/models/power_clamp_model.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
part 'power_clamp_info_event.dart'; part 'power_clamp_info_event.dart';
part 'power_clamp_info_state.dart'; part 'power_clamp_info_state.dart';
@@ -31,6 +32,13 @@ class PowerClampInfoBloc extends Bloc<PowerClampInfoEvent, PowerClampInfoState>
powerClampModel: powerClampModel, powerClampModel: powerClampModel,
), ),
); );
} on APIException catch (e) {
emit(
state.copyWith(
status: PowerClampInfoStatus.error,
errorMessage: e.message,
),
);
} catch (e) { } catch (e) {
emit( emit(
state.copyWith( state.copyWith(
@@ -3,6 +3,7 @@ import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/analytics/models/energy_data_model.dart'; import 'package:syncrow_web/pages/analytics/models/energy_data_model.dart';
import 'package:syncrow_web/pages/analytics/params/get_total_energy_consumption_param.dart'; import 'package:syncrow_web/pages/analytics/params/get_total_energy_consumption_param.dart';
import 'package:syncrow_web/pages/analytics/services/total_energy_consumption/total_energy_consumption_service.dart'; import 'package:syncrow_web/pages/analytics/services/total_energy_consumption/total_energy_consumption_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
part 'total_energy_consumption_event.dart'; part 'total_energy_consumption_event.dart';
part 'total_energy_consumption_state.dart'; part 'total_energy_consumption_state.dart';
@@ -31,6 +32,13 @@ class TotalEnergyConsumptionBloc
status: TotalEnergyConsumptionStatus.loaded, status: TotalEnergyConsumptionStatus.loaded,
), ),
); );
} on APIException catch (e) {
emit(
state.copyWith(
errorMessage: e.message,
status: TotalEnergyConsumptionStatus.failure,
),
);
} catch (e) { } catch (e) {
emit( emit(
state.copyWith( state.copyWith(
@@ -3,6 +3,7 @@ import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/analytics/models/occupacy.dart'; import 'package:syncrow_web/pages/analytics/models/occupacy.dart';
import 'package:syncrow_web/pages/analytics/params/get_occupancy_param.dart'; import 'package:syncrow_web/pages/analytics/params/get_occupancy_param.dart';
import 'package:syncrow_web/pages/analytics/services/occupacy/occupacy_service.dart'; import 'package:syncrow_web/pages/analytics/services/occupacy/occupacy_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
part 'occupancy_event.dart'; part 'occupancy_event.dart';
part 'occupancy_state.dart'; part 'occupancy_state.dart';
@@ -23,6 +24,8 @@ class OccupancyBloc extends Bloc<OccupancyEvent, OccupancyState> {
try { try {
final chartData = await _occupacyService.load(event.param); final chartData = await _occupacyService.load(event.param);
emit(state.copyWith(chartData: chartData, status: OccupancyStatus.loaded)); emit(state.copyWith(chartData: chartData, status: OccupancyStatus.loaded));
} on APIException catch (e) {
emit(state.copyWith(status: OccupancyStatus.failure, errorMessage: e.message));
} catch (e) { } catch (e) {
emit(state.copyWith(status: OccupancyStatus.failure, errorMessage: '$e')); emit(state.copyWith(status: OccupancyStatus.failure, errorMessage: '$e'));
} }
@@ -3,6 +3,7 @@ import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/analytics/models/occupancy_heat_map_model.dart'; import 'package:syncrow_web/pages/analytics/models/occupancy_heat_map_model.dart';
import 'package:syncrow_web/pages/analytics/params/get_occupancy_heat_map_param.dart'; import 'package:syncrow_web/pages/analytics/params/get_occupancy_heat_map_param.dart';
import 'package:syncrow_web/pages/analytics/services/occupancy_heat_map/occupancy_heat_map_service.dart'; import 'package:syncrow_web/pages/analytics/services/occupancy_heat_map/occupancy_heat_map_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
part 'occupancy_heat_map_event.dart'; part 'occupancy_heat_map_event.dart';
part 'occupancy_heat_map_state.dart'; part 'occupancy_heat_map_state.dart';
@@ -30,6 +31,13 @@ class OccupancyHeatMapBloc
heatMapData: occupancyHeatMap, heatMapData: occupancyHeatMap,
), ),
); );
} on APIException catch (e) {
emit(
state.copyWith(
status: OccupancyHeatMapStatus.failure,
errorMessage: e.message,
),
);
} catch (e) { } catch (e) {
emit( emit(
state.copyWith( state.copyWith(
@@ -0,0 +1,11 @@
class GetDeviceLocationDataParam {
const GetDeviceLocationDataParam({
required this.latitude,
required this.longitude,
});
final double latitude;
final double longitude;
Map<String, dynamic> toJson() => {'lat': latitude, 'lon': longitude};
}
@@ -1,6 +1,8 @@
import 'package:dio/dio.dart';
import 'package:syncrow_web/pages/analytics/models/analytics_device.dart'; import 'package:syncrow_web/pages/analytics/models/analytics_device.dart';
import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart'; import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart';
import 'package:syncrow_web/pages/analytics/services/analytics_devices/analytics_devices_service.dart'; import 'package:syncrow_web/pages/analytics/services/analytics_devices/analytics_devices_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/services/api/http_service.dart';
final class RemoteEnergyManagementAnalyticsDevicesService final class RemoteEnergyManagementAnalyticsDevicesService
@@ -9,6 +11,8 @@ final class RemoteEnergyManagementAnalyticsDevicesService
final HTTPService _httpService; final HTTPService _httpService;
static const _defaultErrorMessage = 'Failed to load analytics devices';
@override @override
Future<List<AnalyticsDevice>> getDevices(GetAnalyticsDevicesParam param) async { Future<List<AnalyticsDevice>> getDevices(GetAnalyticsDevicesParam param) async {
try { try {
@@ -29,8 +33,14 @@ final class RemoteEnergyManagementAnalyticsDevicesService
); );
return response; return response;
} on DioException catch (e) {
final message = e.response?.data as Map<String, dynamic>?;
final error = message?['error'] as Map<String, dynamic>?;
final errorMessage = error?['error'] as String? ?? '';
final formattedErrorMessage = [_defaultErrorMessage, errorMessage].join(': ');
throw APIException(formattedErrorMessage);
} catch (e) { } catch (e) {
throw Exception('Failed to load total energy consumption: $e'); throw APIException('$_defaultErrorMessage: $e');
} }
} }
} }
@@ -1,7 +1,9 @@
import 'package:dio/dio.dart';
import 'package:syncrow_web/pages/analytics/models/analytics_device.dart'; import 'package:syncrow_web/pages/analytics/models/analytics_device.dart';
import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart'; import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart';
import 'package:syncrow_web/pages/analytics/services/analytics_devices/analytics_devices_service.dart'; import 'package:syncrow_web/pages/analytics/services/analytics_devices/analytics_devices_service.dart';
import 'package:syncrow_web/pages/common/bloc/project_manager.dart'; import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/services/api/http_service.dart';
class RemoteOccupancyAnalyticsDevicesService implements AnalyticsDevicesService { class RemoteOccupancyAnalyticsDevicesService implements AnalyticsDevicesService {
@@ -9,6 +11,8 @@ class RemoteOccupancyAnalyticsDevicesService implements AnalyticsDevicesService
final HTTPService _httpService; final HTTPService _httpService;
static const _defaultErrorMessage = 'Failed to load analytics devices';
@override @override
Future<List<AnalyticsDevice>> getDevices(GetAnalyticsDevicesParam param) async { Future<List<AnalyticsDevice>> getDevices(GetAnalyticsDevicesParam param) async {
try { try {
@@ -26,8 +30,15 @@ class RemoteOccupancyAnalyticsDevicesService implements AnalyticsDevicesService
final result = requests.map((e) => e.first).toList(); final result = requests.map((e) => e.first).toList();
return result; return result;
} on DioException catch (e) {
final message = e.response?.data as Map<String, dynamic>?;
final error = message?['error'] as Map<String, dynamic>?;
final errorMessage = error?['error'] as String? ?? '';
final formattedErrorMessage = [_defaultErrorMessage, errorMessage].join(': ');
throw APIException(formattedErrorMessage);
} catch (e) { } catch (e) {
throw Exception('Failed to load total energy consumption: $e'); final formattedErrorMessage = [_defaultErrorMessage, e.toString()].join(': ');
throw APIException(formattedErrorMessage);
} }
} }
@@ -54,8 +65,14 @@ class RemoteOccupancyAnalyticsDevicesService implements AnalyticsDevicesService
}, },
); );
return response; return response;
} on DioException catch (e) {
final message = e.response?.data as Map<String, dynamic>?;
final error = message?['error'] as Map<String, dynamic>?;
final errorMessage = error?['error'] as String? ?? '';
final formattedErrorMessage = [_defaultErrorMessage, errorMessage].join(': ');
throw APIException(formattedErrorMessage);
} catch (e) { } catch (e) {
rethrow; throw APIException('$_defaultErrorMessage: $e');
} }
} }
} }
@@ -0,0 +1,40 @@
import 'package:dio/dio.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';
class DeviceLocationDetailsServiceDecorator implements DeviceLocationService {
const DeviceLocationDetailsServiceDecorator(this._decoratee, this._dio);
final DeviceLocationService _decoratee;
final Dio _dio;
@override
Future<DeviceLocationInfo> get(GetDeviceLocationDataParam param) async {
try {
final deviceLocationInfo = await _decoratee.get(param);
final response = await _dio.get<Map<String, dynamic>>(
'reverse',
queryParameters: {
'format': 'json',
'lat': param.latitude,
'lon': param.longitude,
},
);
final data = response.data;
if (data != null) {
final addressData = data['address'] as Map<String, dynamic>;
return deviceLocationInfo.copyWith(
city: addressData['city'],
country: addressData['country_code'].toString().toUpperCase(),
address: addressData['state'],
);
}
return deviceLocationInfo;
} catch (e) {
throw Exception('Failed to load device location info: ${e.toString()}');
}
}
}
@@ -0,0 +1,6 @@
import 'package:syncrow_web/pages/analytics/models/device_location_info.dart';
import 'package:syncrow_web/pages/analytics/params/get_device_location_data_param.dart';
abstract interface class DeviceLocationService {
Future<DeviceLocationInfo> get(GetDeviceLocationDataParam param);
}
@@ -0,0 +1,37 @@
import 'package:dio/dio.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';
import 'package:syncrow_web/services/api/http_service.dart';
class RemoteDeviceLocationService implements DeviceLocationService {
const RemoteDeviceLocationService(this._httpService);
final HTTPService _httpService;
static const _defaultErrorMessage = 'Failed to load device location';
@override
Future<DeviceLocationInfo> get(GetDeviceLocationDataParam param) async {
try {
final response = await _httpService.get(
path: '/weather',
queryParameters: param.toJson(),
expectedResponseModel: (data) {
final response = data as Map<String, dynamic>;
final location = response['data'] as Map<String, dynamic>;
return DeviceLocationInfo.fromJson(location);
},
);
return response;
} on DioException catch (e) {
final message = e.response?.data as Map<String, dynamic>?;
final error = message?['error'] as Map<String, dynamic>?;
final errorMessage = error?['error'] as String? ?? '';
throw Exception(errorMessage);
} catch (e) {
throw Exception('$_defaultErrorMessage: $e');
}
}
}
@@ -1,6 +1,8 @@
import 'package:dio/dio.dart';
import 'package:syncrow_web/pages/analytics/models/phases_energy_consumption.dart'; import 'package:syncrow_web/pages/analytics/models/phases_energy_consumption.dart';
import 'package:syncrow_web/pages/analytics/params/get_energy_consumption_by_phases_param.dart'; import 'package:syncrow_web/pages/analytics/params/get_energy_consumption_by_phases_param.dart';
import 'package:syncrow_web/pages/analytics/services/energy_consumption_by_phases/energy_consumption_by_phases_service.dart'; import 'package:syncrow_web/pages/analytics/services/energy_consumption_by_phases/energy_consumption_by_phases_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/services/api/http_service.dart';
final class RemoteEnergyConsumptionByPhasesService final class RemoteEnergyConsumptionByPhasesService
@@ -9,6 +11,8 @@ final class RemoteEnergyConsumptionByPhasesService
final HTTPService _httpService; final HTTPService _httpService;
static const _defaultErrorMessage = 'Failed to load energy consumption per phase';
@override @override
Future<List<PhasesEnergyConsumption>> load( Future<List<PhasesEnergyConsumption>> load(
GetEnergyConsumptionByPhasesParam param, GetEnergyConsumptionByPhasesParam param,
@@ -28,8 +32,15 @@ final class RemoteEnergyConsumptionByPhasesService
}, },
); );
return response; return response;
} on DioException catch (e) {
final message = e.response?.data as Map<String, dynamic>?;
final error = message?['error'] as Map<String, dynamic>?;
final errorMessage = error?['error'] as String? ?? '';
final formattedErrorMessage = [_defaultErrorMessage, errorMessage].join(': ');
throw APIException(formattedErrorMessage);
} catch (e) { } catch (e) {
throw Exception('Failed to load energy consumption per phase: $e'); final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': ');
throw APIException(formattedErrorMessage);
} }
} }
} }
@@ -1,8 +1,10 @@
import 'package:dio/dio.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/analytics/models/device_energy_data_model.dart'; import 'package:syncrow_web/pages/analytics/models/device_energy_data_model.dart';
import 'package:syncrow_web/pages/analytics/models/energy_data_model.dart'; import 'package:syncrow_web/pages/analytics/models/energy_data_model.dart';
import 'package:syncrow_web/pages/analytics/params/get_energy_consumption_per_device_param.dart'; import 'package:syncrow_web/pages/analytics/params/get_energy_consumption_per_device_param.dart';
import 'package:syncrow_web/pages/analytics/services/energy_consumption_per_device/energy_consumption_per_device_service.dart'; import 'package:syncrow_web/pages/analytics/services/energy_consumption_per_device/energy_consumption_per_device_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/services/api/http_service.dart';
class RemoteEnergyConsumptionPerDeviceService class RemoteEnergyConsumptionPerDeviceService
@@ -11,6 +13,8 @@ class RemoteEnergyConsumptionPerDeviceService
final HTTPService _httpService; final HTTPService _httpService;
static const _defaultErrorMessage = 'Failed to load energy consumption per device';
@override @override
Future<List<DeviceEnergyDataModel>> load( Future<List<DeviceEnergyDataModel>> load(
GetEnergyConsumptionPerDeviceParam param, GetEnergyConsumptionPerDeviceParam param,
@@ -23,8 +27,15 @@ class RemoteEnergyConsumptionPerDeviceService
expectedResponseModel: _EnergyConsumptionPerDeviceMapper.map, expectedResponseModel: _EnergyConsumptionPerDeviceMapper.map,
); );
return response; return response;
} on DioException catch (e) {
final message = e.response?.data as Map<String, dynamic>?;
final error = message?['error'] as Map<String, dynamic>?;
final errorMessage = error?['error'] as String? ?? '';
final formattedErrorMessage = [_defaultErrorMessage, errorMessage].join(': ');
throw APIException(formattedErrorMessage);
} catch (e) { } catch (e) {
throw Exception('Failed to load energy consumption per device: $e'); final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': ');
throw APIException(formattedErrorMessage);
} }
} }
} }
@@ -1,6 +1,8 @@
import 'package:dio/dio.dart';
import 'package:syncrow_web/pages/analytics/models/occupacy.dart'; import 'package:syncrow_web/pages/analytics/models/occupacy.dart';
import 'package:syncrow_web/pages/analytics/params/get_occupancy_param.dart'; import 'package:syncrow_web/pages/analytics/params/get_occupancy_param.dart';
import 'package:syncrow_web/pages/analytics/services/occupacy/occupacy_service.dart'; import 'package:syncrow_web/pages/analytics/services/occupacy/occupacy_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/services/api/http_service.dart';
final class RemoteOccupancyService implements OccupacyService { final class RemoteOccupancyService implements OccupacyService {
@@ -8,6 +10,8 @@ final class RemoteOccupancyService implements OccupacyService {
final HTTPService _httpService; final HTTPService _httpService;
static const _defaultErrorMessage = 'Failed to load occupancy';
@override @override
Future<List<Occupacy>> load(GetOccupancyParam param) async { Future<List<Occupacy>> load(GetOccupancyParam param) async {
try { try {
@@ -25,8 +29,15 @@ final class RemoteOccupancyService implements OccupacyService {
}, },
); );
return response; return response;
} on DioException catch (e) {
final message = e.response?.data as Map<String, dynamic>?;
final error = message?['error'] as Map<String, dynamic>?;
final errorMessage = error?['error'] as String? ?? '';
final formattedErrorMessage = [_defaultErrorMessage, errorMessage].join(': ');
throw APIException(formattedErrorMessage);
} catch (e) { } catch (e) {
throw Exception('Failed to load energy consumption per phase: $e'); final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': ');
throw APIException(formattedErrorMessage);
} }
} }
} }
@@ -1,6 +1,8 @@
import 'package:dio/dio.dart';
import 'package:syncrow_web/pages/analytics/models/occupancy_heat_map_model.dart'; import 'package:syncrow_web/pages/analytics/models/occupancy_heat_map_model.dart';
import 'package:syncrow_web/pages/analytics/params/get_occupancy_heat_map_param.dart'; import 'package:syncrow_web/pages/analytics/params/get_occupancy_heat_map_param.dart';
import 'package:syncrow_web/pages/analytics/services/occupancy_heat_map/occupancy_heat_map_service.dart'; import 'package:syncrow_web/pages/analytics/services/occupancy_heat_map/occupancy_heat_map_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/services/api/http_service.dart';
final class RemoteOccupancyHeatMapService implements OccupancyHeatMapService { final class RemoteOccupancyHeatMapService implements OccupancyHeatMapService {
@@ -8,6 +10,8 @@ final class RemoteOccupancyHeatMapService implements OccupancyHeatMapService {
final HTTPService _httpService; final HTTPService _httpService;
static const _defaultErrorMessage = 'Failed to load occupancy heat map';
@override @override
Future<List<OccupancyHeatMapModel>> load(GetOccupancyHeatMapParam param) async { Future<List<OccupancyHeatMapModel>> load(GetOccupancyHeatMapParam param) async {
try { try {
@@ -28,8 +32,15 @@ final class RemoteOccupancyHeatMapService implements OccupancyHeatMapService {
); );
return response; return response;
} on DioException catch (e) {
final message = e.response?.data as Map<String, dynamic>?;
final error = message?['error'] as Map<String, dynamic>?;
final errorMessage = error?['error'] as String? ?? '';
final formattedErrorMessage = [_defaultErrorMessage, errorMessage].join(': ');
throw APIException(formattedErrorMessage);
} catch (e) { } catch (e) {
throw Exception('Failed to load total energy consumption:'); final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': ');
throw APIException(formattedErrorMessage);
} }
} }
} }
@@ -1,5 +1,7 @@
import 'package:dio/dio.dart';
import 'package:syncrow_web/pages/analytics/services/power_clamp_info/power_clamp_info_service.dart'; import 'package:syncrow_web/pages/analytics/services/power_clamp_info/power_clamp_info_service.dart';
import 'package:syncrow_web/pages/device_managment/power_clamp/models/power_clamp_model.dart'; import 'package:syncrow_web/pages/device_managment/power_clamp/models/power_clamp_model.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/services/api/http_service.dart';
final class RemotePowerClampInfoService implements PowerClampInfoService { final class RemotePowerClampInfoService implements PowerClampInfoService {
@@ -7,6 +9,8 @@ final class RemotePowerClampInfoService implements PowerClampInfoService {
final HTTPService _httpService; final HTTPService _httpService;
static const _defaultErrorMessage = 'Failed to fetch power clamp info';
@override @override
Future<PowerClampModel> getInfo(String deviceId) async { Future<PowerClampModel> getInfo(String deviceId) async {
try { try {
@@ -20,8 +24,15 @@ final class RemotePowerClampInfoService implements PowerClampInfoService {
}, },
); );
return response; return response;
} on DioException catch (e) {
final message = e.response?.data as Map<String, dynamic>?;
final error = message?['error'] as Map<String, dynamic>?;
final errorMessage = error?['error'] as String? ?? '';
final formattedErrorMessage = [_defaultErrorMessage, errorMessage].join(': ');
throw APIException(formattedErrorMessage);
} catch (e) { } catch (e) {
throw Exception('Failed to fetch power clamp info: $e'); final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': ');
throw APIException(formattedErrorMessage);
} }
} }
} }
@@ -1,6 +1,8 @@
import 'package:dio/dio.dart';
import 'package:syncrow_web/pages/analytics/models/energy_data_model.dart'; import 'package:syncrow_web/pages/analytics/models/energy_data_model.dart';
import 'package:syncrow_web/pages/analytics/params/get_total_energy_consumption_param.dart'; import 'package:syncrow_web/pages/analytics/params/get_total_energy_consumption_param.dart';
import 'package:syncrow_web/pages/analytics/services/total_energy_consumption/total_energy_consumption_service.dart'; import 'package:syncrow_web/pages/analytics/services/total_energy_consumption/total_energy_consumption_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/services/api/http_service.dart';
class RemoteTotalEnergyConsumptionService implements TotalEnergyConsumptionService { class RemoteTotalEnergyConsumptionService implements TotalEnergyConsumptionService {
@@ -8,6 +10,8 @@ class RemoteTotalEnergyConsumptionService implements TotalEnergyConsumptionServi
final HTTPService _httpService; final HTTPService _httpService;
static const _defaultErrorMessage = 'Failed to load total energy consumption';
@override @override
Future<List<EnergyDataModel>> load( Future<List<EnergyDataModel>> load(
GetTotalEnergyConsumptionParam param, GetTotalEnergyConsumptionParam param,
@@ -21,8 +25,15 @@ class RemoteTotalEnergyConsumptionService implements TotalEnergyConsumptionServi
); );
return response; return response;
} on DioException catch (e) {
final message = e.response?.data as Map<String, dynamic>?;
final error = message?['error'] as Map<String, dynamic>?;
final errorMessage = error?['error'] as String? ?? '';
final formattedErrorMessage = [_defaultErrorMessage, errorMessage].join(': ');
throw APIException(formattedErrorMessage);
} catch (e) { } catch (e) {
throw Exception('Failed to load total energy consumption: $e'); final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': ');
throw APIException(formattedErrorMessage);
} }
} }
} }
@@ -1,10 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_svg/svg.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/device_info_model.dart'; import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/device_info_model.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/sub_space_model.dart'; import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/sub_space_model.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/sub_space_dialog.dart'; import 'package:syncrow_web/pages/device_managment/device_setting/sub_space_dialog.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart';
import 'package:syncrow_web/web_layout/default_container.dart'; import 'package:syncrow_web/web_layout/default_container.dart';
@@ -28,7 +30,7 @@ class DeviceManagementContent extends StatelessWidget {
Widget? trailing, Widget? trailing,
required Color? valueColor}) { required Color? valueColor}) {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 6.0), padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 10),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
@@ -39,6 +41,7 @@ class DeviceManagementContent extends StatelessWidget {
color: ColorsManager.grayColor, color: ColorsManager.grayColor,
), ),
), ),
const SizedBox(width: 15),
Expanded( Expanded(
child: Text( child: Text(
value, value,
@@ -48,7 +51,7 @@ class DeviceManagementContent extends StatelessWidget {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 12),
trailing ?? const SizedBox.shrink(), trailing ?? const SizedBox.shrink(),
], ],
), ),
@@ -73,15 +76,15 @@ class DeviceManagementContent extends StatelessWidget {
); );
}, },
child: infoRow( child: infoRow(
label: 'Sub-Space:', label: 'Sub-Space:',
value: deviceInfo.subspace.subspaceName, value: deviceInfo.subspace.subspaceName,
valueColor: ColorsManager.textGray, valueColor: ColorsManager.blackColor,
trailing: const Icon( trailing: SvgPicture.asset(
Icons.arrow_forward_ios, Assets.arrowDown,
size: 16, width: 10,
color: ColorsManager.greyColor, height: 10,
), color: ColorsManager.greyColor,
), )),
), ),
), ),
const Divider(color: ColorsManager.dividerColor), const Divider(color: ColorsManager.dividerColor),
@@ -104,7 +107,7 @@ class DeviceManagementContent extends StatelessWidget {
}, },
child: const Icon( child: const Icon(
Icons.copy, Icons.copy,
size: 16, size: 15,
color: ColorsManager.greyColor, color: ColorsManager.greyColor,
), ),
), ),
@@ -51,8 +51,7 @@ class DeviceSettingsPanel extends StatelessWidget {
Container( Container(
width: MediaQuery.of(context).size.width * 0.3, width: MediaQuery.of(context).size.width * 0.3,
color: ColorsManager.grey25, color: ColorsManager.grey25,
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.all(10),
horizontal: 20, vertical: 24),
child: ListView( child: ListView(
children: [ children: [
Row( Row(
@@ -70,37 +69,43 @@ class DeviceSettingsPanel extends StatelessWidget {
children: [ children: [
Text( Text(
'Device Settings', 'Device Settings',
style: style: context.theme.textTheme.titleLarge!
context.theme.textTheme.titleLarge!.copyWith( .copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.w700,
color: ColorsManager.primaryColor, color: ColorsManager.vividBlue
), .withOpacity(0.7),
fontSize: 24),
), ),
], ],
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
DefaultContainer( DefaultContainer(
borderRadius: BorderRadius.circular(15),
child: Row( child: Row(
children: [ children: [
CircleAvatar( Padding(
radius: 40, padding: const EdgeInsets.only(left: 15),
backgroundColor:
const Color.fromARGB(177, 213, 213, 213),
child: CircleAvatar( child: CircleAvatar(
backgroundColor: ColorsManager.whiteColors, radius: 38,
radius: 36, backgroundColor:
child: SvgPicture.asset( ColorsManager.grayBorder.withOpacity(0.5),
iconPath, child: CircleAvatar(
fit: BoxFit.cover, backgroundColor: ColorsManager.whiteColors,
radius: 36,
child: SvgPicture.asset(
iconPath,
fit: BoxFit.cover,
),
), ),
), ),
), ),
const SizedBox(width: 12), const SizedBox(width: 25),
Expanded( Expanded(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const SizedBox(height: 15),
Text( Text(
'Device Name:', 'Device Name:',
style: context.textTheme.bodyMedium! style: context.textTheme.bodyMedium!
@@ -108,50 +113,79 @@ class DeviceSettingsPanel extends StatelessWidget {
color: ColorsManager.grayColor, color: ColorsManager.grayColor,
), ),
), ),
TextFormField( SizedBox(
maxLength: 30, height: 35,
style: const TextStyle( child: Row(
color: ColorsManager.blackColor, children: [
), SizedBox(
textAlign: TextAlign.start, height: 50,
focusNode: _bloc.focusNode, width: 190,
controller: _bloc.nameController, child: TextFormField(
enabled: _bloc.editName, scrollPadding: EdgeInsets.zero,
onFieldSubmitted: (value) { maxLength: 30,
_bloc.add(const ChangeNameEvent( style: const TextStyle(
value: false)); color: ColorsManager.blackColor,
}, fontSize: 16,
decoration: const InputDecoration( ),
border: InputBorder.none, textAlign: TextAlign.start,
fillColor: Colors.white10, focusNode: _bloc.focusNode,
counterText: '', controller: _bloc.nameController,
enabled: _bloc.editName,
onFieldSubmitted: (value) {
_bloc.add(const ChangeNameEvent(
value: false));
},
decoration: InputDecoration(
isDense: true,
contentPadding: EdgeInsets.zero,
border: InputBorder.none,
fillColor: Colors.white10,
counterText: '',
),
),
),
Column(
children: [
SizedBox(
width: 15,
height: 25,
child: Visibility(
visible:
_bloc.editName != true,
replacement: const SizedBox(),
child: InkWell(
onTap: () {
_bloc.add(
const ChangeNameEvent(
value: true));
},
child: SvgPicture.asset(
Assets
.editNameIconSettings,
color: ColorsManager
.lightGrayBorderColor,
height: 15,
width: 15,
),
),
),
),
],
),
],
), ),
), ),
], ],
), ),
), ),
const SizedBox(width: 8),
Visibility(
visible: _bloc.editName != true,
replacement: const SizedBox(),
child: GestureDetector(
onTap: () {
_bloc.add(
const ChangeNameEvent(value: true));
},
child: SvgPicture.asset(
Assets.editNameIconSettings,
color: ColorsManager.grayColor,
height: 20,
width: 20,
),
),
)
], ],
), ),
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
Text('Device Management', style: sectionTitle), Padding(
padding: const EdgeInsets.only(left: 10),
child: Text('Device Management', style: sectionTitle),
),
DeviceManagementContent( DeviceManagementContent(
device: device, device: device,
subSpaces: subSpaces.cast<SubSpaceModel>(), subSpaces: subSpaces.cast<SubSpaceModel>(),
+1
View File
@@ -61,6 +61,7 @@ dependencies:
firebase_crashlytics: ^4.3.2 firebase_crashlytics: ^4.3.2
firebase_database: ^11.3.2 firebase_database: ^11.3.2
bloc: ^9.0.0 bloc: ^9.0.0
geocoding: ^4.0.0
gauge_indicator: ^0.4.3 gauge_indicator: ^0.4.3