mirror of
https://github.com/SyncrowIOT/web.git
synced 2025-07-10 15:17:31 +00:00
Merge branch 'dev' of https://github.com/SyncrowIOT/web into SP-1594-device-location-api-integration
This commit is contained in:
@ -33,7 +33,7 @@ class AirQualityView extends StatelessWidget {
|
||||
return SingleChildScrollView(
|
||||
child: Container(
|
||||
padding: _padding,
|
||||
height: height,
|
||||
height: height * 1.1,
|
||||
child: const Column(
|
||||
children: [
|
||||
Expanded(
|
||||
@ -41,7 +41,7 @@ class AirQualityView extends StatelessWidget {
|
||||
spacing: 32,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 5,
|
||||
flex: 10,
|
||||
child: Column(
|
||||
spacing: 20,
|
||||
children: [
|
||||
@ -50,7 +50,7 @@ class AirQualityView extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(flex: 2, child: AirQualityEndSideWidget()),
|
||||
Expanded(flex: 6, child: AirQualityEndSideWidget()),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -0,0 +1,52 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_gauge.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_humidity_and_temperature.dart';
|
||||
|
||||
class AirQualityEndSideGaugeAndInfo extends StatelessWidget {
|
||||
const AirQualityEndSideGaugeAndInfo({
|
||||
super.key,
|
||||
required this.temperature,
|
||||
required this.humidity,
|
||||
required this.aqiLevel,
|
||||
});
|
||||
|
||||
final int temperature;
|
||||
final int humidity;
|
||||
final String aqiLevel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Expanded(
|
||||
flex: 2,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [Expanded(child: AqiGauge(aqi: aqi))],
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsDirectional.only(end: 20),
|
||||
child: AqiHumidityAndTemperature(
|
||||
temperature: temperature,
|
||||
humidity: humidity,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
double get aqi => switch (aqiLevel) {
|
||||
'level_1' => 25.0,
|
||||
'level_2' => 75.0,
|
||||
'level_3' => 125.0,
|
||||
_ => 0.0,
|
||||
};
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class AirQualityEndSideLiveIndicator extends StatelessWidget {
|
||||
const AirQualityEndSideLiveIndicator({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Entrance',
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.textPrimaryColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
CircleAvatar(
|
||||
backgroundColor: ColorsManager.green.withValues(
|
||||
alpha: 0.5,
|
||||
),
|
||||
radius: 2,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'Live',
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.green.withValues(alpha: 0.5),
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 8,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -1,10 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_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/helpers/fetch_energy_management_data_helper.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/analytics_device_dropdown.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_device_info.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_location_info.dart';
|
||||
import 'package:syncrow_web/pages/analytics/widgets/analytics_sidebar_header.dart';
|
||||
import 'package:syncrow_web/utils/style.dart';
|
||||
|
||||
class AirQualityEndSideWidget extends StatelessWidget {
|
||||
@ -17,70 +14,15 @@ class AirQualityEndSideWidget extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
padding: const EdgeInsetsDirectional.all(32),
|
||||
child: Column(
|
||||
child: const Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeader(context),
|
||||
Text(
|
||||
'Device ID:',
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.textPrimaryColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
SelectableText(
|
||||
context.watch<AnalyticsDevicesBloc>().state.selectedDevice?.uuid ??
|
||||
'N/A',
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
AnalyticsSidebarHeader(title: 'AQI Sensor'),
|
||||
Expanded(flex: 15, child: AqiDeviceInfo()),
|
||||
SizedBox(height: 20),
|
||||
Expanded(flex: 6, child: AqiLocationInfo()),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: FittedBox(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: SelectableText(
|
||||
'AQI Sensor',
|
||||
style: context.textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: ColorsManager.vividBlue.withValues(alpha: 0.6),
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: FittedBox(
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
child: AnalyticsDeviceDropdown(
|
||||
onChanged: (value) {
|
||||
context.read<AnalyticsDevicesBloc>().add(
|
||||
SelectAnalyticsDeviceEvent(value),
|
||||
);
|
||||
FetchEnergyManagementDataHelper.loadRealtimeDeviceChanges(
|
||||
context,
|
||||
deviceUuid: value.uuid,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,129 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/air_quality_end_side_gauge_and_info.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/air_quality_end_side_live_indicator.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_sub_value_widget.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
|
||||
import 'package:syncrow_web/utils/style.dart';
|
||||
|
||||
class AqiDeviceInfo extends StatelessWidget {
|
||||
const AqiDeviceInfo({super.key});
|
||||
|
||||
String _getValueForStatus(
|
||||
List<Status> deviceStatusList,
|
||||
String code, {
|
||||
double defaultValue = 0,
|
||||
String Function(int value)? formatter,
|
||||
}) {
|
||||
try {
|
||||
final foundStatus = deviceStatusList.firstWhere((e) => e.code == code);
|
||||
final value = foundStatus.value.toString();
|
||||
final intValue = int.parse(value);
|
||||
return formatter != null ? formatter(intValue) : intValue.toString();
|
||||
} catch (e) {
|
||||
return defaultValue.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<RealtimeDeviceChangesBloc, RealtimeDeviceChangesState>(
|
||||
builder: (context, state) {
|
||||
final status = state.deviceStatusList;
|
||||
final humidityValue = _getValueForStatus(
|
||||
status,
|
||||
'humidity_value',
|
||||
formatter: (value) => value.toStringAsFixed(0),
|
||||
);
|
||||
|
||||
final tempValue = _getValueForStatus(
|
||||
status,
|
||||
'temp_current',
|
||||
formatter: (value) => (value / 10).toStringAsFixed(0),
|
||||
);
|
||||
final pm25Value = _getValueForStatus(
|
||||
status,
|
||||
'pm25_value',
|
||||
formatter: (value) => value.toString().padLeft(3, '0'),
|
||||
);
|
||||
final pm10Value = _getValueForStatus(
|
||||
status,
|
||||
'pm10',
|
||||
formatter: (value) => value.toString().padLeft(3, '0'),
|
||||
);
|
||||
final co2Value = _getValueForStatus(
|
||||
status,
|
||||
'co2_value',
|
||||
formatter: (value) => value.toString().padLeft(4, '0'),
|
||||
);
|
||||
final ch2oValue = _getValueForStatus(
|
||||
status,
|
||||
'ch2o_value',
|
||||
formatter: (value) => (value / 100).toStringAsFixed(2),
|
||||
);
|
||||
final tvocValue = _getValueForStatus(
|
||||
status,
|
||||
'tvoc_value',
|
||||
formatter: (value) => (value / 100).toStringAsFixed(2),
|
||||
);
|
||||
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
115
lib/pages/analytics/modules/air_quality/widgets/aqi_gauge.dart
Normal file
115
lib/pages/analytics/modules/air_quality/widgets/aqi_gauge.dart
Normal file
@ -0,0 +1,115 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gauge_indicator/gauge_indicator.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class AqiGauge extends StatelessWidget {
|
||||
const AqiGauge({super.key, required this.aqi});
|
||||
|
||||
final double aqi;
|
||||
|
||||
static const _minRange = 0.0;
|
||||
static const _goodRange = 50.0;
|
||||
static const _moderateRange = 100.0;
|
||||
static const _maxRange = 150.0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final (status, statusColor) = _getStatusData(aqi);
|
||||
return AnimatedRadialGauge(
|
||||
value: aqi,
|
||||
debug: false,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
curve: Curves.easeInOut,
|
||||
initialValue: 0,
|
||||
alignment: Alignment.bottomCenter,
|
||||
builder: (context, child, value) {
|
||||
return Align(
|
||||
alignment: AlignmentDirectional.bottomCenter,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.bottomCenter,
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
text: 'Air Quality\n',
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.textPrimaryColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 12,
|
||||
),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: status,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: _darkenColor(statusColor),
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 30,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
axis: GaugeAxis(
|
||||
progressBar: const GaugeProgressBar.basic(color: Colors.transparent),
|
||||
style: const GaugeAxisStyle(
|
||||
cornerRadius: Radius.circular(16),
|
||||
thickness: 14,
|
||||
segmentSpacing: 4,
|
||||
),
|
||||
min: _minRange,
|
||||
max: _maxRange,
|
||||
pointer: GaugePointer.circle(
|
||||
position: const GaugePointerPosition.surface(),
|
||||
radius: MediaQuery.sizeOf(context).width * 0.004,
|
||||
color: ColorsManager.whiteColors,
|
||||
border: GaugePointerBorder(
|
||||
width: 6,
|
||||
color: statusColor,
|
||||
),
|
||||
shadow: const BoxShadow(
|
||||
color: ColorsManager.blackColor,
|
||||
blurRadius: 6,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
),
|
||||
segments: const [
|
||||
GaugeSegment(
|
||||
from: _minRange,
|
||||
to: _goodRange,
|
||||
cornerRadius: Radius.circular(16),
|
||||
color: ColorsManager.goodGreen,
|
||||
),
|
||||
GaugeSegment(
|
||||
from: _goodRange + 1,
|
||||
to: _moderateRange,
|
||||
cornerRadius: Radius.circular(16),
|
||||
color: ColorsManager.moderateYellow,
|
||||
),
|
||||
GaugeSegment(
|
||||
from: _moderateRange + 1,
|
||||
to: _maxRange,
|
||||
cornerRadius: Radius.circular(16),
|
||||
color: ColorsManager.poorOrange,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
(String status, Color color) _getStatusData(double value) {
|
||||
return switch (value) {
|
||||
<= _goodRange => ('Good', ColorsManager.goodGreen),
|
||||
<= _moderateRange => ('Moderate', ColorsManager.moderateYellow),
|
||||
_ => ('Poor', ColorsManager.poorOrange),
|
||||
};
|
||||
}
|
||||
|
||||
Color _darkenColor(Color color) {
|
||||
final black = Colors.black.withValues(alpha: 0.8);
|
||||
return Color.lerp(color, black, 0.4)!;
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.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';
|
||||
|
||||
class AqiHumidityAndTemperature extends StatelessWidget {
|
||||
const AqiHumidityAndTemperature({
|
||||
required this.temperature,
|
||||
required this.humidity,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final int temperature;
|
||||
final int humidity;
|
||||
|
||||
static const iconSize = 12.0;
|
||||
static const colorFilter = ColorFilter.mode(
|
||||
ColorsManager.textPrimaryColor,
|
||||
BlendMode.srcIn,
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: DefaultTextStyle(
|
||||
style: context.textTheme.bodySmall!.copyWith(
|
||||
color: ColorsManager.textPrimaryColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 16,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildIconAndValue(Assets.temperatureAqiSidebar, '$temperature°C'),
|
||||
const SizedBox(height: 10),
|
||||
_buildIconAndValue(Assets.humidityAqiSidebar, '$humidity%'),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildIconAndValue(String icon, String value) {
|
||||
return Row(
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
icon,
|
||||
height: iconSize,
|
||||
width: iconSize,
|
||||
colorFilter: colorFilter,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(value),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.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/style.dart';
|
||||
|
||||
class AqiLocation extends StatelessWidget {
|
||||
const AqiLocation({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: subSectionContainerDecoration.copyWith(
|
||||
boxShadow: const [],
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
padding: const EdgeInsetsDirectional.all(10),
|
||||
child: Row(
|
||||
spacing: 10,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildLocationPin(),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Business Bay, Dubai - UAE',
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.textPrimaryColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLocationPin() {
|
||||
return SvgPicture.asset(
|
||||
Assets.locationPin,
|
||||
height: 12,
|
||||
width: 12,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
colorFilter: const ColorFilter.mode(
|
||||
ColorsManager.vividBlue,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
import 'package:flutter/material.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';
|
||||
import 'package:syncrow_web/utils/style.dart';
|
||||
|
||||
class AqiLocationInfo extends StatelessWidget {
|
||||
const AqiLocationInfo({super.key});
|
||||
|
||||
@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,
|
||||
),
|
||||
AqiLocationInfoCell(
|
||||
label: 'Humidity',
|
||||
value: '25%',
|
||||
svgPath: Assets.aqiHumidity,
|
||||
),
|
||||
AqiLocationInfoCell(
|
||||
label: 'Air Quality',
|
||||
value: ' 120',
|
||||
svgPath: Assets.aqiAirQuality,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class AqiLocationInfoCell extends StatelessWidget {
|
||||
const AqiLocationInfoCell({
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.svgPath,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final String value;
|
||||
final String svgPath;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.whiteColors,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
Align(
|
||||
alignment: AlignmentDirectional.topStart,
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsDirectional.all(10),
|
||||
child: SizedBox(
|
||||
height: 24,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.topStart,
|
||||
child: Text(
|
||||
label,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.textPrimaryColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: AlignmentDirectional.bottomEnd,
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsDirectional.all(10),
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
width: 120,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.bottomEnd,
|
||||
child: Text(
|
||||
value,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.vividBlue.withValues(alpha: 0.7),
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 24,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: AlignmentDirectional.bottomStart,
|
||||
child: SizedBox.square(
|
||||
dimension: MediaQuery.sizeOf(context).width * 0.45,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.bottomStart,
|
||||
child: SvgPicture.asset(svgPath),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,158 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
final class _AqiRange {
|
||||
const _AqiRange({required this.max, required this.color});
|
||||
|
||||
final double max;
|
||||
final Color color;
|
||||
}
|
||||
|
||||
class AqiSubValueWidget extends StatelessWidget {
|
||||
const AqiSubValueWidget({
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.unit,
|
||||
required this.range,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final String value;
|
||||
final String unit;
|
||||
final (double min, double max) range;
|
||||
|
||||
double get _parsedValue => double.parse(value);
|
||||
|
||||
static const List<_AqiRange> _ranges = [
|
||||
_AqiRange(max: 12, color: ColorsManager.goodGreen),
|
||||
_AqiRange(max: 35, color: ColorsManager.poorOrange),
|
||||
_AqiRange(max: 55, color: ColorsManager.poorOrange),
|
||||
_AqiRange(max: 150, color: ColorsManager.unhealthyRed),
|
||||
_AqiRange(max: 250, color: ColorsManager.severePink),
|
||||
_AqiRange(max: 500, color: ColorsManager.hazardousPurple),
|
||||
];
|
||||
|
||||
static List<_AqiRange> _getRangesForValue((double min, double max) range) {
|
||||
final (double min, double max) = range;
|
||||
final rangeSize = (max - min) / 6;
|
||||
return [
|
||||
_AqiRange(max: min + rangeSize, color: ColorsManager.goodGreen),
|
||||
_AqiRange(max: min + (rangeSize * 2), color: ColorsManager.poorOrange),
|
||||
_AqiRange(max: min + (rangeSize * 3), color: ColorsManager.poorOrange),
|
||||
_AqiRange(max: min + (rangeSize * 4), color: ColorsManager.unhealthyRed),
|
||||
_AqiRange(max: min + (rangeSize * 5), color: ColorsManager.severePink),
|
||||
_AqiRange(max: min, color: ColorsManager.hazardousPurple),
|
||||
];
|
||||
}
|
||||
|
||||
int _getActiveSegmentByRange(double value, (double min, double max) range) {
|
||||
final ranges = _getRangesForValue(range);
|
||||
for (int i = 0; i < ranges.length; i++) {
|
||||
if (value <= ranges[i].max) return i;
|
||||
}
|
||||
return ranges.length - 1;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final activeSegment = _getActiveSegmentByRange(_parsedValue, range);
|
||||
return Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsetsDirectional.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.whiteColors,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
spacing: MediaQuery.sizeOf(context).width * 0.0075,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_buildLabel(context),
|
||||
_buildSegmentedBar(activeSegment),
|
||||
_buildValueAndUnit(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildValueAndUnit(BuildContext context) {
|
||||
return Expanded(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
value,
|
||||
style: context.textTheme.titleMedium?.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
unit,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSegmentedBar(int activeSegment) {
|
||||
return Expanded(
|
||||
flex: 4,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Row(
|
||||
spacing: 4,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(_ranges.length, (index) {
|
||||
final isActive = index == activeSegment;
|
||||
final color = _ranges[index].color.withValues(
|
||||
alpha: isActive ? 1.0 : 0.25,
|
||||
);
|
||||
return Expanded(
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
curve: Curves.linear,
|
||||
height: 5,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLabel(BuildContext context) {
|
||||
return Expanded(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Text(
|
||||
label,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -3,16 +3,17 @@ import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
enum AqiType {
|
||||
aqi('AQI'),
|
||||
pm25('PM2.5'),
|
||||
pm10('PM10'),
|
||||
hcho('HCHO'),
|
||||
tvoc('TVOC'),
|
||||
co2('CO2'),
|
||||
c6h6('C6H6');
|
||||
aqi('AQI', ''),
|
||||
pm25('PM2.5', 'µg/m³'),
|
||||
pm10('PM10', 'µg/m³'),
|
||||
hcho('HCHO', 'mg/m³'),
|
||||
tvoc('TVOC', 'µg/m³'),
|
||||
co2('CO2', 'ppm');
|
||||
|
||||
const AqiType(this.value, this.unit);
|
||||
|
||||
final String value;
|
||||
const AqiType(this.value);
|
||||
final String unit;
|
||||
}
|
||||
|
||||
class AqiTypeDropdown extends StatefulWidget {
|
||||
|
@ -6,9 +6,14 @@ import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class AnalyticsDeviceDropdown extends StatelessWidget {
|
||||
const AnalyticsDeviceDropdown({required this.onChanged, super.key});
|
||||
const AnalyticsDeviceDropdown({
|
||||
required this.onChanged,
|
||||
this.showSpaceUuid = false,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final ValueChanged<AnalyticsDevice> onChanged;
|
||||
final bool showSpaceUuid;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -72,17 +77,18 @@ class AnalyticsDeviceDropdown extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(e.name),
|
||||
if (spaceUuid != null)
|
||||
FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Text(
|
||||
spaceUuid,
|
||||
style: _getTextStyle(context)?.copyWith(
|
||||
fontSize: 10,
|
||||
if (showSpaceUuid)
|
||||
if (spaceUuid != null)
|
||||
FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Text(
|
||||
spaceUuid,
|
||||
style: _getTextStyle(context)?.copyWith(
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -2,19 +2,16 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/power_clamp_energy_status.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/power_clamp_info/power_clamp_info_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/helpers/fetch_energy_management_data_helper.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/analytics_device_dropdown.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/energy_consumption_by_phases_chart_box.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/power_clamp_energy_status_widget.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/power_clamp_phases_data_widget.dart';
|
||||
import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart';
|
||||
import 'package:syncrow_web/pages/analytics/widgets/analytics_sidebar_header.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/power_clamp/models/power_clamp_model.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/style.dart';
|
||||
|
||||
class PowerClampEnergyDataWidget extends StatelessWidget {
|
||||
@ -42,26 +39,18 @@ class PowerClampEnergyDataWidget extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AnalyticsErrorWidget(state.errorMessage),
|
||||
_buildHeader(context),
|
||||
Text(
|
||||
'Device ID:',
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.textPrimaryColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 12,
|
||||
),
|
||||
AnalyticsSidebarHeader(
|
||||
title: 'Smart Power Clamp',
|
||||
showSpaceUuid: true,
|
||||
onChanged: (device) {
|
||||
FetchEnergyManagementDataHelper.loadEnergyConsumptionByPhases(
|
||||
context,
|
||||
powerClampUuid: device.uuid,
|
||||
selectedDate:
|
||||
context.read<AnalyticsDatePickerBloc>().state.monthlyDate,
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
SelectableText(
|
||||
context.watch<AnalyticsDevicesBloc>().state.selectedDevice?.uuid ??
|
||||
'N/A',
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: PowerClampEnergyStatusWidget(
|
||||
@ -111,51 +100,6 @@ class PowerClampEnergyDataWidget extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: SelectableText(
|
||||
'Smart Power Clamp',
|
||||
style: context.textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: ColorsManager.vividBlue.withValues(alpha: 0.6),
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
child: AnalyticsDeviceDropdown(
|
||||
onChanged: (value) {
|
||||
FetchEnergyManagementDataHelper.loadEnergyConsumptionByPhases(
|
||||
context,
|
||||
powerClampUuid: value.uuid,
|
||||
selectedDate:
|
||||
context.read<AnalyticsDatePickerBloc>().state.monthlyDate,
|
||||
);
|
||||
FetchEnergyManagementDataHelper.loadRealtimeDeviceChanges(
|
||||
context,
|
||||
deviceUuid: value.uuid,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String _valueFromCode(String code, List<DataPoint> points) {
|
||||
return points
|
||||
.firstWhere((e) => e.code == code, orElse: () => DataPoint(value: '--'))
|
||||
|
@ -1,15 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/power_clamp_energy_status.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/helpers/fetch_energy_management_data_helper.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/analytics_device_dropdown.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/power_clamp_energy_status_widget.dart';
|
||||
import 'package:syncrow_web/pages/analytics/widgets/analytics_sidebar_header.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.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/style.dart';
|
||||
|
||||
class OccupancyEndSideBar extends StatelessWidget {
|
||||
@ -27,28 +23,7 @@ class OccupancyEndSideBar extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeader(context),
|
||||
Text(
|
||||
'Device ID:',
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.textPrimaryColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
SelectableText(
|
||||
context.watch<AnalyticsDevicesBloc>().state.selectedDevice?.uuid ??
|
||||
'N/A',
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const Divider(height: 1, color: ColorsManager.greyColor),
|
||||
const SizedBox(height: 50),
|
||||
const AnalyticsSidebarHeader(title: 'Presnce Sensor'),
|
||||
SizedBox(
|
||||
height: MediaQuery.sizeOf(context).height * 0.2,
|
||||
child: PowerClampEnergyStatusWidget(
|
||||
@ -101,42 +76,4 @@ class OccupancyEndSideBar extends StatelessWidget {
|
||||
.toString();
|
||||
return value == 'null' ? defaultValue ?? '--' : value;
|
||||
}
|
||||
|
||||
Widget _buildHeader(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: SelectableText(
|
||||
'Presnce Sensor',
|
||||
style: context.textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: ColorsManager.vividBlue.withValues(alpha: 0.6),
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
child: AnalyticsDeviceDropdown(
|
||||
onChanged: (value) =>
|
||||
FetchEnergyManagementDataHelper.loadRealtimeDeviceChanges(
|
||||
context,
|
||||
deviceUuid: value.uuid,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user