mirror of
https://github.com/SyncrowIOT/web.git
synced 2026-03-10 20:41:46 +00:00
Merge branch 'dev' into SP-1612-FE-User-cannot-see-the-horizontal-scroll-on-any-of-the-tables-they-have-to-hover-over-it-but-it-s-not-obvious-that-they-can-do-that
This commit is contained in:
@ -17,7 +17,8 @@ class TagDialogTextfieldDropdown extends StatefulWidget {
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_DialogTextfieldDropdownState createState() => _DialogTextfieldDropdownState();
|
||||
_DialogTextfieldDropdownState createState() =>
|
||||
_DialogTextfieldDropdownState();
|
||||
}
|
||||
|
||||
class _DialogTextfieldDropdownState extends State<TagDialogTextfieldDropdown> {
|
||||
@ -36,6 +37,12 @@ class _DialogTextfieldDropdownState extends State<TagDialogTextfieldDropdown> {
|
||||
|
||||
_focusNode.addListener(() {
|
||||
if (!_focusNode.hasFocus) {
|
||||
// Call onSelected when focus is lost
|
||||
final selectedTag = _filteredItems.firstWhere(
|
||||
(tag) => tag.tag == _controller.text,
|
||||
orElse: () => Tag(tag: _controller.text),
|
||||
);
|
||||
widget.onSelected(selectedTag);
|
||||
_closeDropdown();
|
||||
}
|
||||
});
|
||||
@ -43,7 +50,9 @@ class _DialogTextfieldDropdownState extends State<TagDialogTextfieldDropdown> {
|
||||
|
||||
void _filterItems() {
|
||||
setState(() {
|
||||
_filteredItems = widget.items.where((tag) => tag.product?.uuid == widget.product).toList();
|
||||
_filteredItems = widget.items;
|
||||
// .where((tag) => tag.product?.uuid == widget.product)
|
||||
// .toList();
|
||||
});
|
||||
}
|
||||
|
||||
@ -112,7 +121,9 @@ class _DialogTextfieldDropdownState extends State<TagDialogTextfieldDropdown> {
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(color: ColorsManager.textPrimaryColor)),
|
||||
?.copyWith(
|
||||
color: ColorsManager
|
||||
.textPrimaryColor)),
|
||||
onTap: () {
|
||||
_controller.text = tag.tag ?? '';
|
||||
widget.onSelected(tag);
|
||||
@ -156,13 +167,15 @@ class _DialogTextfieldDropdownState extends State<TagDialogTextfieldDropdown> {
|
||||
controller: _controller,
|
||||
focusNode: _focusNode,
|
||||
onFieldSubmitted: (value) {
|
||||
final selectedTag = _filteredItems.firstWhere((tag) => tag.tag == value,
|
||||
final selectedTag = _filteredItems.firstWhere(
|
||||
(tag) => tag.tag == value,
|
||||
orElse: () => Tag(tag: value));
|
||||
widget.onSelected(selectedTag);
|
||||
_closeDropdown();
|
||||
},
|
||||
onTapOutside: (event) {
|
||||
widget.onSelected(_filteredItems.firstWhere((tag) => tag.tag == _controller.text,
|
||||
widget.onSelected(_filteredItems.firstWhere(
|
||||
(tag) => tag.tag == _controller.text,
|
||||
orElse: () => Tag(tag: _controller.text)));
|
||||
_closeDropdown();
|
||||
},
|
||||
|
||||
@ -11,7 +11,6 @@ import 'package:syncrow_web/pages/home/bloc/home_event.dart';
|
||||
import 'package:syncrow_web/pages/routines/bloc/create_routine_bloc/create_routine_bloc.dart';
|
||||
import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart';
|
||||
import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_bloc.dart';
|
||||
import 'package:syncrow_web/services/locator.dart';
|
||||
import 'package:syncrow_web/utils/app_routes.dart';
|
||||
@ -21,8 +20,10 @@ import 'package:syncrow_web/utils/theme/theme.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
try {
|
||||
const environment =
|
||||
String.fromEnvironment('FLAVOR', defaultValue: 'production');
|
||||
const environment = String.fromEnvironment(
|
||||
'FLAVOR',
|
||||
defaultValue: 'production',
|
||||
);
|
||||
await dotenv.load(fileName: '.env.$environment');
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await Firebase.initializeApp(
|
||||
@ -40,7 +41,7 @@ class MyApp extends StatelessWidget {
|
||||
initialLocation: RoutesConst.auth,
|
||||
routes: AppRoutes.getRoutes(),
|
||||
redirect: (context, state) async {
|
||||
String checkToken = await AuthBloc.getTokenAndValidate();
|
||||
final checkToken = await AuthBloc.getTokenAndValidate();
|
||||
final loggedIn = checkToken == 'Success';
|
||||
final goingToLogin = state.uri.toString() == RoutesConst.auth;
|
||||
|
||||
@ -58,8 +59,7 @@ class MyApp extends StatelessWidget {
|
||||
BlocProvider<CreateRoutineBloc>(
|
||||
create: (context) => CreateRoutineBloc(),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => HomeBloc()..add(const FetchUserInfo())),
|
||||
BlocProvider(create: (context) => HomeBloc()..add(FetchUserInfo())),
|
||||
BlocProvider<VisitorPasswordBloc>(
|
||||
create: (context) => VisitorPasswordBloc(),
|
||||
),
|
||||
@ -67,7 +67,7 @@ class MyApp extends StatelessWidget {
|
||||
create: (context) => RoutineBloc(),
|
||||
),
|
||||
BlocProvider<SpaceTreeBloc>(
|
||||
create: (context) => SpaceTreeBloc()..add(InitialEvent()),
|
||||
create: (context) => SpaceTreeBloc(),
|
||||
),
|
||||
],
|
||||
child: MaterialApp.router(
|
||||
|
||||
@ -11,7 +11,6 @@ import 'package:syncrow_web/pages/home/bloc/home_event.dart';
|
||||
import 'package:syncrow_web/pages/routines/bloc/create_routine_bloc/create_routine_bloc.dart';
|
||||
import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart';
|
||||
import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_bloc.dart';
|
||||
import 'package:syncrow_web/services/locator.dart';
|
||||
import 'package:syncrow_web/utils/app_routes.dart';
|
||||
@ -21,7 +20,10 @@ import 'package:syncrow_web/utils/theme/theme.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
try {
|
||||
const environment = String.fromEnvironment('FLAVOR', defaultValue: 'development');
|
||||
const environment = String.fromEnvironment(
|
||||
'FLAVOR',
|
||||
defaultValue: 'development',
|
||||
);
|
||||
await dotenv.load(fileName: '.env.$environment');
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await Firebase.initializeApp(
|
||||
@ -39,7 +41,7 @@ class MyApp extends StatelessWidget {
|
||||
initialLocation: RoutesConst.auth,
|
||||
routes: AppRoutes.getRoutes(),
|
||||
redirect: (context, state) async {
|
||||
String checkToken = await AuthBloc.getTokenAndValidate();
|
||||
final checkToken = await AuthBloc.getTokenAndValidate();
|
||||
final loggedIn = checkToken == 'Success';
|
||||
final goingToLogin = state.uri.toString() == RoutesConst.auth;
|
||||
|
||||
@ -57,7 +59,7 @@ class MyApp extends StatelessWidget {
|
||||
BlocProvider<CreateRoutineBloc>(
|
||||
create: (context) => CreateRoutineBloc(),
|
||||
),
|
||||
BlocProvider(create: (context) => HomeBloc()..add(const FetchUserInfo())),
|
||||
BlocProvider(create: (context) => HomeBloc()..add(FetchUserInfo())),
|
||||
BlocProvider<VisitorPasswordBloc>(
|
||||
create: (context) => VisitorPasswordBloc(),
|
||||
),
|
||||
@ -65,7 +67,7 @@ class MyApp extends StatelessWidget {
|
||||
create: (context) => RoutineBloc(),
|
||||
),
|
||||
BlocProvider<SpaceTreeBloc>(
|
||||
create: (context) => SpaceTreeBloc()..add(InitialEvent()),
|
||||
create: (context) => SpaceTreeBloc(),
|
||||
),
|
||||
],
|
||||
child: MaterialApp.router(
|
||||
|
||||
@ -11,7 +11,6 @@ import 'package:syncrow_web/pages/home/bloc/home_event.dart';
|
||||
import 'package:syncrow_web/pages/routines/bloc/create_routine_bloc/create_routine_bloc.dart';
|
||||
import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart';
|
||||
import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_bloc.dart';
|
||||
import 'package:syncrow_web/services/locator.dart';
|
||||
import 'package:syncrow_web/utils/app_routes.dart';
|
||||
@ -39,7 +38,7 @@ class MyApp extends StatelessWidget {
|
||||
initialLocation: RoutesConst.auth,
|
||||
routes: AppRoutes.getRoutes(),
|
||||
redirect: (context, state) async {
|
||||
String checkToken = await AuthBloc.getTokenAndValidate();
|
||||
final checkToken = await AuthBloc.getTokenAndValidate();
|
||||
final loggedIn = checkToken == 'Success';
|
||||
final goingToLogin = state.uri.toString() == RoutesConst.auth;
|
||||
|
||||
@ -57,7 +56,7 @@ class MyApp extends StatelessWidget {
|
||||
BlocProvider<CreateRoutineBloc>(
|
||||
create: (context) => CreateRoutineBloc(),
|
||||
),
|
||||
BlocProvider(create: (context) => HomeBloc()..add(const FetchUserInfo())),
|
||||
BlocProvider(create: (context) => HomeBloc()..add(FetchUserInfo())),
|
||||
BlocProvider<VisitorPasswordBloc>(
|
||||
create: (context) => VisitorPasswordBloc(),
|
||||
),
|
||||
@ -65,7 +64,7 @@ class MyApp extends StatelessWidget {
|
||||
create: (context) => RoutineBloc(),
|
||||
),
|
||||
BlocProvider<SpaceTreeBloc>(
|
||||
create: (context) => SpaceTreeBloc()..add(InitialEvent()),
|
||||
create: (context) => SpaceTreeBloc(),
|
||||
),
|
||||
],
|
||||
child: MaterialApp.router(
|
||||
|
||||
@ -267,7 +267,8 @@ class AccessBloc extends Bloc<AccessEvent, AccessState> {
|
||||
selectedIndex = 0;
|
||||
effectiveTimeTimeStamp = null;
|
||||
expirationTimeTimeStamp = null;
|
||||
add(FetchTableData());
|
||||
filteredData = List.from(data);
|
||||
emit(TableLoaded(filteredData));
|
||||
}
|
||||
|
||||
String timestampToDate(dynamic timestamp) {
|
||||
|
||||
56
lib/pages/analytics/helpers/dashed_border_painter.dart
Normal file
56
lib/pages/analytics/helpers/dashed_border_painter.dart
Normal file
@ -0,0 +1,56 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class DashedBorderPainter extends CustomPainter {
|
||||
final double dashWidth;
|
||||
final double dashSpace;
|
||||
final Color color;
|
||||
|
||||
DashedBorderPainter({
|
||||
this.dashWidth = 4.0,
|
||||
this.dashSpace = 2.0,
|
||||
this.color = Colors.black,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paint = Paint()
|
||||
..color = color
|
||||
..strokeWidth = 0.5
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
final Path topPath = Path()
|
||||
..moveTo(0, 0)
|
||||
..lineTo(size.width, 0);
|
||||
|
||||
final Path bottomPath = Path()
|
||||
..moveTo(0, size.height)
|
||||
..lineTo(size.width, size.height);
|
||||
|
||||
final dashedTopPath = _createDashedPath(topPath, dashWidth, dashSpace);
|
||||
final dashedBottomPath = _createDashedPath(bottomPath, dashWidth, dashSpace);
|
||||
|
||||
canvas.drawPath(dashedTopPath, paint);
|
||||
canvas.drawPath(dashedBottomPath, paint);
|
||||
}
|
||||
|
||||
Path _createDashedPath(Path source, double dashWidth, double dashSpace) {
|
||||
final Path dashedPath = Path();
|
||||
for (PathMetric pathMetric in source.computeMetrics()) {
|
||||
double distance = 0.0;
|
||||
while (distance < pathMetric.length) {
|
||||
final double nextDistance = distance + dashWidth;
|
||||
dashedPath.addPath(
|
||||
pathMetric.extractPath(distance, nextDistance),
|
||||
Offset.zero,
|
||||
);
|
||||
distance = nextDistance + dashSpace;
|
||||
}
|
||||
}
|
||||
return dashedPath;
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||
}
|
||||
56
lib/pages/analytics/models/air_quality_data_model.dart
Normal file
56
lib/pages/analytics/models/air_quality_data_model.dart
Normal file
@ -0,0 +1,56 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class AirQualityDataModel extends Equatable {
|
||||
const AirQualityDataModel({
|
||||
required this.date,
|
||||
required this.data,
|
||||
});
|
||||
|
||||
final DateTime date;
|
||||
final List<AirQualityPercentageData> data;
|
||||
|
||||
factory AirQualityDataModel.fromJson(Map<String, dynamic> json) {
|
||||
return AirQualityDataModel(
|
||||
date: DateTime.parse(json['date'] as String),
|
||||
data: (json['data'] as List<dynamic>)
|
||||
.map(
|
||||
(e) => AirQualityPercentageData.fromJson(e as Map<String, dynamic>),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
static final Map<String, Color> metricColors = {
|
||||
'good': ColorsManager.goodGreen.withValues(alpha: 0.7),
|
||||
'moderate': ColorsManager.moderateYellow.withValues(alpha: 0.7),
|
||||
'unhealthy_sensitive': ColorsManager.poorOrange.withValues(alpha: 0.7),
|
||||
'unhealthy': ColorsManager.unhealthyRed.withValues(alpha: 0.7),
|
||||
'very_unhealthy': ColorsManager.severePink.withValues(alpha: 0.7),
|
||||
'hazardous': ColorsManager.hazardousPurple.withValues(alpha: 0.7),
|
||||
};
|
||||
|
||||
@override
|
||||
List<Object?> get props => [date, data];
|
||||
}
|
||||
|
||||
class AirQualityPercentageData extends Equatable {
|
||||
const AirQualityPercentageData({
|
||||
required this.type,
|
||||
required this.percentage,
|
||||
});
|
||||
|
||||
final String type;
|
||||
final double percentage;
|
||||
|
||||
factory AirQualityPercentageData.fromJson(Map<String, dynamic> json) {
|
||||
return AirQualityPercentageData(
|
||||
type: json['type'] as String? ?? '',
|
||||
percentage: (json['percentage'] as num?)?.toDouble() ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [type, percentage];
|
||||
}
|
||||
82
lib/pages/analytics/models/analytics_device.dart
Normal file
82
lib/pages/analytics/models/analytics_device.dart
Normal file
@ -0,0 +1,82 @@
|
||||
class AnalyticsDevice {
|
||||
const AnalyticsDevice({
|
||||
required this.uuid,
|
||||
required this.name,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.deviceTuyaUuid,
|
||||
this.isActive,
|
||||
this.productDevice,
|
||||
this.spaceUuid,
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
});
|
||||
|
||||
final String uuid;
|
||||
final String name;
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
final String? deviceTuyaUuid;
|
||||
final bool? isActive;
|
||||
final ProductDevice? productDevice;
|
||||
final String? spaceUuid;
|
||||
final double? latitude;
|
||||
final double? longitude;
|
||||
|
||||
factory AnalyticsDevice.fromJson(Map<String, dynamic> json) {
|
||||
return AnalyticsDevice(
|
||||
uuid: json['uuid'] as String,
|
||||
name: json['name'] as String,
|
||||
createdAt: json['createdAt'] != null
|
||||
? DateTime.parse(json['createdAt'] as String)
|
||||
: null,
|
||||
updatedAt: json['updatedAt'] != null
|
||||
? DateTime.parse(json['updatedAt'] as String)
|
||||
: null,
|
||||
deviceTuyaUuid: json['deviceTuyaUuid'] as String?,
|
||||
isActive: json['isActive'] as bool?,
|
||||
productDevice: json['productDevice'] != null
|
||||
? ProductDevice.fromJson(json['productDevice'] as Map<String, dynamic>)
|
||||
: null,
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ProductDevice {
|
||||
const ProductDevice({
|
||||
this.uuid,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.catName,
|
||||
this.prodId,
|
||||
this.name,
|
||||
this.prodType,
|
||||
});
|
||||
|
||||
final String? uuid;
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
final String? catName;
|
||||
final String? prodId;
|
||||
final String? name;
|
||||
final String? prodType;
|
||||
|
||||
factory ProductDevice.fromJson(Map<String, dynamic> json) {
|
||||
return ProductDevice(
|
||||
uuid: json['uuid'] as String?,
|
||||
createdAt: json['createdAt'] != null
|
||||
? DateTime.parse(json['createdAt'] as String)
|
||||
: null,
|
||||
updatedAt: json['updatedAt'] != null
|
||||
? DateTime.parse(json['updatedAt'] as String)
|
||||
: null,
|
||||
catName: json['catName'] as String?,
|
||||
prodId: json['prodId'] as String?,
|
||||
name: json['name'] as String?,
|
||||
prodType: json['prodType'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
55
lib/pages/analytics/models/device_location_info.dart
Normal file
55
lib/pages/analytics/models/device_location_info.dart
Normal file
@ -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,
|
||||
];
|
||||
}
|
||||
32
lib/pages/analytics/models/occupacy.dart
Normal file
32
lib/pages/analytics/models/occupacy.dart
Normal file
@ -0,0 +1,32 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class Occupacy extends Equatable {
|
||||
final DateTime date;
|
||||
final String occupancy;
|
||||
final String spaceUuid;
|
||||
final int occupiedSeconds;
|
||||
|
||||
const Occupacy({
|
||||
required this.date,
|
||||
required this.occupancy,
|
||||
required this.spaceUuid,
|
||||
required this.occupiedSeconds,
|
||||
});
|
||||
|
||||
factory Occupacy.fromJson(Map<String, dynamic> json) {
|
||||
return Occupacy(
|
||||
date: DateTime.parse(json['event_date'] as String? ?? '${DateTime.now()}'),
|
||||
occupancy: (json['occupancy_percentage'] ?? 0).toString(),
|
||||
spaceUuid: json['space_uuid'] as String? ?? '',
|
||||
occupiedSeconds: json['occupied_seconds'] as int? ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
date,
|
||||
occupancy,
|
||||
spaceUuid,
|
||||
occupiedSeconds,
|
||||
];
|
||||
}
|
||||
28
lib/pages/analytics/models/occupancy_heat_map_model.dart
Normal file
28
lib/pages/analytics/models/occupancy_heat_map_model.dart
Normal file
@ -0,0 +1,28 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class OccupancyHeatMapModel extends Equatable {
|
||||
final String uuid;
|
||||
|
||||
final DateTime eventDate;
|
||||
|
||||
final int countTotalPresenceDetected;
|
||||
|
||||
const OccupancyHeatMapModel({
|
||||
required this.uuid,
|
||||
required this.eventDate,
|
||||
required this.countTotalPresenceDetected,
|
||||
});
|
||||
|
||||
factory OccupancyHeatMapModel.fromJson(Map<String, dynamic> json) {
|
||||
return OccupancyHeatMapModel(
|
||||
uuid: json['uuid'] as String? ?? '',
|
||||
eventDate: DateTime.parse(
|
||||
json['event_date'] as String? ?? '${DateTime.now()}',
|
||||
),
|
||||
countTotalPresenceDetected: json['count_total_presence_detected'] as int? ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [uuid, eventDate, countTotalPresenceDetected];
|
||||
}
|
||||
@ -1,27 +1,66 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class PhasesEnergyConsumption extends Equatable {
|
||||
final int month;
|
||||
final double phaseA;
|
||||
final double phaseB;
|
||||
final double phaseC;
|
||||
final String uuid;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
final String deviceUuid;
|
||||
final DateTime date;
|
||||
final double energyConsumedKw;
|
||||
final double energyConsumedA;
|
||||
final double energyConsumedB;
|
||||
final double energyConsumedC;
|
||||
|
||||
const PhasesEnergyConsumption({
|
||||
required this.month,
|
||||
required this.phaseA,
|
||||
required this.phaseB,
|
||||
required this.phaseC,
|
||||
required this.uuid,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.deviceUuid,
|
||||
required this.date,
|
||||
required this.energyConsumedKw,
|
||||
required this.energyConsumedA,
|
||||
required this.energyConsumedB,
|
||||
required this.energyConsumedC,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [month, phaseA, phaseB, phaseC];
|
||||
List<Object?> get props => [
|
||||
uuid,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
deviceUuid,
|
||||
date,
|
||||
energyConsumedKw,
|
||||
energyConsumedA,
|
||||
energyConsumedB,
|
||||
energyConsumedC,
|
||||
];
|
||||
|
||||
factory PhasesEnergyConsumption.fromJson(Map<String, dynamic> json) {
|
||||
return PhasesEnergyConsumption(
|
||||
month: json['month'] as int,
|
||||
phaseA: (json['phaseA'] as num).toDouble(),
|
||||
phaseB: (json['phaseB'] as num).toDouble(),
|
||||
phaseC: (json['phaseC'] as num).toDouble(),
|
||||
uuid: json['uuid'] as String,
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
updatedAt: DateTime.parse(json['updatedAt'] as String),
|
||||
deviceUuid: json['deviceUuid'] as String,
|
||||
date: DateTime.parse(json['date'] as String),
|
||||
energyConsumedKw: double.parse(json['energyConsumedKw']),
|
||||
energyConsumedA: double.parse(json['energyConsumedA']),
|
||||
energyConsumedB: double.parse(json['energyConsumedB']),
|
||||
energyConsumedC: double.parse(json['energyConsumedC']),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'uuid': uuid,
|
||||
'createdAt': createdAt.toIso8601String(),
|
||||
'updatedAt': updatedAt.toIso8601String(),
|
||||
'deviceUuid': deviceUuid,
|
||||
'date': date.toIso8601String().split('T')[0],
|
||||
'energyConsumedKw': energyConsumedKw.toString(),
|
||||
'energyConsumedA': energyConsumedA.toString(),
|
||||
'energyConsumedB': energyConsumedB.toString(),
|
||||
'energyConsumedC': energyConsumedC.toString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
49
lib/pages/analytics/models/range_of_aqi.dart
Normal file
49
lib/pages/analytics/models/range_of_aqi.dart
Normal file
@ -0,0 +1,49 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class RangeOfAqi extends Equatable {
|
||||
final DateTime date;
|
||||
final List<RangeOfAqiValue> data;
|
||||
|
||||
const RangeOfAqi({
|
||||
required this.data,
|
||||
required this.date,
|
||||
});
|
||||
|
||||
factory RangeOfAqi.fromJson(Map<String, dynamic> json) {
|
||||
return RangeOfAqi(
|
||||
date: DateTime.parse(json['date'] as String),
|
||||
data: (json['data'] as List<dynamic>)
|
||||
.map((e) => RangeOfAqiValue.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [data, date];
|
||||
}
|
||||
|
||||
class RangeOfAqiValue extends Equatable {
|
||||
final String type;
|
||||
final double min;
|
||||
final double average;
|
||||
final double max;
|
||||
|
||||
const RangeOfAqiValue({
|
||||
required this.type,
|
||||
required this.min,
|
||||
required this.average,
|
||||
required this.max,
|
||||
});
|
||||
|
||||
factory RangeOfAqiValue.fromJson(Map<String, dynamic> json) {
|
||||
return RangeOfAqiValue(
|
||||
type: json['type'] as String,
|
||||
min: (json['min'] as num).toDouble(),
|
||||
average: (json['average'] as num).toDouble(),
|
||||
max: (json['max'] as num).toDouble(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [type, min, average, max];
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/air_quality_distribution_service.dart';
|
||||
|
||||
part 'air_quality_distribution_event.dart';
|
||||
part 'air_quality_distribution_state.dart';
|
||||
|
||||
class AirQualityDistributionBloc
|
||||
extends Bloc<AirQualityDistributionEvent, AirQualityDistributionState> {
|
||||
final AirQualityDistributionService _aqiDistributionService;
|
||||
|
||||
AirQualityDistributionBloc(
|
||||
this._aqiDistributionService,
|
||||
) : super(const AirQualityDistributionState()) {
|
||||
on<LoadAirQualityDistribution>(_onLoadAirQualityDistribution);
|
||||
on<ClearAirQualityDistribution>(_onClearAirQualityDistribution);
|
||||
on<UpdateAqiTypeEvent>(_onUpdateAqiTypeEvent);
|
||||
}
|
||||
|
||||
Future<void> _onLoadAirQualityDistribution(
|
||||
LoadAirQualityDistribution event,
|
||||
Emitter<AirQualityDistributionState> emit,
|
||||
) async {
|
||||
try {
|
||||
emit(state.copyWith(status: AirQualityDistributionStatus.loading));
|
||||
final result = await _aqiDistributionService.getAirQualityDistribution(
|
||||
event.param,
|
||||
);
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: AirQualityDistributionStatus.success,
|
||||
chartData: result,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(
|
||||
AirQualityDistributionState(
|
||||
status: AirQualityDistributionStatus.failure,
|
||||
errorMessage: e.toString(),
|
||||
selectedAqiType: state.selectedAqiType,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onClearAirQualityDistribution(
|
||||
ClearAirQualityDistribution event,
|
||||
Emitter<AirQualityDistributionState> emit,
|
||||
) async {
|
||||
emit(const AirQualityDistributionState());
|
||||
}
|
||||
|
||||
void _onUpdateAqiTypeEvent(
|
||||
UpdateAqiTypeEvent event,
|
||||
Emitter<AirQualityDistributionState> emit,
|
||||
) {
|
||||
emit(state.copyWith(selectedAqiType: event.aqiType));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
part of 'air_quality_distribution_bloc.dart';
|
||||
|
||||
sealed class AirQualityDistributionEvent extends Equatable {
|
||||
const AirQualityDistributionEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
final class LoadAirQualityDistribution extends AirQualityDistributionEvent {
|
||||
final GetAirQualityDistributionParam param;
|
||||
|
||||
const LoadAirQualityDistribution(this.param);
|
||||
|
||||
@override
|
||||
List<Object> get props => [param];
|
||||
}
|
||||
|
||||
final class UpdateAqiTypeEvent extends AirQualityDistributionEvent {
|
||||
const UpdateAqiTypeEvent(this.aqiType);
|
||||
|
||||
final AqiType aqiType;
|
||||
|
||||
@override
|
||||
List<Object> get props => [aqiType];
|
||||
}
|
||||
|
||||
final class ClearAirQualityDistribution extends AirQualityDistributionEvent {
|
||||
const ClearAirQualityDistribution();
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
part of 'air_quality_distribution_bloc.dart';
|
||||
|
||||
enum AirQualityDistributionStatus {
|
||||
initial,
|
||||
loading,
|
||||
success,
|
||||
failure,
|
||||
}
|
||||
|
||||
class AirQualityDistributionState extends Equatable {
|
||||
const AirQualityDistributionState({
|
||||
this.status = AirQualityDistributionStatus.initial,
|
||||
this.chartData = const [],
|
||||
this.errorMessage,
|
||||
this.selectedAqiType = AqiType.aqi,
|
||||
});
|
||||
|
||||
final AirQualityDistributionStatus status;
|
||||
final List<AirQualityDataModel> chartData;
|
||||
final String? errorMessage;
|
||||
final AqiType selectedAqiType;
|
||||
|
||||
AirQualityDistributionState copyWith({
|
||||
AirQualityDistributionStatus? status,
|
||||
List<AirQualityDataModel>? chartData,
|
||||
String? errorMessage,
|
||||
AqiType? selectedAqiType,
|
||||
}) {
|
||||
return AirQualityDistributionState(
|
||||
status: status ?? this.status,
|
||||
chartData: chartData ?? this.chartData,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
selectedAqiType: selectedAqiType ?? this.selectedAqiType,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [status, chartData, errorMessage, selectedAqiType];
|
||||
}
|
||||
@ -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];
|
||||
}
|
||||
@ -0,0 +1,80 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/range_of_aqi.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_range_of_aqi_param.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/range_of_aqi/range_of_aqi_service.dart';
|
||||
|
||||
part 'range_of_aqi_event.dart';
|
||||
part 'range_of_aqi_state.dart';
|
||||
|
||||
class RangeOfAqiBloc extends Bloc<RangeOfAqiEvent, RangeOfAqiState> {
|
||||
RangeOfAqiBloc(this._rangeOfAqiService) : super(const RangeOfAqiState()) {
|
||||
on<LoadRangeOfAqiEvent>(_onLoadRangeOfAqiEvent);
|
||||
on<ClearRangeOfAqiEvent>(_onClearRangeOfAqiEvent);
|
||||
on<UpdateAqiTypeEvent>(_onUpdateAqiTypeEvent);
|
||||
}
|
||||
|
||||
final RangeOfAqiService _rangeOfAqiService;
|
||||
|
||||
Future<void> _onLoadRangeOfAqiEvent(
|
||||
LoadRangeOfAqiEvent event,
|
||||
Emitter<RangeOfAqiState> emit,
|
||||
) async {
|
||||
emit(
|
||||
state.copyWith(status: RangeOfAqiStatus.loading),
|
||||
);
|
||||
try {
|
||||
final rangeOfAqi = await _rangeOfAqiService.load(event.param);
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: RangeOfAqiStatus.loaded,
|
||||
rangeOfAqi: rangeOfAqi,
|
||||
filteredRangeOfAqi: _arrangeChartDataByType(
|
||||
rangeOfAqi,
|
||||
state.selectedAqiType,
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: RangeOfAqiStatus.failure,
|
||||
errorMessage: '$e',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onUpdateAqiTypeEvent(
|
||||
UpdateAqiTypeEvent event,
|
||||
Emitter<RangeOfAqiState> emit,
|
||||
) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
selectedAqiType: event.aqiType,
|
||||
filteredRangeOfAqi: _arrangeChartDataByType(state.rangeOfAqi, event.aqiType),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<RangeOfAqi> _arrangeChartDataByType(
|
||||
List<RangeOfAqi> rangeOfAqi,
|
||||
AqiType aqiType,
|
||||
) {
|
||||
final filteredRangeOfAqi = rangeOfAqi.map(
|
||||
(data) => RangeOfAqi(
|
||||
date: data.date,
|
||||
data: data.data.where((value) => value.type == aqiType.code).toList(),
|
||||
),
|
||||
);
|
||||
return filteredRangeOfAqi.toList();
|
||||
}
|
||||
|
||||
void _onClearRangeOfAqiEvent(
|
||||
ClearRangeOfAqiEvent event,
|
||||
Emitter<RangeOfAqiState> emit,
|
||||
) {
|
||||
emit(const RangeOfAqiState());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
part of 'range_of_aqi_bloc.dart';
|
||||
|
||||
sealed class RangeOfAqiEvent extends Equatable {
|
||||
const RangeOfAqiEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class LoadRangeOfAqiEvent extends RangeOfAqiEvent {
|
||||
const LoadRangeOfAqiEvent(this.param);
|
||||
|
||||
final GetRangeOfAqiParam param;
|
||||
|
||||
@override
|
||||
List<Object> get props => [param];
|
||||
}
|
||||
|
||||
class UpdateAqiTypeEvent extends RangeOfAqiEvent {
|
||||
const UpdateAqiTypeEvent(this.aqiType);
|
||||
|
||||
final AqiType aqiType;
|
||||
|
||||
@override
|
||||
List<Object> get props => [aqiType];
|
||||
}
|
||||
|
||||
class ClearRangeOfAqiEvent extends RangeOfAqiEvent {
|
||||
const ClearRangeOfAqiEvent();
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
part of 'range_of_aqi_bloc.dart';
|
||||
|
||||
enum RangeOfAqiStatus { initial, loading, loaded, failure }
|
||||
|
||||
final class RangeOfAqiState extends Equatable {
|
||||
const RangeOfAqiState({
|
||||
this.rangeOfAqi = const [],
|
||||
this.filteredRangeOfAqi = const [],
|
||||
this.status = RangeOfAqiStatus.initial,
|
||||
this.errorMessage,
|
||||
this.selectedAqiType = AqiType.aqi,
|
||||
});
|
||||
|
||||
final RangeOfAqiStatus status;
|
||||
final List<RangeOfAqi> rangeOfAqi;
|
||||
final List<RangeOfAqi> filteredRangeOfAqi;
|
||||
final String? errorMessage;
|
||||
final AqiType selectedAqiType;
|
||||
|
||||
RangeOfAqiState copyWith({
|
||||
RangeOfAqiStatus? status,
|
||||
List<RangeOfAqi>? rangeOfAqi,
|
||||
List<RangeOfAqi>? filteredRangeOfAqi,
|
||||
String? errorMessage,
|
||||
AqiType? selectedAqiType,
|
||||
}) {
|
||||
return RangeOfAqiState(
|
||||
status: status ?? this.status,
|
||||
rangeOfAqi: rangeOfAqi ?? this.rangeOfAqi,
|
||||
filteredRangeOfAqi: filteredRangeOfAqi ?? this.filteredRangeOfAqi,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
selectedAqiType: selectedAqiType ?? this.selectedAqiType,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props =>
|
||||
[status, rangeOfAqi, filteredRangeOfAqi, errorMessage, selectedAqiType];
|
||||
}
|
||||
@ -0,0 +1,122 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/device_location/device_location_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_device_location_data_param.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_range_of_aqi_param.dart';
|
||||
|
||||
abstract final class FetchAirQualityDataHelper {
|
||||
const FetchAirQualityDataHelper._();
|
||||
|
||||
static void loadAirQualityData(
|
||||
BuildContext context, {
|
||||
required DateTime date,
|
||||
required String communityUuid,
|
||||
required String spaceUuid,
|
||||
bool shouldFetchAnalyticsDevices = true,
|
||||
}) {
|
||||
final date = context.read<AnalyticsDatePickerBloc>().state.monthlyDate;
|
||||
final aqiType = context.read<AirQualityDistributionBloc>().state.selectedAqiType;
|
||||
loadAnalyticsDevices(
|
||||
context,
|
||||
communityUuid: communityUuid,
|
||||
spaceUuid: spaceUuid,
|
||||
);
|
||||
loadRangeOfAqi(
|
||||
context,
|
||||
spaceUuid: spaceUuid,
|
||||
date: date,
|
||||
);
|
||||
loadAirQualityDistribution(
|
||||
context,
|
||||
spaceUuid: spaceUuid,
|
||||
date: date,
|
||||
aqiType: aqiType,
|
||||
);
|
||||
}
|
||||
|
||||
static void clearAllData(BuildContext context) {
|
||||
context.read<AnalyticsDevicesBloc>().add(
|
||||
const ClearAnalyticsDeviceEvent(),
|
||||
);
|
||||
context.read<RealtimeDeviceChangesBloc>().add(
|
||||
const RealtimeDeviceChangesClosed(),
|
||||
);
|
||||
context.read<AirQualityDistributionBloc>().add(
|
||||
const ClearAirQualityDistribution(),
|
||||
);
|
||||
context.read<RangeOfAqiBloc>().add(const ClearRangeOfAqiEvent());
|
||||
|
||||
context.read<DeviceLocationBloc>().add(const ClearDeviceLocationEvent());
|
||||
}
|
||||
|
||||
static void loadAnalyticsDevices(
|
||||
BuildContext context, {
|
||||
required String communityUuid,
|
||||
required String spaceUuid,
|
||||
}) {
|
||||
context.read<AnalyticsDevicesBloc>().add(
|
||||
LoadAnalyticsDevicesEvent(
|
||||
param: GetAnalyticsDevicesParam(
|
||||
communityUuid: communityUuid,
|
||||
spaceUuid: spaceUuid,
|
||||
deviceTypes: ['AQI'],
|
||||
requestType: AnalyticsDeviceRequestType.occupancy,
|
||||
),
|
||||
onSuccess: (device) {
|
||||
context.read<RealtimeDeviceChangesBloc>()
|
||||
..add(const RealtimeDeviceChangesClosed())
|
||||
..add(RealtimeDeviceChangesStarted(device.uuid));
|
||||
|
||||
context.read<DeviceLocationBloc>().add(
|
||||
LoadDeviceLocationEvent(
|
||||
GetDeviceLocationDataParam(
|
||||
latitude: device.latitude ?? 0,
|
||||
longitude: device.longitude ?? 0,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static void loadRangeOfAqi(
|
||||
BuildContext context, {
|
||||
required String spaceUuid,
|
||||
required DateTime date,
|
||||
}) {
|
||||
context.read<RangeOfAqiBloc>().add(
|
||||
LoadRangeOfAqiEvent(
|
||||
GetRangeOfAqiParam(
|
||||
date: date,
|
||||
spaceUuid: spaceUuid,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static void loadAirQualityDistribution(
|
||||
BuildContext context, {
|
||||
required String spaceUuid,
|
||||
required DateTime date,
|
||||
required AqiType aqiType,
|
||||
}) {
|
||||
context.read<AirQualityDistributionBloc>().add(
|
||||
LoadAirQualityDistribution(
|
||||
GetAirQualityDistributionParam(
|
||||
spaceUuid: spaceUuid,
|
||||
date: date,
|
||||
aqiType: aqiType,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,115 @@
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/range_of_aqi.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
abstract final class RangeOfAqiChartsHelper {
|
||||
const RangeOfAqiChartsHelper._();
|
||||
|
||||
static const gradientData = <(Color color, String label)>[
|
||||
(ColorsManager.goodGreen, 'Good'),
|
||||
(ColorsManager.moderateYellow, 'Moderate'),
|
||||
(ColorsManager.poorOrange, 'Poor'),
|
||||
(ColorsManager.unhealthyRed, 'Unhealthy'),
|
||||
(ColorsManager.severePink, 'Severe'),
|
||||
(ColorsManager.hazardousPurple, 'Hazardous'),
|
||||
];
|
||||
|
||||
static FlTitlesData titlesData(BuildContext context, List<RangeOfAqi> data) {
|
||||
final titlesData = EnergyManagementChartsHelper.titlesData(context);
|
||||
return titlesData.copyWith(
|
||||
bottomTitles: titlesData.bottomTitles.copyWith(
|
||||
sideTitles: titlesData.bottomTitles.sideTitles.copyWith(
|
||||
getTitlesWidget: (value, meta) => Padding(
|
||||
padding: const EdgeInsetsDirectional.only(top: 20.0),
|
||||
child: Text(
|
||||
data.isNotEmpty ? data[value.toInt()].date.day.toString() : '',
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
fontSize: 12,
|
||||
color: ColorsManager.lightGreyColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
leftTitles: titlesData.leftTitles.copyWith(
|
||||
sideTitles: titlesData.leftTitles.sideTitles.copyWith(
|
||||
reservedSize: 70,
|
||||
interval: 50,
|
||||
maxIncluded: false,
|
||||
getTitlesWidget: (value, meta) {
|
||||
final text = value >= 300 ? '301+' : value.toInt().toString();
|
||||
return Padding(
|
||||
padding: const EdgeInsetsDirectional.only(end: 12),
|
||||
child: FittedBox(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
text,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
fontSize: 12,
|
||||
color: ColorsManager.lightGreyColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static List<LineTooltipItem?> getTooltipItems(
|
||||
List<LineBarSpot> touchedSpots,
|
||||
List<RangeOfAqi> chartData,
|
||||
) {
|
||||
return touchedSpots.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final spot = entry.value;
|
||||
|
||||
final label = switch (spot.barIndex) {
|
||||
0 => 'Max',
|
||||
1 => 'Avg',
|
||||
2 => 'Min',
|
||||
_ => '',
|
||||
};
|
||||
|
||||
final date = DateFormat('dd/MM').format(chartData[spot.x.toInt()].date);
|
||||
|
||||
return LineTooltipItem(
|
||||
index == 0 ? '$date\n' : '',
|
||||
const TextStyle(
|
||||
color: ColorsManager.textPrimaryColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 12,
|
||||
),
|
||||
children: [
|
||||
TextSpan(text: '$label: ${spot.y.toStringAsFixed(0)}'),
|
||||
],
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
static LineTouchData lineTouchData(
|
||||
List<RangeOfAqi> chartData,
|
||||
) {
|
||||
return LineTouchData(
|
||||
touchTooltipData: LineTouchTooltipData(
|
||||
getTooltipColor: (touchTooltipItem) => ColorsManager.whiteColors,
|
||||
tooltipBorder: const BorderSide(
|
||||
color: ColorsManager.semiTransparentBlack,
|
||||
),
|
||||
tooltipRoundedRadius: 16,
|
||||
showOnTopOfTheChartBoxArea: false,
|
||||
tooltipPadding: const EdgeInsets.all(8),
|
||||
getTooltipItems: (touchedSpots) => RangeOfAqiChartsHelper.getTooltipItems(
|
||||
touchedSpots,
|
||||
chartData,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,71 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart';
|
||||
|
||||
class AirQualityView extends StatelessWidget {
|
||||
const AirQualityView({super.key});
|
||||
|
||||
static const _padding = EdgeInsetsDirectional.all(32);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final isMediumOrLess = constraints.maxWidth <= 900;
|
||||
final height = MediaQuery.sizeOf(context).height;
|
||||
if (isMediumOrLess) {
|
||||
return SingleChildScrollView(
|
||||
padding: _padding,
|
||||
child: Column(
|
||||
spacing: 32,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: height * 1.2,
|
||||
child: const AirQualityEndSideWidget(),
|
||||
),
|
||||
SizedBox(
|
||||
height: height * 0.5,
|
||||
child: const RangeOfAqiChartBox(),
|
||||
),
|
||||
SizedBox(
|
||||
height: height * 0.5,
|
||||
child: const AqiDistributionChartBox(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Container(
|
||||
padding: _padding,
|
||||
height: height * 1.1,
|
||||
child: const Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
spacing: 32,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 10,
|
||||
child: Column(
|
||||
spacing: 20,
|
||||
children: [
|
||||
Expanded(child: RangeOfAqiChartBox()),
|
||||
Expanded(child: AqiDistributionChartBox()),
|
||||
],
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
import 'package:flutter/material.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 {
|
||||
const AirQualityEndSideWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: subSectionContainerDecoration.copyWith(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
padding: const EdgeInsetsDirectional.all(32),
|
||||
child: const Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AnalyticsSidebarHeader(title: 'AQI Sensor'),
|
||||
Expanded(flex: 15, child: AqiDeviceInfo()),
|
||||
SizedBox(height: 20),
|
||||
Expanded(flex: 6, child: AqiLocationInfo()),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,128 @@
|
||||
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: Column(
|
||||
spacing: 6,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
const AirQualityEndSideLiveIndicator(),
|
||||
AirQualityEndSideGaugeAndInfo(
|
||||
aqiLevel: status
|
||||
.firstWhere(
|
||||
(e) => e.code == 'air_quality_index',
|
||||
orElse: () => Status(code: 'air_quality_index', value: ''),
|
||||
)
|
||||
.value
|
||||
.toString(),
|
||||
temperature: int.parse(tempValue),
|
||||
humidity: int.parse(humidityValue),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
AqiSubValueWidget(
|
||||
range: (0, 999),
|
||||
label: AqiType.pm25.value,
|
||||
value: pm25Value,
|
||||
unit: AqiType.pm25.unit,
|
||||
),
|
||||
AqiSubValueWidget(
|
||||
range: (0, 999),
|
||||
label: AqiType.pm10.value,
|
||||
value: pm10Value,
|
||||
unit: AqiType.pm10.unit,
|
||||
),
|
||||
AqiSubValueWidget(
|
||||
range: (0, 5),
|
||||
label: AqiType.hcho.value,
|
||||
value: ch2oValue,
|
||||
unit: AqiType.hcho.unit,
|
||||
),
|
||||
AqiSubValueWidget(
|
||||
range: (0, 999),
|
||||
label: AqiType.tvoc.value,
|
||||
value: tvocValue,
|
||||
unit: AqiType.tvoc.unit,
|
||||
),
|
||||
AqiSubValueWidget(
|
||||
range: (0, 5000),
|
||||
label: AqiType.co2.value,
|
||||
value: co2Value,
|
||||
unit: AqiType.co2.unit,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,164 @@
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class AqiDistributionChart extends StatelessWidget {
|
||||
const AqiDistributionChart({super.key, required this.chartData});
|
||||
final List<AirQualityDataModel> chartData;
|
||||
|
||||
static const _rodStackItemsSpacing = 0.4;
|
||||
static const _barWidth = 13.0;
|
||||
static final _barBorderRadius = BorderRadius.circular(22);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BarChart(
|
||||
BarChartData(
|
||||
maxY: 100.1,
|
||||
gridData: EnergyManagementChartsHelper.gridData(
|
||||
horizontalInterval: 20,
|
||||
),
|
||||
borderData: EnergyManagementChartsHelper.borderData(),
|
||||
barTouchData: _barTouchData(context),
|
||||
titlesData: _titlesData(context),
|
||||
barGroups: _buildBarGroups(),
|
||||
),
|
||||
duration: Duration.zero,
|
||||
);
|
||||
}
|
||||
|
||||
List<BarChartGroupData> _buildBarGroups() {
|
||||
return List.generate(chartData.length, (index) {
|
||||
final data = chartData[index];
|
||||
final stackItems = <BarChartRodData>[];
|
||||
double currentY = 0;
|
||||
var isFirstElement = true;
|
||||
|
||||
for (final percentageData in data.data) {
|
||||
stackItems.add(
|
||||
BarChartRodData(
|
||||
fromY: currentY,
|
||||
toY: currentY + percentageData.percentage,
|
||||
color: AirQualityDataModel.metricColors[percentageData.type],
|
||||
borderRadius: isFirstElement
|
||||
? const BorderRadius.only(
|
||||
topLeft: Radius.circular(22),
|
||||
topRight: Radius.circular(22),
|
||||
)
|
||||
: _barBorderRadius,
|
||||
width: _barWidth,
|
||||
),
|
||||
);
|
||||
currentY += percentageData.percentage + _rodStackItemsSpacing;
|
||||
isFirstElement = false;
|
||||
}
|
||||
|
||||
return BarChartGroupData(
|
||||
x: index,
|
||||
barRods: stackItems,
|
||||
groupVertically: true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
BarTouchData _barTouchData(BuildContext context) {
|
||||
return BarTouchData(
|
||||
touchTooltipData: BarTouchTooltipData(
|
||||
getTooltipColor: (_) => ColorsManager.whiteColors,
|
||||
tooltipBorder: const BorderSide(
|
||||
color: ColorsManager.semiTransparentBlack,
|
||||
),
|
||||
tooltipRoundedRadius: 16,
|
||||
tooltipPadding: const EdgeInsets.all(8),
|
||||
getTooltipItem: (group, groupIndex, rod, rodIndex) {
|
||||
final data = chartData[group.x];
|
||||
|
||||
final children = <TextSpan>[];
|
||||
|
||||
final textStyle = context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
fontSize: 8,
|
||||
);
|
||||
|
||||
for (final percentageData in data.data) {
|
||||
final percentage = percentageData.percentage.toStringAsFixed(1);
|
||||
final type = percentageData.type[0].toUpperCase() +
|
||||
percentageData.type.substring(1).replaceAll('_', ' ');
|
||||
children.add(TextSpan(
|
||||
text: '\n$type: $percentage%',
|
||||
style: textStyle,
|
||||
));
|
||||
}
|
||||
|
||||
return BarTooltipItem(
|
||||
DateFormat('dd/MM/yyyy').format(data.date),
|
||||
context.textTheme.bodyMedium!.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
fontSize: 9,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
textAlign: TextAlign.start,
|
||||
children: children,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
FlTitlesData _titlesData(BuildContext context) {
|
||||
final titlesData = EnergyManagementChartsHelper.titlesData(
|
||||
context,
|
||||
leftTitlesInterval: 20,
|
||||
);
|
||||
|
||||
final leftTitles = titlesData.leftTitles.copyWith(
|
||||
sideTitles: titlesData.leftTitles.sideTitles.copyWith(
|
||||
reservedSize: 70,
|
||||
interval: 20,
|
||||
maxIncluded: false,
|
||||
minIncluded: true,
|
||||
getTitlesWidget: (value, meta) => Padding(
|
||||
padding: const EdgeInsetsDirectional.only(end: 12),
|
||||
child: FittedBox(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
'${value.toStringAsFixed(0)}%',
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
fontSize: 12,
|
||||
color: ColorsManager.lightGreyColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final bottomTitles = AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
getTitlesWidget: (value, _) => FittedBox(
|
||||
alignment: AlignmentDirectional.bottomCenter,
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
chartData[value.toInt()].date.day.toString(),
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.lightGreyColor,
|
||||
fontSize: 8,
|
||||
),
|
||||
),
|
||||
),
|
||||
reservedSize: 36,
|
||||
),
|
||||
);
|
||||
|
||||
return titlesData.copyWith(
|
||||
leftTitles: leftTitles,
|
||||
bottomTitles: bottomTitles,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart';
|
||||
import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart';
|
||||
import 'package:syncrow_web/utils/style.dart';
|
||||
|
||||
class AqiDistributionChartBox extends StatelessWidget {
|
||||
const AqiDistributionChartBox({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<AirQualityDistributionBloc, AirQualityDistributionState>(
|
||||
builder: (context, state) {
|
||||
return Container(
|
||||
padding: const EdgeInsetsDirectional.all(30),
|
||||
decoration: subSectionContainerDecoration.copyWith(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (state.errorMessage != null) ...[
|
||||
AnalyticsErrorWidget(state.errorMessage),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
AqiDistributionChartTitle(
|
||||
isLoading: state.status == AirQualityDistributionStatus.loading,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const Divider(),
|
||||
const SizedBox(height: 20),
|
||||
Expanded(
|
||||
child: AqiDistributionChart(chartData: state.chartData),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,68 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart';
|
||||
import 'package:syncrow_web/pages/analytics/widgets/charts_loading_widget.dart';
|
||||
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
|
||||
|
||||
class AqiDistributionChartTitle extends StatelessWidget {
|
||||
const AqiDistributionChartTitle({required this.isLoading, super.key});
|
||||
|
||||
final bool isLoading;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
ChartsLoadingWidget(isLoading: isLoading),
|
||||
const Expanded(
|
||||
flex: 3,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: ChartTitle(
|
||||
title: Text('Distribution over Air Quality Index'),
|
||||
),
|
||||
),
|
||||
),
|
||||
FittedBox(
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
fit: BoxFit.scaleDown,
|
||||
child: AqiTypeDropdown(
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
final bloc = context.read<AirQualityDistributionBloc>();
|
||||
try {
|
||||
final param = _makeLoadAqiDistributionParam(context, value);
|
||||
bloc.add(LoadAirQualityDistribution(param));
|
||||
} catch (_) {
|
||||
return;
|
||||
} finally {
|
||||
bloc.add(UpdateAqiTypeEvent(value));
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
GetAirQualityDistributionParam _makeLoadAqiDistributionParam(
|
||||
BuildContext context,
|
||||
AqiType aqiType,
|
||||
) {
|
||||
final date = context.read<AnalyticsDatePickerBloc>().state.monthlyDate;
|
||||
final spaceUuid =
|
||||
context.read<SpaceTreeBloc>().state.selectedSpaces.firstOrNull ?? '';
|
||||
if (spaceUuid.isEmpty) throw Exception('Space UUID is empty');
|
||||
return GetAirQualityDistributionParam(
|
||||
date: date,
|
||||
spaceUuid: spaceUuid,
|
||||
aqiType: aqiType,
|
||||
);
|
||||
}
|
||||
}
|
||||
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,79 @@
|
||||
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({
|
||||
required this.city,
|
||||
required this.country,
|
||||
required this.address,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String? city;
|
||||
final String? country;
|
||||
final String? address;
|
||||
|
||||
String _getFormattedLocation() {
|
||||
if (city == null && country == null && address == null) {
|
||||
return 'N/A';
|
||||
}
|
||||
|
||||
final parts = <String>[];
|
||||
|
||||
if (city != null) parts.add(city!);
|
||||
if (address != null) parts.add(address!);
|
||||
final locationPart = parts.join(', ');
|
||||
|
||||
if (country != null) {
|
||||
return locationPart.isEmpty ? country! : '$locationPart - $country';
|
||||
}
|
||||
|
||||
return locationPart;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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(
|
||||
_getFormattedLocation(),
|
||||
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,56 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/device_location/device_location_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_location.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_location_info_cell.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
import 'package:syncrow_web/utils/style.dart';
|
||||
|
||||
class AqiLocationInfo extends StatelessWidget {
|
||||
const AqiLocationInfo({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<DeviceLocationBloc, DeviceLocationState>(
|
||||
builder: (context, state) {
|
||||
final info = state.locationInfo;
|
||||
return Container(
|
||||
decoration: secondarySection.copyWith(boxShadow: const []),
|
||||
padding: const EdgeInsetsDirectional.all(20),
|
||||
child: Column(
|
||||
spacing: 8,
|
||||
children: [
|
||||
AqiLocation(
|
||||
city: info?.city,
|
||||
country: info?.country,
|
||||
address: info?.address,
|
||||
),
|
||||
Expanded(
|
||||
child: Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
AqiLocationInfoCell(
|
||||
label: 'Temperature',
|
||||
value: ' ${info?.temperature?.roundToDouble() ?? '--'}°',
|
||||
svgPath: Assets.aqiTemperature,
|
||||
),
|
||||
AqiLocationInfoCell(
|
||||
label: 'Humidity',
|
||||
value: '${info?.humidity?.roundToDouble() ?? '--'}%',
|
||||
svgPath: Assets.aqiHumidity,
|
||||
),
|
||||
AqiLocationInfoCell(
|
||||
label: 'Air Quality',
|
||||
value: ' ${info?.airQuality?.roundToDouble() ?? '--'}',
|
||||
svgPath: Assets.aqiAirQuality,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,77 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
enum AqiType {
|
||||
aqi('AQI', '', 'aqi'),
|
||||
pm25('PM2.5', 'µg/m³', 'pm25'),
|
||||
pm10('PM10', 'µg/m³', 'pm10'),
|
||||
hcho('HCHO', 'mg/m³', 'cho2'),
|
||||
tvoc('TVOC', 'µg/m³', 'voc'),
|
||||
co2('CO2', 'ppm', 'co2');
|
||||
|
||||
const AqiType(this.value, this.unit, this.code);
|
||||
|
||||
final String value;
|
||||
final String unit;
|
||||
final String code;
|
||||
}
|
||||
|
||||
class AqiTypeDropdown extends StatefulWidget {
|
||||
const AqiTypeDropdown({super.key, required this.onChanged});
|
||||
|
||||
final ValueChanged<AqiType?> onChanged;
|
||||
|
||||
@override
|
||||
State<AqiTypeDropdown> createState() => _AqiTypeDropdownState();
|
||||
}
|
||||
|
||||
class _AqiTypeDropdownState extends State<AqiTypeDropdown> {
|
||||
AqiType? _selectedItem = AqiType.aqi;
|
||||
|
||||
void _updateSelectedItem(AqiType? item) => setState(() => _selectedItem = item);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: ColorsManager.greyColor,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: DropdownButton<AqiType?>(
|
||||
value: _selectedItem,
|
||||
isDense: true,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
dropdownColor: ColorsManager.whiteColors,
|
||||
underline: const SizedBox.shrink(),
|
||||
icon: const RotatedBox(
|
||||
quarterTurns: 1,
|
||||
child: Icon(Icons.chevron_right, size: 24),
|
||||
),
|
||||
style: _getTextStyle(context),
|
||||
padding: const EdgeInsetsDirectional.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 2,
|
||||
),
|
||||
items: AqiType.values
|
||||
.map((e) => DropdownMenuItem(value: e, child: Text(e.value)))
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
_updateSelectedItem(value);
|
||||
widget.onChanged(value);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
TextStyle? _getTextStyle(BuildContext context) {
|
||||
return context.textTheme.labelSmall?.copyWith(
|
||||
color: ColorsManager.textPrimaryColor,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 12,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,111 @@
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/range_of_aqi.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class RangeOfAqiChart extends StatelessWidget {
|
||||
final List<RangeOfAqi> chartData;
|
||||
|
||||
const RangeOfAqiChart({
|
||||
super.key,
|
||||
required this.chartData,
|
||||
});
|
||||
|
||||
List<(List<double> values, Color color, Color? dotColor)> get _lines {
|
||||
final sortedData = List<RangeOfAqi>.from(chartData)
|
||||
..sort((a, b) => a.date.compareTo(b.date));
|
||||
|
||||
return [
|
||||
(
|
||||
sortedData.map((e) {
|
||||
final value = e.data.firstOrNull;
|
||||
return value?.max ?? 0;
|
||||
}).toList(),
|
||||
ColorsManager.maxPurple,
|
||||
ColorsManager.maxPurpleDot,
|
||||
),
|
||||
(
|
||||
sortedData.map((e) {
|
||||
final value = e.data.firstOrNull;
|
||||
return value?.average ?? 0;
|
||||
}).toList(),
|
||||
Colors.white,
|
||||
null,
|
||||
),
|
||||
(
|
||||
sortedData.map((e) {
|
||||
final value = e.data.firstOrNull;
|
||||
return value?.min ?? 0;
|
||||
}).toList(),
|
||||
ColorsManager.minBlue,
|
||||
ColorsManager.minBlueDot,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LineChart(
|
||||
LineChartData(
|
||||
minY: 0,
|
||||
maxY: 301,
|
||||
clipData: const FlClipData.vertical(),
|
||||
gridData: EnergyManagementChartsHelper.gridData(horizontalInterval: 50),
|
||||
titlesData: RangeOfAqiChartsHelper.titlesData(context, chartData),
|
||||
borderData: EnergyManagementChartsHelper.borderData(),
|
||||
lineTouchData: RangeOfAqiChartsHelper.lineTouchData(chartData),
|
||||
betweenBarsData: [
|
||||
BetweenBarsData(
|
||||
fromIndex: 0,
|
||||
toIndex: 2,
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
stops: const [0.0, 0.2, 0.4, 0.6, 0.8, 1.0],
|
||||
colors: RangeOfAqiChartsHelper.gradientData.map((e) {
|
||||
final (color, _) = e;
|
||||
return color.withValues(alpha: 0.6);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
lineBarsData: _lines.map((e) {
|
||||
final (values, color, dotColor) = e;
|
||||
return _buildLine(values: values, color: color, dotColor: dotColor);
|
||||
}).toList(),
|
||||
),
|
||||
duration: Duration.zero,
|
||||
);
|
||||
}
|
||||
|
||||
FlDotData _buildDotData(Color color) {
|
||||
return FlDotData(
|
||||
show: true,
|
||||
getDotPainter: (_, __, ___, ____) => FlDotCirclePainter(
|
||||
radius: 2,
|
||||
color: ColorsManager.whiteColors,
|
||||
strokeWidth: 2,
|
||||
strokeColor: color,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
LineChartBarData _buildLine({
|
||||
required List<double> values,
|
||||
required Color color,
|
||||
Color? dotColor,
|
||||
}) {
|
||||
const invisibleDot = FlDotData(show: false);
|
||||
return LineChartBarData(
|
||||
spots: List.generate(values.length, (i) => FlSpot(i.toDouble(), values[i])),
|
||||
isCurved: true,
|
||||
color: color,
|
||||
barWidth: 4,
|
||||
isStrokeCapRound: true,
|
||||
dotData: dotColor != null ? _buildDotData(dotColor) : invisibleDot,
|
||||
belowBarData: BarAreaData(show: false),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart';
|
||||
import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart';
|
||||
import 'package:syncrow_web/utils/style.dart';
|
||||
|
||||
class RangeOfAqiChartBox extends StatelessWidget {
|
||||
const RangeOfAqiChartBox({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<RangeOfAqiBloc, RangeOfAqiState>(
|
||||
builder: (context, state) {
|
||||
return Container(
|
||||
padding: const EdgeInsetsDirectional.all(30),
|
||||
decoration: subSectionContainerDecoration.copyWith(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (state.errorMessage != null) ...[
|
||||
AnalyticsErrorWidget(state.errorMessage),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
RangeOfAqiChartTitle(
|
||||
isLoading: state.status == RangeOfAqiStatus.loading,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const Divider(),
|
||||
const SizedBox(height: 20),
|
||||
Expanded(child: RangeOfAqiChart(chartData: state.filteredRangeOfAqi)),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,82 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart';
|
||||
import 'package:syncrow_web/pages/analytics/widgets/charts_loading_widget.dart';
|
||||
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
|
||||
|
||||
class RangeOfAqiChartTitle extends StatelessWidget {
|
||||
const RangeOfAqiChartTitle({
|
||||
required this.isLoading,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final bool isLoading;
|
||||
|
||||
static const List<(Color color, String title, bool hasBorder)> _colors = [
|
||||
(Color(0xFF962DFF), 'Max', false),
|
||||
(Color(0xFF93AAFD), 'Min', false),
|
||||
(Colors.transparent, 'Avg', true),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
ChartsLoadingWidget(isLoading: isLoading),
|
||||
const Expanded(
|
||||
flex: 3,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: ChartTitle(title: Text('Range of AQI')),
|
||||
),
|
||||
),
|
||||
const Spacer(flex: 3),
|
||||
..._colors.map(
|
||||
(e) {
|
||||
final (color, title, hasBorder) = e;
|
||||
return Expanded(
|
||||
child: IntrinsicHeight(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.fitWidth,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsDirectional.only(end: 16),
|
||||
child: ChartInformativeCell(
|
||||
title: Text(title),
|
||||
color: color,
|
||||
hasBorder: hasBorder,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const Spacer(),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
child: AqiTypeDropdown(
|
||||
onChanged: (value) {
|
||||
final spaceTreeState = context.read<SpaceTreeBloc>().state;
|
||||
final spaceUuid = spaceTreeState.selectedSpaces.firstOrNull;
|
||||
|
||||
if (spaceUuid == null) return;
|
||||
|
||||
if (value != null) {
|
||||
context.read<RangeOfAqiBloc>().add(UpdateAqiTypeEvent(value));
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -2,16 +2,23 @@ import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
part 'analytics_date_picker_event.dart';
|
||||
part 'analytics_date_picker_state.dart';
|
||||
|
||||
class AnalyticsDatePickerBloc extends Bloc<AnalyticsDatePickerEvent, DateTime> {
|
||||
AnalyticsDatePickerBloc() : super(DateTime.now()) {
|
||||
class AnalyticsDatePickerBloc
|
||||
extends Bloc<AnalyticsDatePickerEvent, AnalyticsDatePickerState> {
|
||||
AnalyticsDatePickerBloc() : super(AnalyticsDatePickerState()) {
|
||||
on<UpdateAnalyticsDatePickerEvent>(_onUpdateAnalyticsDatePickerEvent);
|
||||
}
|
||||
|
||||
void _onUpdateAnalyticsDatePickerEvent(
|
||||
UpdateAnalyticsDatePickerEvent event,
|
||||
Emitter<DateTime> emit,
|
||||
Emitter<AnalyticsDatePickerState> emit,
|
||||
) {
|
||||
emit(event.date);
|
||||
emit(
|
||||
state.copyWith(
|
||||
monthlyDate: event.montlyDate ?? state.monthlyDate,
|
||||
yearlyDate: event.yearlyDate ?? state.yearlyDate,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,10 +8,11 @@ sealed class AnalyticsDatePickerEvent extends Equatable {
|
||||
}
|
||||
|
||||
final class UpdateAnalyticsDatePickerEvent extends AnalyticsDatePickerEvent {
|
||||
const UpdateAnalyticsDatePickerEvent(this.date);
|
||||
const UpdateAnalyticsDatePickerEvent({this.montlyDate, this.yearlyDate});
|
||||
|
||||
final DateTime date;
|
||||
final DateTime? montlyDate;
|
||||
final DateTime? yearlyDate;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [date];
|
||||
List<Object?> get props => [montlyDate, yearlyDate];
|
||||
}
|
||||
|
||||
@ -0,0 +1,25 @@
|
||||
part of 'analytics_date_picker_bloc.dart';
|
||||
|
||||
final class AnalyticsDatePickerState extends Equatable {
|
||||
AnalyticsDatePickerState({
|
||||
DateTime? monthlyDate,
|
||||
DateTime? yearlyDate,
|
||||
}) : monthlyDate = monthlyDate ?? DateTime.now(),
|
||||
yearlyDate = yearlyDate ?? DateTime.now();
|
||||
|
||||
final DateTime monthlyDate;
|
||||
final DateTime yearlyDate;
|
||||
|
||||
AnalyticsDatePickerState copyWith({
|
||||
DateTime? monthlyDate,
|
||||
DateTime? yearlyDate,
|
||||
}) {
|
||||
return AnalyticsDatePickerState(
|
||||
monthlyDate: monthlyDate ?? this.monthlyDate,
|
||||
yearlyDate: yearlyDate ?? this.yearlyDate,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [monthlyDate, yearlyDate];
|
||||
}
|
||||
@ -0,0 +1,77 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.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/services/analytics_devices/analytics_devices_service.dart';
|
||||
import 'package:syncrow_web/services/api/api_exception.dart';
|
||||
|
||||
part 'analytics_devices_event.dart';
|
||||
part 'analytics_devices_state.dart';
|
||||
|
||||
class AnalyticsDevicesBloc
|
||||
extends Bloc<AnalyticsDevicesEvent, AnalyticsDevicesState> {
|
||||
AnalyticsDevicesBloc(
|
||||
this._analyticsDevicesService,
|
||||
) : super(const AnalyticsDevicesState()) {
|
||||
on<LoadAnalyticsDevicesEvent>(_onLoadAnalyticsDevices);
|
||||
on<SelectAnalyticsDeviceEvent>(_onSelectAnalyticsDevice);
|
||||
on<ClearAnalyticsDeviceEvent>(_onClearAnalyticsDevice);
|
||||
}
|
||||
final AnalyticsDevicesService _analyticsDevicesService;
|
||||
|
||||
Future<void> _onLoadAnalyticsDevices(
|
||||
LoadAnalyticsDevicesEvent event,
|
||||
Emitter<AnalyticsDevicesState> emit,
|
||||
) async {
|
||||
emit(const AnalyticsDevicesState(status: AnalyticsDevicesStatus.loading));
|
||||
|
||||
try {
|
||||
final devices = await _analyticsDevicesService.getDevices(event.param);
|
||||
emit(
|
||||
AnalyticsDevicesState(
|
||||
status: AnalyticsDevicesStatus.loaded,
|
||||
devices: devices,
|
||||
selectedDevice: devices.firstOrNull,
|
||||
),
|
||||
);
|
||||
if (devices.isNotEmpty) {
|
||||
event.onSuccess(devices.first);
|
||||
}
|
||||
} on APIException catch (e) {
|
||||
emit(
|
||||
AnalyticsDevicesState(
|
||||
status: AnalyticsDevicesStatus.failure,
|
||||
errorMessage: e.message,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(
|
||||
AnalyticsDevicesState(
|
||||
status: AnalyticsDevicesStatus.failure,
|
||||
errorMessage: e.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onSelectAnalyticsDevice(
|
||||
SelectAnalyticsDeviceEvent event,
|
||||
Emitter<AnalyticsDevicesState> emit,
|
||||
) {
|
||||
emit(
|
||||
AnalyticsDevicesState(
|
||||
selectedDevice: event.device,
|
||||
devices: state.devices,
|
||||
errorMessage: state.errorMessage,
|
||||
status: state.status,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onClearAnalyticsDevice(
|
||||
ClearAnalyticsDeviceEvent event,
|
||||
Emitter<AnalyticsDevicesState> emit,
|
||||
) {
|
||||
emit(const AnalyticsDevicesState());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
part of 'analytics_devices_bloc.dart';
|
||||
|
||||
sealed class AnalyticsDevicesEvent extends Equatable {
|
||||
const AnalyticsDevicesEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
final class LoadAnalyticsDevicesEvent extends AnalyticsDevicesEvent {
|
||||
const LoadAnalyticsDevicesEvent({required this.param, required this.onSuccess});
|
||||
|
||||
final GetAnalyticsDevicesParam param;
|
||||
final void Function(AnalyticsDevice device) onSuccess;
|
||||
|
||||
@override
|
||||
List<Object> get props => [param];
|
||||
}
|
||||
|
||||
final class SelectAnalyticsDeviceEvent extends AnalyticsDevicesEvent {
|
||||
const SelectAnalyticsDeviceEvent(this.device);
|
||||
|
||||
final AnalyticsDevice device;
|
||||
|
||||
@override
|
||||
List<Object> get props => [device];
|
||||
}
|
||||
|
||||
final class ClearAnalyticsDeviceEvent extends AnalyticsDevicesEvent {
|
||||
const ClearAnalyticsDeviceEvent();
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
part of 'analytics_devices_bloc.dart';
|
||||
|
||||
enum AnalyticsDevicesStatus { initial, loading, loaded, failure }
|
||||
|
||||
final class AnalyticsDevicesState extends Equatable {
|
||||
const AnalyticsDevicesState({
|
||||
this.status = AnalyticsDevicesStatus.initial,
|
||||
this.devices = const [],
|
||||
this.errorMessage,
|
||||
this.selectedDevice,
|
||||
});
|
||||
|
||||
final AnalyticsDevicesStatus status;
|
||||
final List<AnalyticsDevice> devices;
|
||||
final AnalyticsDevice? selectedDevice;
|
||||
final String? errorMessage;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [status, devices, errorMessage, selectedDevice];
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/views/air_quality_view.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/views/analytics_energy_management_view.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/views/analytics_occupancy_view.dart';
|
||||
|
||||
@ -10,6 +11,10 @@ enum AnalyticsPageTab {
|
||||
occupancy(
|
||||
title: 'Occupancy',
|
||||
child: AnalyticsOccupancyView(),
|
||||
),
|
||||
airQuality(
|
||||
title: 'Air Quality',
|
||||
child: AirQualityView(),
|
||||
);
|
||||
|
||||
const AnalyticsPageTab({
|
||||
|
||||
@ -0,0 +1,54 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/strategies/analytics_data_loading_strategy.dart';
|
||||
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
|
||||
|
||||
final class AirQualityDataLoadingStrategy implements AnalyticsDataLoadingStrategy {
|
||||
@override
|
||||
void onCommunitySelected(
|
||||
BuildContext context,
|
||||
CommunityModel community,
|
||||
List<SpaceModel> spaces,
|
||||
) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@override
|
||||
void onSpaceSelected(
|
||||
BuildContext context,
|
||||
CommunityModel community,
|
||||
SpaceModel space,
|
||||
) {
|
||||
final spaceTreeBloc = context.read<SpaceTreeBloc>();
|
||||
final isSpaceSelected = spaceTreeBloc.state.selectedSpaces.contains(space.uuid);
|
||||
|
||||
final hasSelectedSpaces = spaceTreeBloc.state.selectedSpaces.isNotEmpty;
|
||||
if (hasSelectedSpaces) clearData(context);
|
||||
|
||||
if (isSpaceSelected) return;
|
||||
|
||||
spaceTreeBloc
|
||||
..add(const SpaceTreeClearSelectionEvent())
|
||||
..add(OnSpaceSelected(community, space.uuid ?? '', []));
|
||||
|
||||
FetchAirQualityDataHelper.loadAirQualityData(
|
||||
context,
|
||||
communityUuid: community.uuid,
|
||||
spaceUuid: space.uuid ?? '',
|
||||
date: context.read<AnalyticsDatePickerBloc>().state.monthlyDate,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void clearData(BuildContext context) {
|
||||
context.read<SpaceTreeBloc>().add(
|
||||
const AnalyticsClearAllSpaceTreeSelectionsEvent(),
|
||||
);
|
||||
FetchAirQualityDataHelper.clearAllData(context);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
|
||||
|
||||
abstract class AnalyticsDataLoadingStrategy {
|
||||
void onCommunitySelected(
|
||||
BuildContext context,
|
||||
CommunityModel community,
|
||||
List<SpaceModel> spaces,
|
||||
);
|
||||
void onSpaceSelected(
|
||||
BuildContext context,
|
||||
CommunityModel community,
|
||||
SpaceModel space,
|
||||
);
|
||||
void clearData(BuildContext context);
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/enums/analytics_page_tab.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/strategies/air_quality_data_loading_strategy.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/strategies/analytics_data_loading_strategy.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/strategies/energy_management_data_loading_strategy.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/strategies/occupancy_data_loading_strategy.dart';
|
||||
|
||||
abstract final class AnalyticsDataLoadingStrategyFactory {
|
||||
const AnalyticsDataLoadingStrategyFactory._();
|
||||
static AnalyticsDataLoadingStrategy getStrategy(AnalyticsPageTab tab) {
|
||||
return switch (tab) {
|
||||
AnalyticsPageTab.energyManagement => EnergyManagementDataLoadingStrategy(),
|
||||
AnalyticsPageTab.occupancy => OccupancyDataLoadingStrategy(),
|
||||
AnalyticsPageTab.airQuality => AirQualityDataLoadingStrategy(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,72 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/strategies/analytics_data_loading_strategy.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/fetch_energy_management_data_helper.dart';
|
||||
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
|
||||
|
||||
class EnergyManagementDataLoadingStrategy implements AnalyticsDataLoadingStrategy {
|
||||
@override
|
||||
void onCommunitySelected(
|
||||
BuildContext context,
|
||||
CommunityModel community,
|
||||
List<SpaceModel> spaces,
|
||||
) {
|
||||
final spaceTreeBloc = context.read<SpaceTreeBloc>();
|
||||
final isCommunitySelected =
|
||||
spaceTreeBloc.state.selectedCommunities.contains(community.uuid);
|
||||
|
||||
if (isCommunitySelected) {
|
||||
clearData(context);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onSpaceSelected(
|
||||
BuildContext context,
|
||||
CommunityModel community,
|
||||
SpaceModel space,
|
||||
) {
|
||||
final spaceTreeBloc = context.read<SpaceTreeBloc>();
|
||||
final isSpaceSelected = spaceTreeBloc.state.selectedSpaces.contains(space.uuid);
|
||||
final hasSelectedSpaces = spaceTreeBloc.state.selectedSpaces.isNotEmpty;
|
||||
|
||||
if (isSpaceSelected) {
|
||||
final firstSelectedSpace = spaceTreeBloc.state.selectedSpaces.first;
|
||||
final isTheFirstSelectedSpace = firstSelectedSpace == space.uuid;
|
||||
if (isTheFirstSelectedSpace) {
|
||||
clearData(context);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasSelectedSpaces) {
|
||||
clearData(context);
|
||||
}
|
||||
|
||||
spaceTreeBloc.add(
|
||||
OnSpaceSelected(
|
||||
community,
|
||||
space.uuid ?? '',
|
||||
space.children,
|
||||
),
|
||||
);
|
||||
|
||||
FetchEnergyManagementDataHelper.loadEnergyManagementData(
|
||||
context,
|
||||
communityId: community.uuid,
|
||||
spaceId: space.uuid ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void clearData(BuildContext context) {
|
||||
context.read<SpaceTreeBloc>().add(
|
||||
const AnalyticsClearAllSpaceTreeSelectionsEvent(),
|
||||
);
|
||||
FetchEnergyManagementDataHelper.clearAllData(context);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,52 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/strategies/analytics_data_loading_strategy.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart';
|
||||
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
|
||||
|
||||
class OccupancyDataLoadingStrategy implements AnalyticsDataLoadingStrategy {
|
||||
@override
|
||||
void onCommunitySelected(
|
||||
BuildContext context,
|
||||
CommunityModel community,
|
||||
List<SpaceModel> spaces,
|
||||
) {
|
||||
// Do Nothing
|
||||
}
|
||||
|
||||
@override
|
||||
void onSpaceSelected(
|
||||
BuildContext context,
|
||||
CommunityModel community,
|
||||
SpaceModel space,
|
||||
) {
|
||||
final spaceTreeBloc = context.read<SpaceTreeBloc>();
|
||||
final isSpaceSelected = spaceTreeBloc.state.selectedSpaces.contains(space.uuid);
|
||||
|
||||
final hasSelectedSpaces = spaceTreeBloc.state.selectedSpaces.isNotEmpty;
|
||||
if (hasSelectedSpaces) clearData(context);
|
||||
|
||||
if (isSpaceSelected) return;
|
||||
|
||||
spaceTreeBloc
|
||||
..add(const SpaceTreeClearSelectionEvent())
|
||||
..add(OnSpaceSelected(community, space.uuid ?? '', []));
|
||||
|
||||
FetchOccupancyDataHelper.loadOccupancyData(
|
||||
context,
|
||||
communityId: community.uuid,
|
||||
spaceId: space.uuid ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void clearData(BuildContext context) {
|
||||
context.read<SpaceTreeBloc>().add(
|
||||
const AnalyticsClearAllSpaceTreeSelectionsEvent(),
|
||||
);
|
||||
FetchOccupancyDataHelper.clearAllData(context);
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,11 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/device_location/device_location_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_tab/analytics_tab_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/analytics_communities_sidebar.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/analytics_page_tabs_and_children.dart';
|
||||
@ -8,19 +14,45 @@ import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/ener
|
||||
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/blocs/total_energy_consumption/total_energy_consumption_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/energy_consumption_by_phases/fake_energy_consumption_by_phases_service.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/energy_consumption_per_device/fake_energy_consumption_per_device_service.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy/occupancy_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy_heat_map/occupancy_heat_map_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/remote_air_quality_distribution_service.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/analytics_devices/analytics_devices_service_delagate.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/analytics_devices/remote_energy_management_analytics_devices_service.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/analytics_devices/remote_occupancy_analytics_devices_service.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/device_location/device_location_details_service_decorator.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/device_location/remote_device_location_service.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/energy_consumption_by_phases/remote_energy_consumption_by_phases_service.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/energy_consumption_per_device/remote_energy_consumption_per_device_service.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/occupacy/remote_occupancy_service.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/occupancy_heat_map/remote_occupancy_heat_map_service.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/power_clamp_info/remote_power_clamp_info_service.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/range_of_aqi/remote_range_of_aqi_service.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/realtime_device_service/firebase_realtime_device_service.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/total_energy_consumption/fake_total_energy_consumption_service.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/total_energy_consumption/remote_total_energy_consumption_service.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart';
|
||||
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart';
|
||||
import 'package:syncrow_web/services/api/http_service.dart';
|
||||
import 'package:syncrow_web/utils/theme/responsive_text_theme.dart';
|
||||
import 'package:syncrow_web/web_layout/web_scaffold.dart';
|
||||
|
||||
class AnalyticsPage extends StatelessWidget {
|
||||
class AnalyticsPage extends StatefulWidget {
|
||||
const AnalyticsPage({super.key});
|
||||
|
||||
@override
|
||||
State<AnalyticsPage> createState() => _AnalyticsPageState();
|
||||
}
|
||||
|
||||
class _AnalyticsPageState extends State<AnalyticsPage> {
|
||||
late final HTTPService _httpService;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_httpService = HTTPService();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
@ -30,22 +62,22 @@ class AnalyticsPage extends StatelessWidget {
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => TotalEnergyConsumptionBloc(
|
||||
FakeTotalEnergyConsumptionService(),
|
||||
RemoteTotalEnergyConsumptionService(_httpService),
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => EnergyConsumptionByPhasesBloc(
|
||||
FakeEnergyConsumptionByPhasesService(),
|
||||
RemoteEnergyConsumptionByPhasesService(_httpService),
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => EnergyConsumptionPerDeviceBloc(
|
||||
FakeEnergyConsumptionPerDeviceService(),
|
||||
RemoteEnergyConsumptionPerDeviceService(_httpService),
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => PowerClampInfoBloc(
|
||||
RemotePowerClampInfoService(HTTPService()),
|
||||
RemotePowerClampInfoService(_httpService),
|
||||
),
|
||||
),
|
||||
BlocProvider<RealtimeDeviceChangesBloc>(
|
||||
@ -53,15 +85,66 @@ class AnalyticsPage extends StatelessWidget {
|
||||
FirebaseRealtimeDeviceService(),
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => OccupancyBloc(
|
||||
RemoteOccupancyService(_httpService),
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => OccupancyHeatMapBloc(
|
||||
RemoteOccupancyHeatMapService(_httpService),
|
||||
),
|
||||
),
|
||||
BlocProvider(create: (context) => AnalyticsDatePickerBloc()),
|
||||
BlocProvider(
|
||||
create: (context) => AnalyticsDevicesBloc(
|
||||
AnalyticsDevicesServiceDelegate(
|
||||
RemoteOccupancyAnalyticsDevicesService(_httpService),
|
||||
RemoteEnergyManagementAnalyticsDevicesService(_httpService),
|
||||
),
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => RangeOfAqiBloc(
|
||||
RemoteRangeOfAqiService(_httpService),
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => AirQualityDistributionBloc(
|
||||
RemoteAirQualityDistributionService(_httpService),
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => DeviceLocationBloc(
|
||||
DeviceLocationDetailsServiceDecorator(
|
||||
RemoteDeviceLocationService(_httpService),
|
||||
Dio(
|
||||
BaseOptions(
|
||||
baseUrl: 'https://nominatim.openstreetmap.org/',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: const AnalyticsPageForm(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AnalyticsPageForm extends StatelessWidget {
|
||||
class AnalyticsPageForm extends StatefulWidget {
|
||||
const AnalyticsPageForm({super.key});
|
||||
|
||||
@override
|
||||
State<AnalyticsPageForm> createState() => _AnalyticsPageFormState();
|
||||
}
|
||||
|
||||
class _AnalyticsPageFormState extends State<AnalyticsPageForm> {
|
||||
@override
|
||||
void initState() {
|
||||
context.read<SpaceTreeBloc>().add(InitialEvent());
|
||||
super.initState();
|
||||
}
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WebScaffold(
|
||||
|
||||
@ -1,55 +1,29 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_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/helpers/fetch_energy_management_data_helper.dart';
|
||||
import 'package:syncrow_web/pages/space_tree/view/space_tree_view.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_tab/analytics_tab_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/strategies/analytics_data_loading_strategy_factory.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/sidebar/analytics_space_tree_view.dart';
|
||||
|
||||
class AnalyticsCommunitiesSidebar extends StatelessWidget {
|
||||
const AnalyticsCommunitiesSidebar({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Builder(
|
||||
builder: (context) {
|
||||
return Expanded(
|
||||
child: SpaceTreeView(
|
||||
title: const Text('Communities'),
|
||||
shouldDisableDeselectingChildrenOfSelectedParent: true,
|
||||
onSelect: () {
|
||||
/// Necessary to wait for the state to update before fethcing the data.
|
||||
Future.delayed(
|
||||
const Duration(milliseconds: 100),
|
||||
() {
|
||||
if (context.mounted) {
|
||||
FetchEnergyManagementDataHelper.fetchEnergyManagementData(
|
||||
context,
|
||||
);
|
||||
FetchEnergyManagementDataHelper.loadRealtimeDeviceChanges(
|
||||
context,
|
||||
);
|
||||
context.read<PowerClampInfoBloc>().add(
|
||||
const ClearPowerClampInfoEvent(),
|
||||
);
|
||||
final (selectedCommunities, selectedSpaces) =
|
||||
FetchEnergyManagementDataHelper
|
||||
.getSelectedCommunitiesAndSpaces(context);
|
||||
if (selectedCommunities.isEmpty && selectedSpaces.isEmpty) {
|
||||
context.read<PowerClampInfoBloc>().add(
|
||||
const ClearPowerClampInfoEvent(),
|
||||
);
|
||||
} else {
|
||||
FetchEnergyManagementDataHelper.loadPowerClampInfo(
|
||||
context,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
isSide: false,
|
||||
),
|
||||
);
|
||||
},
|
||||
final selectedTab = context.watch<AnalyticsTabBloc>().state;
|
||||
final strategy = AnalyticsDataLoadingStrategyFactory.getStrategy(selectedTab);
|
||||
|
||||
return Expanded(
|
||||
child: AnalyticsSpaceTreeView(
|
||||
onSelectCommunity: (community, spaces) {
|
||||
strategy.onCommunitySelected(context, community, spaces);
|
||||
},
|
||||
onSelectSpace: (community, space) {
|
||||
strategy.onSpaceSelected(context, community, space);
|
||||
},
|
||||
onSelectChildSpace: (community, child) {
|
||||
strategy.onSpaceSelected(context, community, child);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,17 +1,26 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:intl/intl.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/widgets/month_picker_widget.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/fetch_energy_management_data_helper.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/year_picker_widget.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
|
||||
class AnalyticsDateFilterButton extends StatefulWidget {
|
||||
const AnalyticsDateFilterButton({super.key});
|
||||
enum DatePickerType { month, year }
|
||||
|
||||
static final _color = ColorsManager.blackColor.withValues(alpha: 0.8);
|
||||
class AnalyticsDateFilterButton extends StatefulWidget {
|
||||
const AnalyticsDateFilterButton({
|
||||
required this.selectedDate,
|
||||
required this.onDateSelected,
|
||||
this.datePickerType = DatePickerType.month,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final DateTime selectedDate;
|
||||
final void Function(DateTime)? onDateSelected;
|
||||
final DatePickerType datePickerType;
|
||||
|
||||
static final Color _color = ColorsManager.blackColor.withValues(alpha: 0.8);
|
||||
|
||||
@override
|
||||
State<AnalyticsDateFilterButton> createState() =>
|
||||
@ -19,79 +28,67 @@ class AnalyticsDateFilterButton extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _AnalyticsDateFilterButtonState extends State<AnalyticsDateFilterButton> {
|
||||
late final AnalyticsDatePickerBloc _analyticsDatePickerBloc;
|
||||
@override
|
||||
void initState() {
|
||||
_analyticsDatePickerBloc = AnalyticsDatePickerBloc();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_analyticsDatePickerBloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _analyticsDatePickerBloc,
|
||||
child: Builder(builder: (context) {
|
||||
final selectedDate = context.watch<AnalyticsDatePickerBloc>().state;
|
||||
return TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: AnalyticsDateFilterButton._color,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: const BorderSide(
|
||||
color: ColorsManager.greyColor,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
backgroundColor: ColorsManager.transparentColor,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 32,
|
||||
vertical: 16,
|
||||
),
|
||||
return TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: AnalyticsDateFilterButton._color,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: const BorderSide(
|
||||
color: ColorsManager.greyColor,
|
||||
width: 1,
|
||||
),
|
||||
icon: SvgPicture.asset(
|
||||
Assets.blankCalendar,
|
||||
height: 20,
|
||||
width: 20,
|
||||
colorFilter:
|
||||
ColorFilter.mode(AnalyticsDateFilterButton._color, BlendMode.srcIn),
|
||||
),
|
||||
label: Text(
|
||||
_formatDate(selectedDate),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) => MonthPickerWidget(
|
||||
selectedDate: selectedDate,
|
||||
),
|
||||
backgroundColor: ColorsManager.transparentColor,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 32,
|
||||
vertical: 16,
|
||||
),
|
||||
),
|
||||
icon: SvgPicture.asset(
|
||||
Assets.blankCalendar,
|
||||
height: 20,
|
||||
width: 20,
|
||||
colorFilter:
|
||||
ColorFilter.mode(AnalyticsDateFilterButton._color, BlendMode.srcIn),
|
||||
),
|
||||
label: Text(
|
||||
_formatDate(widget.selectedDate),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => switch (widget.datePickerType) {
|
||||
DatePickerType.month => MonthPickerWidget(
|
||||
selectedDate: widget.selectedDate,
|
||||
onDateSelected: (value) {
|
||||
_analyticsDatePickerBloc.add(
|
||||
UpdateAnalyticsDatePickerEvent(value),
|
||||
);
|
||||
FetchEnergyManagementDataHelper.fetchEnergyManagementData(
|
||||
context,
|
||||
selectedDate: value,
|
||||
);
|
||||
widget.onDateSelected?.call(value);
|
||||
},
|
||||
),
|
||||
DatePickerType.year => YearPickerWidget(
|
||||
selectedDate: widget.selectedDate,
|
||||
onDateSelected: (value) {
|
||||
widget.onDateSelected?.call(value);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
String _formatDate(DateTime? date) {
|
||||
final formatter = DateFormat('MMMM yyyy');
|
||||
final formattedDate = formatter.format(date ?? DateTime.now());
|
||||
final formatterBasedOnDatePickerType = switch (widget.datePickerType) {
|
||||
DatePickerType.month => DateFormat('MMMM yyyy'),
|
||||
DatePickerType.year => DateFormat('yyyy'),
|
||||
};
|
||||
final formattedDate = formatterBasedOnDatePickerType.format(
|
||||
date ?? DateTime.now(),
|
||||
);
|
||||
return formattedDate;
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_tab/analytics_tab_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/enums/analytics_page_tab.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/strategies/analytics_data_loading_strategy_factory.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class AnalyticsPageTabButton extends StatelessWidget {
|
||||
@ -17,9 +18,12 @@ class AnalyticsPageTabButton extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextButton(
|
||||
onPressed: () => context.read<AnalyticsTabBloc>().add(
|
||||
onPressed: () {
|
||||
AnalyticsDataLoadingStrategyFactory.getStrategy(tab).clearData(context);
|
||||
context.read<AnalyticsTabBloc>().add(
|
||||
UpdateAnalyticsTabEvent(tab),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
tab.title,
|
||||
textAlign: TextAlign.center,
|
||||
|
||||
@ -1,9 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_tab/analytics_tab_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/enums/analytics_page_tab.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/analytics_date_filter_button.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/analytics_page_tab_button.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/fetch_energy_management_data_helper.dart';
|
||||
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
|
||||
import 'package:syncrow_web/utils/style.dart';
|
||||
|
||||
class AnalyticsPageTabsAndChildren extends StatelessWidget {
|
||||
@ -38,9 +42,7 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
...AnalyticsPageTab.values.map(
|
||||
(tab) => AnimatedSwitcher(
|
||||
switchInCurve: Curves.easeIn,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
(tab) => _buildAnimation(
|
||||
child: AnalyticsPageTabButton(
|
||||
key: ValueKey(selectedTab),
|
||||
tab: tab,
|
||||
@ -53,12 +55,25 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
const Expanded(
|
||||
flex: 2,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
child: AnalyticsDateFilterButton(),
|
||||
Visibility(
|
||||
key: ValueKey(selectedTab),
|
||||
visible: selectedTab == AnalyticsPageTab.energyManagement ||
|
||||
selectedTab == AnalyticsPageTab.airQuality,
|
||||
child: Expanded(
|
||||
flex: 2,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
child: AnalyticsDateFilterButton(
|
||||
onDateSelected: (value) {
|
||||
_onDateChanged(context, value, selectedTab);
|
||||
},
|
||||
selectedDate: context
|
||||
.watch<AnalyticsDatePickerBloc>()
|
||||
.state
|
||||
.monthlyDate,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -67,14 +82,89 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget {
|
||||
),
|
||||
Expanded(
|
||||
flex: 8,
|
||||
child: AnimatedSwitcher(
|
||||
switchInCurve: Curves.easeIn,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: selectedTab.child,
|
||||
),
|
||||
child: _buildAnimation(child: selectedTab.child),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAnimation({required Widget child}) {
|
||||
return AnimatedSwitcher(
|
||||
switchInCurve: Curves.easeIn,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
void _onDateChanged(
|
||||
BuildContext context,
|
||||
DateTime date,
|
||||
AnalyticsPageTab selectedTab,
|
||||
) {
|
||||
context.read<AnalyticsDatePickerBloc>().add(
|
||||
UpdateAnalyticsDatePickerEvent(montlyDate: date),
|
||||
);
|
||||
|
||||
final spaceTreeState = context.read<SpaceTreeBloc>().state;
|
||||
final communities = spaceTreeState.selectedCommunities;
|
||||
final spaces = spaceTreeState.selectedSpaces;
|
||||
if (spaceTreeState.selectedSpaces.isNotEmpty) {
|
||||
switch (selectedTab) {
|
||||
case AnalyticsPageTab.energyManagement:
|
||||
_onEnergyManagementDateChanged(
|
||||
context,
|
||||
date: date,
|
||||
communityUuid: communities.firstOrNull ?? '',
|
||||
spaceUuid: spaces.firstOrNull ?? '',
|
||||
);
|
||||
return;
|
||||
case AnalyticsPageTab.airQuality:
|
||||
_onAirQualityDateChanged(
|
||||
context,
|
||||
date: date,
|
||||
communityUuid: communities.firstOrNull ?? '',
|
||||
spaceUuid: spaces.firstOrNull ?? '',
|
||||
);
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _onEnergyManagementDateChanged(
|
||||
BuildContext context, {
|
||||
required DateTime date,
|
||||
required String communityUuid,
|
||||
required String spaceUuid,
|
||||
}) {
|
||||
context.read<AnalyticsDatePickerBloc>().add(
|
||||
UpdateAnalyticsDatePickerEvent(montlyDate: date),
|
||||
);
|
||||
|
||||
FetchEnergyManagementDataHelper.loadEnergyManagementData(
|
||||
context,
|
||||
shouldFetchAnalyticsDevices: false,
|
||||
selectedDate: date,
|
||||
communityId: communityUuid,
|
||||
spaceId: spaceUuid,
|
||||
);
|
||||
}
|
||||
|
||||
void _onAirQualityDateChanged(
|
||||
BuildContext context, {
|
||||
required DateTime date,
|
||||
required String communityUuid,
|
||||
required String spaceUuid,
|
||||
}) {
|
||||
if (spaceUuid.isEmpty) return;
|
||||
FetchAirQualityDataHelper.loadAirQualityData(
|
||||
context,
|
||||
date: date,
|
||||
communityUuid: communityUuid,
|
||||
spaceUuid: spaceUuid,
|
||||
shouldFetchAnalyticsDevices: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,59 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class ChartInformativeCell extends StatelessWidget {
|
||||
const ChartInformativeCell({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.color,
|
||||
this.hasBorder = false,
|
||||
});
|
||||
|
||||
final Widget title;
|
||||
final Color color;
|
||||
final bool hasBorder;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: MediaQuery.sizeOf(context).height * 0.0385,
|
||||
padding: const EdgeInsetsDirectional.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 12,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadiusDirectional.circular(8),
|
||||
border: Border.all(
|
||||
color: ColorsManager.greyColor,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.center,
|
||||
child: Row(
|
||||
spacing: 6,
|
||||
children: [
|
||||
Container(
|
||||
height: 8,
|
||||
width: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
border: Border.all(color: ColorsManager.grayBorder),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
DefaultTextStyle(
|
||||
style: const TextStyle(
|
||||
color: ColorsManager.blackColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 14,
|
||||
),
|
||||
child: title,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -45,7 +45,7 @@ class _MonthPickerWidgetState extends State<MonthPickerWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
backgroundColor: ColorsManager.whiteColors,
|
||||
child: Container(
|
||||
padding: const EdgeInsetsDirectional.all(20),
|
||||
width: 320,
|
||||
@ -121,6 +121,7 @@ class _MonthPickerWidgetState extends State<MonthPickerWidget> {
|
||||
}
|
||||
|
||||
Row _buildYearSelector() {
|
||||
final currentYear = DateTime.now().year;
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
@ -134,17 +135,35 @@ class _MonthPickerWidgetState extends State<MonthPickerWidget> {
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
onPressed: () => setState(() => _currentYear = _currentYear - 1),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_currentYear = _currentYear - 1;
|
||||
});
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.chevron_left,
|
||||
color: ColorsManager.grey700,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => setState(() => _currentYear = _currentYear + 1),
|
||||
icon: const Icon(
|
||||
onPressed: _currentYear < currentYear
|
||||
? () {
|
||||
setState(() {
|
||||
_currentYear = _currentYear + 1;
|
||||
// Clear selected month if it becomes invalid in the new year
|
||||
if (_currentYear == currentYear &&
|
||||
_selectedMonth != null &&
|
||||
_selectedMonth! > DateTime.now().month - 1) {
|
||||
_selectedMonth = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
: null,
|
||||
icon: Icon(
|
||||
Icons.chevron_right,
|
||||
color: ColorsManager.grey700,
|
||||
color: _currentYear < currentYear
|
||||
? ColorsManager.grey700
|
||||
: ColorsManager.grey700.withValues(alpha: 0.3),
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -152,11 +171,13 @@ class _MonthPickerWidgetState extends State<MonthPickerWidget> {
|
||||
}
|
||||
|
||||
Widget _buildMonthsGrid() {
|
||||
final currentDate = DateTime.now();
|
||||
final isCurrentYear = _currentYear == currentDate.year;
|
||||
|
||||
return GridView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: 12,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.all(8),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
childAspectRatio: 2.5,
|
||||
@ -165,25 +186,43 @@ class _MonthPickerWidgetState extends State<MonthPickerWidget> {
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final isSelected = _selectedMonth == index;
|
||||
final isFutureMonth = isCurrentYear && index > currentDate.month - 1;
|
||||
|
||||
return InkWell(
|
||||
onTap: () => setState(() => _selectedMonth = index),
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
onTap: isFutureMonth ? null : () => setState(() => _selectedMonth = index),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? ColorsManager.vividBlue.withValues(alpha: 0.7)
|
||||
: const Color(0xFFEDF2F7),
|
||||
borderRadius:
|
||||
isSelected ? BorderRadius.circular(15) : BorderRadius.zero,
|
||||
color: const Color(0xFFEDF2F7),
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: index % 3 == 0 ? const Radius.circular(16) : Radius.zero,
|
||||
bottomLeft: index % 3 == 0 ? const Radius.circular(16) : Radius.zero,
|
||||
topRight: index % 3 == 2 ? const Radius.circular(16) : Radius.zero,
|
||||
bottomRight:
|
||||
index % 3 == 2 ? const Radius.circular(16) : Radius.zero,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
_monthNames[index],
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
fontSize: 12,
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? ColorsManager.whiteColors
|
||||
: ColorsManager.blackColor.withValues(alpha: 0.8),
|
||||
fontWeight: FontWeight.w500,
|
||||
? ColorsManager.vividBlue.withValues(alpha: 0.7)
|
||||
: isFutureMonth
|
||||
? ColorsManager.grey700.withValues(alpha: 0.1)
|
||||
: const Color(0xFFEDF2F7),
|
||||
borderRadius:
|
||||
isSelected ? BorderRadius.circular(15) : BorderRadius.zero,
|
||||
),
|
||||
child: Text(
|
||||
_monthNames[index],
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
fontSize: 12,
|
||||
color: isSelected
|
||||
? ColorsManager.whiteColors
|
||||
: isFutureMonth
|
||||
? ColorsManager.blackColor.withValues(alpha: 0.3)
|
||||
: ColorsManager.blackColor.withValues(alpha: 0.8),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -0,0 +1,188 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/common/widgets/search_bar.dart';
|
||||
import 'package:syncrow_web/common/widgets/sidebar_communities_list.dart';
|
||||
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart';
|
||||
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_state.dart';
|
||||
import 'package:syncrow_web/pages/space_tree/view/custom_expansion.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class AnalyticsSpaceTreeView extends StatefulWidget {
|
||||
const AnalyticsSpaceTreeView({
|
||||
super.key,
|
||||
this.onSelectCommunity,
|
||||
this.onSelectSpace,
|
||||
this.onSelectChildSpace,
|
||||
});
|
||||
|
||||
final void Function(
|
||||
CommunityModel community,
|
||||
List<SpaceModel> spaces,
|
||||
)? onSelectCommunity;
|
||||
final void Function(
|
||||
CommunityModel community,
|
||||
SpaceModel space,
|
||||
)? onSelectSpace;
|
||||
final void Function(
|
||||
CommunityModel community,
|
||||
SpaceModel child,
|
||||
)? onSelectChildSpace;
|
||||
|
||||
@override
|
||||
State<AnalyticsSpaceTreeView> createState() => _AnalyticsSpaceTreeViewState();
|
||||
}
|
||||
|
||||
class _AnalyticsSpaceTreeViewState extends State<AnalyticsSpaceTreeView> {
|
||||
late final ScrollController _scrollController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_scrollController = ScrollController();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<SpaceTreeBloc, SpaceTreeState>(builder: (context, state) {
|
||||
final communities = state.searchQuery.isNotEmpty
|
||||
? state.filteredCommunity
|
||||
: state.communityList;
|
||||
return Container(
|
||||
height: MediaQuery.sizeOf(context).height,
|
||||
decoration: const BoxDecoration(color: ColorsManager.whiteColors),
|
||||
child: state is SpaceTreeLoadingState
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: Column(
|
||||
children: [
|
||||
Container(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: DefaultTextStyle(
|
||||
style: context.textTheme.titleMedium!.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
fontSize: 20,
|
||||
),
|
||||
child: const Text('Communities'),
|
||||
),
|
||||
),
|
||||
CustomSearchBar(
|
||||
onSearchChanged: (query) => context.read<SpaceTreeBloc>().add(
|
||||
SearchQueryEvent(query),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Expanded(
|
||||
child: state.isSearching
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: SidebarCommunitiesList(
|
||||
onScrollToEnd: () {
|
||||
if (!state.paginationIsLoading) {
|
||||
context.read<SpaceTreeBloc>().add(
|
||||
PaginationEvent(
|
||||
state.paginationModel,
|
||||
state.communityList,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
scrollController: _scrollController,
|
||||
communities: communities,
|
||||
itemBuilder: (context, index) {
|
||||
return CustomExpansionTileSpaceTree(
|
||||
title: communities[index].name,
|
||||
isSelected: state.selectedCommunities
|
||||
.contains(communities[index].uuid),
|
||||
isSoldCheck: state.selectedCommunities
|
||||
.contains(communities[index].uuid),
|
||||
onExpansionChanged: () =>
|
||||
context.read<SpaceTreeBloc>().add(
|
||||
OnCommunityExpanded(
|
||||
communities[index].uuid,
|
||||
),
|
||||
),
|
||||
isExpanded: state.expandedCommunities.contains(
|
||||
communities[index].uuid,
|
||||
),
|
||||
onItemSelected: () => widget.onSelectCommunity?.call(
|
||||
communities[index],
|
||||
communities[index].spaces,
|
||||
),
|
||||
children: communities[index].spaces.map(
|
||||
(space) {
|
||||
return CustomExpansionTileSpaceTree(
|
||||
title: space.name,
|
||||
isExpanded:
|
||||
state.expandedSpaces.contains(space.uuid),
|
||||
onItemSelected: () =>
|
||||
widget.onSelectSpace?.call(
|
||||
communities[index],
|
||||
space,
|
||||
),
|
||||
onExpansionChanged: () =>
|
||||
context.read<SpaceTreeBloc>().add(
|
||||
OnSpaceExpanded(
|
||||
communities[index].uuid,
|
||||
space.uuid ?? '',
|
||||
),
|
||||
),
|
||||
isSelected: state.selectedSpaces
|
||||
.contains(space.uuid) ||
|
||||
state.soldCheck.contains(space.uuid),
|
||||
isSoldCheck:
|
||||
state.soldCheck.contains(space.uuid),
|
||||
children: _buildNestedSpaces(
|
||||
context,
|
||||
state,
|
||||
space,
|
||||
communities[index],
|
||||
),
|
||||
);
|
||||
},
|
||||
).toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
if (state.paginationIsLoading) const CircularProgressIndicator(),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
List<Widget> _buildNestedSpaces(
|
||||
BuildContext context,
|
||||
SpaceTreeState state,
|
||||
SpaceModel space,
|
||||
CommunityModel community,
|
||||
) {
|
||||
return space.children.map((child) {
|
||||
return CustomExpansionTileSpaceTree(
|
||||
isSelected: state.selectedSpaces.contains(child.uuid) ||
|
||||
state.soldCheck.contains(child.uuid),
|
||||
isSoldCheck: state.soldCheck.contains(child.uuid),
|
||||
title: child.name,
|
||||
isExpanded: state.expandedSpaces.contains(child.uuid),
|
||||
onItemSelected: () {
|
||||
widget.onSelectChildSpace?.call(community, child);
|
||||
},
|
||||
onExpansionChanged: () {
|
||||
context.read<SpaceTreeBloc>().add(
|
||||
OnSpaceExpanded(community.uuid, child.uuid ?? ''),
|
||||
);
|
||||
},
|
||||
children: _buildNestedSpaces(context, state, child, community),
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
@ -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';
|
||||
|
||||
class YearPickerWidget extends StatefulWidget {
|
||||
const YearPickerWidget({
|
||||
super.key,
|
||||
required this.selectedDate,
|
||||
required this.onDateSelected,
|
||||
});
|
||||
|
||||
final DateTime selectedDate;
|
||||
final ValueChanged<DateTime>? onDateSelected;
|
||||
|
||||
@override
|
||||
State<YearPickerWidget> createState() => _YearPickerWidgetState();
|
||||
}
|
||||
|
||||
class _YearPickerWidgetState extends State<YearPickerWidget> {
|
||||
late int _currentYear;
|
||||
|
||||
static final years = List.generate(
|
||||
DateTime.now().year - (DateTime.now().year - 5) + 1,
|
||||
(index) => (2020 + index),
|
||||
).where((year) => year <= DateTime.now().year).toList();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_currentYear = widget.selectedDate.year;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
backgroundColor: ColorsManager.whiteColors,
|
||||
child: Container(
|
||||
padding: const EdgeInsetsDirectional.all(20),
|
||||
width: 320,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildMonthsGrid(),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
spacing: 12,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
style: FilledButton.styleFrom(
|
||||
fixedSize: const Size(106, 40),
|
||||
backgroundColor: const Color(0xFFEDF2F7),
|
||||
padding: const EdgeInsetsDirectional.symmetric(
|
||||
vertical: 10,
|
||||
horizontal: 16,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'Cancel',
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: ColorsManager.grey700,
|
||||
),
|
||||
),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
final date = DateTime(_currentYear);
|
||||
widget.onDateSelected?.call(date);
|
||||
},
|
||||
style: FilledButton.styleFrom(
|
||||
fixedSize: const Size(106, 40),
|
||||
backgroundColor: ColorsManager.vividBlue.withValues(
|
||||
alpha: 0.7,
|
||||
),
|
||||
padding: const EdgeInsetsDirectional.symmetric(
|
||||
vertical: 10,
|
||||
horizontal: 16,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'Done',
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: ColorsManager.whiteColors,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMonthsGrid() {
|
||||
return GridView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: years.length,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
childAspectRatio: 2.5,
|
||||
mainAxisSpacing: 8,
|
||||
mainAxisExtent: 30,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final isSelected = _currentYear == years[index];
|
||||
return InkWell(
|
||||
onTap: () => setState(() => _currentYear = years[index]),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFEDF2F7),
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: index % 3 == 0 ? const Radius.circular(16) : Radius.zero,
|
||||
bottomLeft: index % 3 == 0 ? const Radius.circular(16) : Radius.zero,
|
||||
topRight: index % 3 == 2 ? const Radius.circular(16) : Radius.zero,
|
||||
bottomRight:
|
||||
index % 3 == 2 ? const Radius.circular(16) : Radius.zero,
|
||||
),
|
||||
),
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? ColorsManager.vividBlue.withValues(alpha: 0.7)
|
||||
: const Color(0xFFEDF2F7),
|
||||
borderRadius:
|
||||
isSelected ? BorderRadius.circular(15) : BorderRadius.zero,
|
||||
),
|
||||
child: Text(
|
||||
years[index].toString(),
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
fontSize: 12,
|
||||
color: isSelected
|
||||
? ColorsManager.whiteColors
|
||||
: ColorsManager.blackColor.withValues(alpha: 0.8),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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/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/services/api/api_exception.dart';
|
||||
|
||||
part 'energy_consumption_by_phases_event.dart';
|
||||
part 'energy_consumption_by_phases_state.dart';
|
||||
@ -31,6 +32,13 @@ class EnergyConsumptionByPhasesBloc
|
||||
chartData: chartData,
|
||||
),
|
||||
);
|
||||
} on APIException catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: EnergyConsumptionByPhasesStatus.failure,
|
||||
errorMessage: e.message,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(
|
||||
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/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/services/api/api_exception.dart';
|
||||
|
||||
part 'energy_consumption_per_device_event.dart';
|
||||
part 'energy_consumption_per_device_state.dart';
|
||||
@ -13,7 +14,8 @@ class EnergyConsumptionPerDeviceBloc
|
||||
this._energyConsumptionPerDeviceService,
|
||||
) : super(const EnergyConsumptionPerDeviceState()) {
|
||||
on<LoadEnergyConsumptionPerDeviceEvent>(_onLoadEnergyConsumptionPerDeviceEvent);
|
||||
on<ClearEnergyConsumptionPerDeviceEvent>(_onClearEnergyConsumptionPerDeviceEvent);
|
||||
on<ClearEnergyConsumptionPerDeviceEvent>(
|
||||
_onClearEnergyConsumptionPerDeviceEvent);
|
||||
}
|
||||
|
||||
final EnergyConsumptionPerDeviceService _energyConsumptionPerDeviceService;
|
||||
@ -31,6 +33,13 @@ class EnergyConsumptionPerDeviceBloc
|
||||
chartData: chartData,
|
||||
),
|
||||
);
|
||||
} on APIException catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: EnergyConsumptionPerDeviceStatus.failure,
|
||||
errorMessage: e.message,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(
|
||||
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/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/services/api/api_exception.dart';
|
||||
|
||||
part 'power_clamp_info_event.dart';
|
||||
part 'power_clamp_info_state.dart';
|
||||
@ -31,6 +32,13 @@ class PowerClampInfoBloc extends Bloc<PowerClampInfoEvent, PowerClampInfoState>
|
||||
powerClampModel: powerClampModel,
|
||||
),
|
||||
);
|
||||
} on APIException catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: PowerClampInfoStatus.error,
|
||||
errorMessage: e.message,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(
|
||||
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/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/services/api/api_exception.dart';
|
||||
|
||||
part 'total_energy_consumption_event.dart';
|
||||
part 'total_energy_consumption_state.dart';
|
||||
@ -31,6 +32,13 @@ class TotalEnergyConsumptionBloc
|
||||
status: TotalEnergyConsumptionStatus.loaded,
|
||||
),
|
||||
);
|
||||
} on APIException catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
errorMessage: e.message,
|
||||
status: TotalEnergyConsumptionStatus.failure,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
|
||||
@ -1,20 +0,0 @@
|
||||
import 'package:syncrow_web/pages/analytics/models/phases_energy_consumption.dart';
|
||||
|
||||
abstract final class EnergyConsumptionByPhasesChartHelper {
|
||||
const EnergyConsumptionByPhasesChartHelper._();
|
||||
|
||||
static const fakeData = <PhasesEnergyConsumption>[
|
||||
PhasesEnergyConsumption(month: 1, phaseA: 200, phaseB: 300, phaseC: 400),
|
||||
PhasesEnergyConsumption(month: 2, phaseA: 300, phaseB: 400, phaseC: 500),
|
||||
PhasesEnergyConsumption(month: 3, phaseA: 400, phaseB: 500, phaseC: 600),
|
||||
PhasesEnergyConsumption(month: 4, phaseA: 100, phaseB: 100, phaseC: 100),
|
||||
PhasesEnergyConsumption(month: 5, phaseA: 300, phaseB: 400, phaseC: 500),
|
||||
PhasesEnergyConsumption(month: 6, phaseA: 300, phaseB: 100, phaseC: 400),
|
||||
PhasesEnergyConsumption(month: 7, phaseA: 300, phaseB: 100, phaseC: 400),
|
||||
PhasesEnergyConsumption(month: 8, phaseA: 500, phaseB: 100, phaseC: 100),
|
||||
PhasesEnergyConsumption(month: 9, phaseA: 500, phaseB: 100, phaseC: 200),
|
||||
PhasesEnergyConsumption(month: 10, phaseA: 100, phaseB: 50, phaseC: 50),
|
||||
PhasesEnergyConsumption(month: 11, phaseA: 600, phaseB: 750, phaseC: 130),
|
||||
PhasesEnergyConsumption(month: 12, phaseA: 100, phaseB: 80, phaseC: 100),
|
||||
];
|
||||
}
|
||||
@ -21,12 +21,13 @@ abstract final class EnergyManagementChartsHelper {
|
||||
reservedSize: 32,
|
||||
showTitles: true,
|
||||
maxIncluded: true,
|
||||
minIncluded: true,
|
||||
getTitlesWidget: (value, meta) => Padding(
|
||||
padding: const EdgeInsetsDirectional.only(top: 20.0),
|
||||
child: Text(
|
||||
value.toString(),
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.greyColor,
|
||||
color: ColorsManager.lightGreyColor,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
@ -36,7 +37,8 @@ abstract final class EnergyManagementChartsHelper {
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
maxIncluded: true,
|
||||
maxIncluded: false,
|
||||
minIncluded: false,
|
||||
interval: leftTitlesInterval,
|
||||
reservedSize: 110,
|
||||
getTitlesWidget: (value, meta) => Padding(
|
||||
@ -48,7 +50,7 @@ abstract final class EnergyManagementChartsHelper {
|
||||
value.formatNumberToKwh,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
fontSize: 12,
|
||||
color: ColorsManager.greyColor,
|
||||
color: ColorsManager.lightGreyColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -91,31 +93,40 @@ abstract final class EnergyManagementChartsHelper {
|
||||
);
|
||||
}
|
||||
|
||||
static FlBorderData borderData() {
|
||||
return FlBorderData(
|
||||
show: true,
|
||||
border: const Border.symmetric(
|
||||
horizontal: BorderSide(
|
||||
color: ColorsManager.greyColor,
|
||||
style: BorderStyle.solid,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static FlGridData gridData() {
|
||||
return const FlGridData(
|
||||
static FlGridData gridData({
|
||||
double horizontalInterval = 250,
|
||||
}) {
|
||||
return FlGridData(
|
||||
show: true,
|
||||
drawVerticalLine: false,
|
||||
drawHorizontalLine: true,
|
||||
horizontalInterval: horizontalInterval,
|
||||
getDrawingHorizontalLine: (value) {
|
||||
return FlLine(
|
||||
color: ColorsManager.greyColor,
|
||||
strokeWidth: 1,
|
||||
dashArray: value == 0 ? null : [5, 5],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static FlBorderData borderData() {
|
||||
return FlBorderData(
|
||||
border: const Border(
|
||||
bottom: BorderSide(
|
||||
color: ColorsManager.greyColor,
|
||||
style: BorderStyle.solid,
|
||||
),
|
||||
),
|
||||
show: true,
|
||||
);
|
||||
}
|
||||
|
||||
static LineTouchData lineTouchData() {
|
||||
return LineTouchData(
|
||||
handleBuiltInTouches: true,
|
||||
touchSpotThreshold: 2,
|
||||
touchSpotThreshold: 16,
|
||||
touchTooltipData: EnergyManagementChartsHelper.lineTouchTooltipData(),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,52 +1,77 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/analytics_device.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/energy_consumption_by_phases/energy_consumption_by_phases_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/energy_consumption_per_device/energy_consumption_per_device_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/blocs/total_energy_consumption/total_energy_consumption_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.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_per_device_param.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_total_energy_consumption_param.dart';
|
||||
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
|
||||
|
||||
abstract final class FetchEnergyManagementDataHelper {
|
||||
const FetchEnergyManagementDataHelper._();
|
||||
|
||||
static void fetchEnergyManagementData(
|
||||
BuildContext context, {
|
||||
DateTime? selectedDate,
|
||||
}) {
|
||||
final (selectedCommunities, selectedSpaces) =
|
||||
getSelectedCommunitiesAndSpaces(context);
|
||||
static AnalyticsDevice? getSelectedDevice(BuildContext context) {
|
||||
return context.read<AnalyticsDevicesBloc>().state.selectedDevice;
|
||||
}
|
||||
|
||||
if (selectedCommunities.isEmpty && selectedSpaces.isEmpty) {
|
||||
static void loadEnergyManagementData(
|
||||
BuildContext context, {
|
||||
required String communityId,
|
||||
required String spaceId,
|
||||
DateTime? selectedDate,
|
||||
bool shouldFetchAnalyticsDevices = true,
|
||||
}) {
|
||||
if (communityId.isEmpty && spaceId.isEmpty) {
|
||||
clearAllData(context);
|
||||
return;
|
||||
}
|
||||
|
||||
loadTotalEnergyConsumption(context);
|
||||
loadEnergyConsumptionByPhases(context);
|
||||
loadEnergyConsumptionPerDevice(context);
|
||||
return;
|
||||
}
|
||||
|
||||
static (List<String> selectedCommunities, List<String> selectedSpaces)
|
||||
getSelectedCommunitiesAndSpaces(BuildContext context) {
|
||||
final spaceTreeState = context.read<SpaceTreeBloc>().state;
|
||||
final selectedCommunities = spaceTreeState.selectedCommunities;
|
||||
final selectedSpaces = spaceTreeState.selectedSpaces;
|
||||
|
||||
return (selectedCommunities, selectedSpaces);
|
||||
final datePickerState = context.read<AnalyticsDatePickerBloc>().state;
|
||||
final selectedDate0 = selectedDate ?? datePickerState.monthlyDate;
|
||||
if (shouldFetchAnalyticsDevices) {
|
||||
loadAnalyticsDevices(
|
||||
context,
|
||||
communityUuid: communityId,
|
||||
spaceUuid: spaceId,
|
||||
selectedDate: selectedDate0,
|
||||
);
|
||||
loadRealtimeDeviceChanges(context);
|
||||
loadPowerClampInfo(context);
|
||||
}
|
||||
loadTotalEnergyConsumption(
|
||||
context,
|
||||
selectedDate: selectedDate0,
|
||||
spaceId: spaceId,
|
||||
);
|
||||
final selectedDevice = getSelectedDevice(context);
|
||||
if (selectedDevice case final AnalyticsDevice device) {
|
||||
loadEnergyConsumptionByPhases(
|
||||
context,
|
||||
powerClampUuid: device.uuid,
|
||||
selectedDate: selectedDate0,
|
||||
);
|
||||
}
|
||||
loadEnergyConsumptionPerDevice(
|
||||
context,
|
||||
spaceId: spaceId,
|
||||
selectedDate: selectedDate0,
|
||||
);
|
||||
}
|
||||
|
||||
static void loadEnergyConsumptionByPhases(
|
||||
BuildContext context, {
|
||||
required String powerClampUuid,
|
||||
DateTime? selectedDate,
|
||||
}) {
|
||||
final param = GetEnergyConsumptionByPhasesParam(
|
||||
startDate: selectedDate,
|
||||
spaceId: '',
|
||||
date: selectedDate,
|
||||
powerClampUuid: powerClampUuid,
|
||||
);
|
||||
context.read<EnergyConsumptionByPhasesBloc>().add(
|
||||
LoadEnergyConsumptionByPhasesEvent(param: param),
|
||||
@ -56,46 +81,90 @@ abstract final class FetchEnergyManagementDataHelper {
|
||||
static void loadTotalEnergyConsumption(
|
||||
BuildContext context, {
|
||||
DateTime? selectedDate,
|
||||
required String spaceId,
|
||||
}) {
|
||||
final (selectedCommunities, selectedSpaces) =
|
||||
getSelectedCommunitiesAndSpaces(context);
|
||||
|
||||
final param = GetTotalEnergyConsumptionParam(
|
||||
spaceId: selectedCommunities.firstOrNull,
|
||||
startDate: selectedDate,
|
||||
spaceId: spaceId,
|
||||
monthDate: selectedDate,
|
||||
);
|
||||
context.read<TotalEnergyConsumptionBloc>().add(
|
||||
TotalEnergyConsumptionLoadEvent(param: param),
|
||||
);
|
||||
}
|
||||
|
||||
static void loadEnergyConsumptionPerDevice(BuildContext context) {
|
||||
const param = GetEnergyConsumptionPerDeviceParam();
|
||||
static void loadEnergyConsumptionPerDevice(
|
||||
BuildContext context, {
|
||||
DateTime? selectedDate,
|
||||
required String spaceId,
|
||||
}) {
|
||||
final param = GetEnergyConsumptionPerDeviceParam(
|
||||
spaceId: spaceId,
|
||||
monthDate: selectedDate,
|
||||
);
|
||||
context.read<EnergyConsumptionPerDeviceBloc>().add(
|
||||
const LoadEnergyConsumptionPerDeviceEvent(param),
|
||||
LoadEnergyConsumptionPerDeviceEvent(param),
|
||||
);
|
||||
}
|
||||
|
||||
static void loadPowerClampInfo(BuildContext context) {
|
||||
context.read<PowerClampInfoBloc>().add(
|
||||
const LoadPowerClampInfoEvent('cb71d6ad-6e29-4eaa-ae3e-1a0d1c5f60fa'),
|
||||
final selectedDevice = getSelectedDevice(context);
|
||||
if (selectedDevice case final AnalyticsDevice device) {
|
||||
context.read<PowerClampInfoBloc>().add(
|
||||
LoadPowerClampInfoEvent(device.uuid),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static void loadRealtimeDeviceChanges(
|
||||
BuildContext context, {
|
||||
String? deviceUuid,
|
||||
}) {
|
||||
final selectedDevice = getSelectedDevice(context);
|
||||
|
||||
context.read<RealtimeDeviceChangesBloc>().add(
|
||||
RealtimeDeviceChangesStarted(deviceUuid ?? selectedDevice?.uuid ?? ''),
|
||||
);
|
||||
}
|
||||
|
||||
static void loadRealtimeDeviceChanges(BuildContext context) {
|
||||
context.read<RealtimeDeviceChangesBloc>().add(
|
||||
const RealtimeDeviceChangesStarted('cb71d6ad-6e29-4eaa-ae3e-1a0d1c5f60fa'),
|
||||
static void loadAnalyticsDevices(
|
||||
BuildContext context, {
|
||||
required String communityUuid,
|
||||
required String spaceUuid,
|
||||
required DateTime selectedDate,
|
||||
}) {
|
||||
context.read<AnalyticsDevicesBloc>().add(
|
||||
LoadAnalyticsDevicesEvent(
|
||||
onSuccess: (device) {
|
||||
context.read<PowerClampInfoBloc>().add(
|
||||
LoadPowerClampInfoEvent(device.uuid),
|
||||
);
|
||||
loadEnergyConsumptionByPhases(
|
||||
context,
|
||||
powerClampUuid: device.uuid,
|
||||
selectedDate: selectedDate,
|
||||
);
|
||||
context.read<RealtimeDeviceChangesBloc>().add(
|
||||
RealtimeDeviceChangesStarted(device.uuid),
|
||||
);
|
||||
},
|
||||
param: GetAnalyticsDevicesParam(
|
||||
communityUuid: communityUuid,
|
||||
spaceUuid: spaceUuid,
|
||||
deviceTypes: ['PC'],
|
||||
requestType: AnalyticsDeviceRequestType.energyManagement,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static void clearAllData(BuildContext context) {
|
||||
context.read<PowerClampInfoBloc>().add(
|
||||
const ClearPowerClampInfoEvent(),
|
||||
);
|
||||
context.read<RealtimeDeviceChangesBloc>().add(
|
||||
const RealtimeDeviceChangesClosed(),
|
||||
);
|
||||
|
||||
context.read<PowerClampInfoBloc>().add(
|
||||
const ClearPowerClampInfoEvent(),
|
||||
);
|
||||
context.read<EnergyConsumptionPerDeviceBloc>().add(
|
||||
const ClearEnergyConsumptionPerDeviceEvent(),
|
||||
);
|
||||
@ -107,5 +176,6 @@ abstract final class FetchEnergyManagementDataHelper {
|
||||
context.read<EnergyConsumptionByPhasesBloc>().add(
|
||||
const ClearEnergyConsumptionByPhasesEvent(),
|
||||
);
|
||||
context.read<AnalyticsDevicesBloc>().add(const ClearAnalyticsDeviceEvent());
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,7 +7,6 @@ class AnalyticsEnergyManagementView extends StatelessWidget {
|
||||
const AnalyticsEnergyManagementView({super.key});
|
||||
|
||||
static const _padding = EdgeInsetsDirectional.all(32);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
|
||||
@ -0,0 +1,114 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/analytics_device.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart';
|
||||
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,
|
||||
this.showSpaceUuid = false,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final ValueChanged<AnalyticsDevice> onChanged;
|
||||
final bool showSpaceUuid;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<AnalyticsDevicesBloc, AnalyticsDevicesState>(
|
||||
builder: (context, state) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: ColorsManager.greyColor,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Visibility(
|
||||
visible: state.devices.isNotEmpty,
|
||||
replacement: _buildNoDevicesFound(context),
|
||||
child: _buildDevicesDropdown(context, state),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static const _defaultPadding = EdgeInsetsDirectional.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 2,
|
||||
);
|
||||
|
||||
Widget _buildNoDevicesFound(BuildContext context) {
|
||||
return Padding(
|
||||
padding: _defaultPadding,
|
||||
child: Text(
|
||||
'no devices found',
|
||||
style: _getTextStyle(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDevicesDropdown(BuildContext context, AnalyticsDevicesState state) {
|
||||
final spaceUuid = state.selectedDevice?.spaceUuid;
|
||||
return DropdownButton<AnalyticsDevice?>(
|
||||
value: state.selectedDevice,
|
||||
isDense: true,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
dropdownColor: ColorsManager.whiteColors,
|
||||
underline: const SizedBox.shrink(),
|
||||
icon: const RotatedBox(
|
||||
quarterTurns: 1,
|
||||
child: Icon(Icons.chevron_right, size: 16),
|
||||
),
|
||||
style: _getTextStyle(context),
|
||||
padding: _defaultPadding,
|
||||
selectedItemBuilder: (context) {
|
||||
return state.devices.map((e) => Text(e.name)).toList();
|
||||
},
|
||||
items: state.devices.map((e) {
|
||||
return DropdownMenuItem(
|
||||
value: e,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(e.name),
|
||||
if (showSpaceUuid)
|
||||
if (spaceUuid != null)
|
||||
FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Text(
|
||||
spaceUuid,
|
||||
style: _getTextStyle(context)?.copyWith(
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
if (value case final AnalyticsDevice device) {
|
||||
context.read<AnalyticsDevicesBloc>().add(
|
||||
SelectAnalyticsDeviceEvent(device),
|
||||
);
|
||||
onChanged.call(device);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
TextStyle? _getTextStyle(BuildContext context) {
|
||||
return context.textTheme.labelSmall?.copyWith(
|
||||
color: ColorsManager.textPrimaryColor,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 14,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/analytics/helpers/get_month_name_from_int.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/phases_energy_consumption.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
@ -18,7 +18,11 @@ class EnergyConsumptionByPhasesChart extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return BarChart(
|
||||
BarChartData(
|
||||
gridData: EnergyManagementChartsHelper.gridData(),
|
||||
|
||||
gridData: EnergyManagementChartsHelper.gridData().copyWith(
|
||||
checkToShowHorizontalLine: (value) => true,
|
||||
horizontalInterval: 250,
|
||||
),
|
||||
borderData: EnergyManagementChartsHelper.borderData(),
|
||||
barTouchData: _barTouchData(context),
|
||||
titlesData: _titlesData(context),
|
||||
@ -31,25 +35,29 @@ class EnergyConsumptionByPhasesChart extends StatelessWidget {
|
||||
barRods: [
|
||||
BarChartRodData(
|
||||
color: ColorsManager.vividBlue.withValues(alpha: 0.1),
|
||||
toY: data.phaseA + data.phaseB + data.phaseC,
|
||||
toY: data.energyConsumedA +
|
||||
data.energyConsumedB +
|
||||
data.energyConsumedC,
|
||||
rodStackItems: [
|
||||
BarChartRodStackItem(
|
||||
0,
|
||||
data.phaseA,
|
||||
data.energyConsumedA,
|
||||
ColorsManager.vividBlue.withValues(alpha: 0.8),
|
||||
),
|
||||
BarChartRodStackItem(
|
||||
data.phaseA,
|
||||
data.phaseA + data.phaseB,
|
||||
data.energyConsumedA,
|
||||
data.energyConsumedA + data.energyConsumedB,
|
||||
ColorsManager.vividBlue.withValues(alpha: 0.4),
|
||||
),
|
||||
BarChartRodStackItem(
|
||||
data.phaseA + data.phaseB,
|
||||
data.phaseA + data.phaseB + data.phaseC,
|
||||
data.energyConsumedA + data.energyConsumedB,
|
||||
data.energyConsumedA +
|
||||
data.energyConsumedB +
|
||||
data.energyConsumedC,
|
||||
ColorsManager.vividBlue.withValues(alpha: 0.15),
|
||||
),
|
||||
],
|
||||
width: 16,
|
||||
width: 8,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(8),
|
||||
topRight: Radius.circular(8),
|
||||
@ -59,6 +67,7 @@ class EnergyConsumptionByPhasesChart extends StatelessWidget {
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
duration: Duration.zero,
|
||||
);
|
||||
}
|
||||
|
||||
@ -91,18 +100,27 @@ class EnergyConsumptionByPhasesChart extends StatelessWidget {
|
||||
}) {
|
||||
final data = energyData;
|
||||
|
||||
final month = data[group.x.toInt()].month.getMonthName;
|
||||
final phaseA = data[group.x.toInt()].phaseA;
|
||||
final phaseB = data[group.x.toInt()].phaseB;
|
||||
final phaseC = data[group.x.toInt()].phaseC;
|
||||
final date = DateFormat('dd/MM/yyyy').format(data[group.x.toInt()].date);
|
||||
final phaseA = data[group.x.toInt()].energyConsumedA;
|
||||
final phaseB = data[group.x.toInt()].energyConsumedB;
|
||||
final phaseC = data[group.x.toInt()].energyConsumedC;
|
||||
final total = data[group.x.toInt()].energyConsumedKw;
|
||||
|
||||
return BarTooltipItem(
|
||||
'$month\n',
|
||||
'$date\n',
|
||||
context.textTheme.bodyMedium!.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
fontSize: 14,
|
||||
),
|
||||
textAlign: TextAlign.start,
|
||||
children: [
|
||||
TextSpan(
|
||||
text: 'Total: $total\n',
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: 'Phase A: $phaseA\n',
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
@ -144,23 +162,23 @@ class EnergyConsumptionByPhasesChart extends StatelessWidget {
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
getTitlesWidget: (value, _) {
|
||||
final month = energyData[value.toInt()].month.getMonthName;
|
||||
final month = DateFormat('d').format(energyData[value.toInt()].date);
|
||||
return FittedBox(
|
||||
alignment: AlignmentDirectional.bottomCenter,
|
||||
alignment: AlignmentDirectional.center,
|
||||
fit: BoxFit.scaleDown,
|
||||
child: RotatedBox(
|
||||
quarterTurns: 3,
|
||||
child: Text(
|
||||
month,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.greyColor,
|
||||
color: ColorsManager.lightGreyColor,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
reservedSize: 36,
|
||||
reservedSize: 18,
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@ -19,10 +19,12 @@ class EnergyConsumptionByPhasesChartBox extends StatelessWidget {
|
||||
decoration: secondarySection,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 20,
|
||||
children: [
|
||||
AnalyticsErrorWidget(state.errorMessage),
|
||||
EnergyConsumptionByPhasesTitle(isLoading: state.status == EnergyConsumptionByPhasesStatus.loading,),
|
||||
EnergyConsumptionByPhasesTitle(
|
||||
isLoading: state.status == EnergyConsumptionByPhasesStatus.loading,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Expanded(
|
||||
child: EnergyConsumptionByPhasesChart(
|
||||
energyData: state.chartData,
|
||||
|
||||
@ -12,11 +12,16 @@ class EnergyConsumptionPerDeviceChart extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return LineChart(
|
||||
LineChartData(
|
||||
clipData: const FlClipData.vertical(),
|
||||
titlesData: EnergyManagementChartsHelper.titlesData(
|
||||
context,
|
||||
leftTitlesInterval: 250,
|
||||
),
|
||||
gridData: EnergyManagementChartsHelper.gridData(),
|
||||
|
||||
gridData: EnergyManagementChartsHelper.gridData().copyWith(
|
||||
checkToShowHorizontalLine: (value) => true,
|
||||
horizontalInterval: 250,
|
||||
),
|
||||
borderData: EnergyManagementChartsHelper.borderData(),
|
||||
lineTouchData: EnergyManagementChartsHelper.lineTouchData(),
|
||||
lineBarsData: chartData.map((e) {
|
||||
@ -33,7 +38,7 @@ class EnergyConsumptionPerDeviceChart extends StatelessWidget {
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
duration: Durations.extralong1,
|
||||
duration: Duration.zero,
|
||||
curve: Curves.easeIn,
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
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/blocs/energy_consumption_per_device/energy_consumption_per_device_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart.dart';
|
||||
@ -22,7 +23,6 @@ class EnergyConsumptionPerDeviceChartBox extends StatelessWidget {
|
||||
),
|
||||
padding: const EdgeInsets.all(30),
|
||||
child: Column(
|
||||
spacing: 20,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AnalyticsErrorWidget(state.errorMessage),
|
||||
@ -46,11 +46,14 @@ class EnergyConsumptionPerDeviceChartBox extends StatelessWidget {
|
||||
flex: 2,
|
||||
child: EnergyConsumptionPerDeviceDevicesList(
|
||||
chartData: state.chartData,
|
||||
devices: context.watch<AnalyticsDevicesBloc>().state.devices,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Divider(height: 0),
|
||||
const SizedBox(height: 20),
|
||||
Expanded(
|
||||
child: EnergyConsumptionPerDeviceChart(chartData: state.chartData),
|
||||
),
|
||||
|
||||
@ -1,10 +1,16 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/analytics_device.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/device_energy_data_model.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart';
|
||||
|
||||
class EnergyConsumptionPerDeviceDevicesList extends StatelessWidget {
|
||||
const EnergyConsumptionPerDeviceDevicesList({required this.chartData, super.key});
|
||||
const EnergyConsumptionPerDeviceDevicesList({
|
||||
required this.chartData,
|
||||
required this.devices,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final List<AnalyticsDevice> devices;
|
||||
final List<DeviceEnergyDataModel> chartData;
|
||||
|
||||
@override
|
||||
@ -16,47 +22,27 @@ class EnergyConsumptionPerDeviceDevicesList extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: chartData.map((e) => _buildDeviceCell(context, e)).toList(),
|
||||
children: devices.map((e) => _buildDeviceCell(context, e)).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDeviceCell(BuildContext context, DeviceEnergyDataModel device) {
|
||||
return Container(
|
||||
height: MediaQuery.sizeOf(context).height * 0.0365,
|
||||
padding: const EdgeInsetsDirectional.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 12,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadiusDirectional.circular(8),
|
||||
border: Border.all(
|
||||
color: ColorsManager.greyColor,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.center,
|
||||
child: Row(
|
||||
spacing: 6,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 4,
|
||||
backgroundColor: device.color,
|
||||
),
|
||||
Text(
|
||||
device.deviceName,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
color: ColorsManager.blackColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Widget _buildDeviceCell(BuildContext context, AnalyticsDevice device) {
|
||||
final deviceColor = chartData
|
||||
.firstWhere(
|
||||
(element) => element.deviceId == device.uuid,
|
||||
orElse: () => const DeviceEnergyDataModel(
|
||||
energy: [],
|
||||
deviceName: '',
|
||||
deviceId: '',
|
||||
color: Colors.red,
|
||||
),
|
||||
)
|
||||
.color;
|
||||
|
||||
return Tooltip(
|
||||
message: '${device.name}\n${device.spaceUuid ?? ''}',
|
||||
child: ChartInformativeCell(title: Text(device.name), color: deviceColor),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,36 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class PowerClampEnergyDataDeviceDropdown extends StatelessWidget {
|
||||
const PowerClampEnergyDataDeviceDropdown({super.key});
|
||||
|
||||
static final _color = ColorsManager.blackColor.withValues(alpha: 0.8);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: _color,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: const BorderSide(
|
||||
color: ColorsManager.greyColor,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
backgroundColor: ColorsManager.transparentColor,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 32,
|
||||
vertical: 16,
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'Device 1',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
onPressed: () {},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,17 +1,17 @@
|
||||
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/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/energy_consumption_by_phases_chart_box.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/power_clamp_energy_data_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/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 {
|
||||
@ -39,25 +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',
|
||||
showSpaceUuidInDevicesDropdown: true,
|
||||
onChanged: (device) {
|
||||
FetchEnergyManagementDataHelper.loadEnergyConsumptionByPhases(
|
||||
context,
|
||||
powerClampUuid: device.uuid,
|
||||
selectedDate:
|
||||
context.read<AnalyticsDatePickerBloc>().state.monthlyDate,
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
SelectableText(
|
||||
state.powerClampModel?.productUuid ?? 'N/A',
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: PowerClampEnergyStatusWidget(
|
||||
@ -65,13 +58,18 @@ class PowerClampEnergyDataWidget extends StatelessWidget {
|
||||
PowerClampEnergyStatus(
|
||||
iconPath: Assets.powerActiveIcon,
|
||||
title: 'Active',
|
||||
value: _valueFromCode('EnergyConsumed', generalDataPoints),
|
||||
value: _valueFromCode('ActivePower', generalDataPoints),
|
||||
unit: 'W',
|
||||
),
|
||||
PowerClampEnergyStatus(
|
||||
iconPath: Assets.voltMeterIcon,
|
||||
title: 'Current',
|
||||
value: _valueFromCode('Current', generalDataPoints),
|
||||
value: _valueFromCode('Current', generalDataPoints)
|
||||
.replaceAllMapped(
|
||||
RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
|
||||
(Match m) => '${m[1]}.',
|
||||
)
|
||||
.replaceAll('.0', ''),
|
||||
unit: 'A',
|
||||
),
|
||||
PowerClampEnergyStatus(
|
||||
@ -102,37 +100,6 @@ class PowerClampEnergyDataWidget extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
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(),
|
||||
const Expanded(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
child: PowerClampEnergyDataDeviceDropdown(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String _valueFromCode(String code, List<DataPoint> points) {
|
||||
return points
|
||||
.firstWhere((e) => e.code == code, orElse: () => DataPoint(value: '--'))
|
||||
|
||||
@ -48,6 +48,9 @@ class PowerClampEnergyStatusWidget extends StatelessWidget {
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 16,
|
||||
),
|
||||
softWrap: true,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
trailing: Text.rich(
|
||||
TextSpan(
|
||||
|
||||
@ -55,7 +55,7 @@ class PowerClampPhasesDataWidget extends StatelessWidget {
|
||||
iconPath: Assets.powerActiveIcon,
|
||||
title: 'Active Power',
|
||||
value: _valueFromCode(
|
||||
code: 'ReactivePower$phaseSuffix',
|
||||
code: 'ActivePower$phaseSuffix',
|
||||
points: phase?.dataPoints,
|
||||
),
|
||||
unit: 'W',
|
||||
@ -125,7 +125,48 @@ class PowerClampPhasesDataWidget extends StatelessWidget {
|
||||
(e) => e.code == code,
|
||||
orElse: () => DataPoint(value: '--'),
|
||||
);
|
||||
final value = element?.value;
|
||||
if (code.contains('Current')) {
|
||||
return _formatCurrentValue(value?.toString());
|
||||
}
|
||||
if (code.contains('PowerFactor')) {
|
||||
return _formatPowerFactor(value?.toString());
|
||||
}
|
||||
if (code.contains('Voltage')) {
|
||||
return _formatVoltage(value?.toString());
|
||||
}
|
||||
return value?.toString() ?? '--';
|
||||
}
|
||||
|
||||
return element?.value.toString() ?? '--';
|
||||
String _formatCurrentValue(String? value) {
|
||||
if (value == null) return '--';
|
||||
String str = value;
|
||||
if (str.isEmpty || str == '--') return '--';
|
||||
str = str.replaceAll(RegExp(r'[^0-9]'), '');
|
||||
if (str.isEmpty) return '--';
|
||||
if (str.length == 1) return '${str[0]}.0';
|
||||
return '${str[0]}.${str.substring(1)}';
|
||||
}
|
||||
|
||||
String _formatPowerFactor(String? value) {
|
||||
if (value == null) return '--';
|
||||
String str = value;
|
||||
if (str.isEmpty || str == '--') return '--';
|
||||
str = str.replaceAll(RegExp(r'[^0-9]'), '');
|
||||
if (str.isEmpty) return '--';
|
||||
final intValue = int.tryParse(str);
|
||||
if (intValue == null) return '--';
|
||||
final doubleValue = intValue / 100;
|
||||
return doubleValue.toStringAsFixed(2);
|
||||
}
|
||||
|
||||
String _formatVoltage(String? value) {
|
||||
if (value == null) return '--';
|
||||
String str = value;
|
||||
if (str.isEmpty || str == '--') return '--';
|
||||
str = str.replaceAll(RegExp(r'[^0-9]'), '');
|
||||
if (str.isEmpty) return '--';
|
||||
if (str.length == 1) return '0.${str[0]}';
|
||||
return '${str.substring(0, str.length - 1)}.${str.substring(str.length - 1)}';
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,15 +4,6 @@ import 'package:syncrow_web/pages/analytics/models/energy_data_model.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
// energy_consumption_chart will return id, name and consumption
|
||||
const phasesJson = {
|
||||
"1": {
|
||||
"phaseOne": 1000,
|
||||
"phaseTwo": 2000,
|
||||
"phaseThree": 3000,
|
||||
}
|
||||
};
|
||||
|
||||
class TotalEnergyConsumptionChart extends StatelessWidget {
|
||||
const TotalEnergyConsumptionChart({required this.chartData, super.key});
|
||||
|
||||
@ -23,17 +14,22 @@ class TotalEnergyConsumptionChart extends StatelessWidget {
|
||||
return Expanded(
|
||||
child: LineChart(
|
||||
LineChartData(
|
||||
clipData: const FlClipData.vertical(),
|
||||
titlesData: EnergyManagementChartsHelper.titlesData(
|
||||
context,
|
||||
leftTitlesInterval: 5000,
|
||||
leftTitlesInterval: 250,
|
||||
),
|
||||
gridData: EnergyManagementChartsHelper.gridData().copyWith(
|
||||
checkToShowHorizontalLine: (value) => true,
|
||||
horizontalInterval: 250,
|
||||
),
|
||||
gridData: EnergyManagementChartsHelper.gridData(),
|
||||
borderData: EnergyManagementChartsHelper.borderData(),
|
||||
lineTouchData: EnergyManagementChartsHelper.lineTouchData(),
|
||||
lineBarsData: _lineBarsData,
|
||||
),
|
||||
duration: Durations.extralong1,
|
||||
duration: Duration.zero,
|
||||
curve: Curves.easeIn,
|
||||
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -41,15 +37,12 @@ class TotalEnergyConsumptionChart extends StatelessWidget {
|
||||
List<LineChartBarData> get _lineBarsData {
|
||||
return [
|
||||
LineChartBarData(
|
||||
preventCurveOvershootingThreshold: 0.1,
|
||||
curveSmoothness: 0.55,
|
||||
preventCurveOverShooting: true,
|
||||
spots: chartData
|
||||
.asMap()
|
||||
.entries
|
||||
.map(
|
||||
(entry) => FlSpot(
|
||||
entry.key.toDouble(),
|
||||
entry.value.date.day.toDouble(),
|
||||
entry.value.value,
|
||||
),
|
||||
)
|
||||
|
||||
@ -19,7 +19,6 @@ class TotalEnergyConsumptionChartBox extends StatelessWidget {
|
||||
),
|
||||
padding: const EdgeInsets.all(30),
|
||||
child: Column(
|
||||
spacing: 20,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AnalyticsErrorWidget(state.errorMessage),
|
||||
@ -39,7 +38,9 @@ class TotalEnergyConsumptionChartBox extends StatelessWidget {
|
||||
const Spacer(flex: 4),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Divider(),
|
||||
const SizedBox(height: 20),
|
||||
TotalEnergyConsumptionChart(chartData: state.chartData),
|
||||
],
|
||||
),
|
||||
|
||||
@ -0,0 +1,40 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.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/services/occupacy/occupacy_service.dart';
|
||||
import 'package:syncrow_web/services/api/api_exception.dart';
|
||||
|
||||
part 'occupancy_event.dart';
|
||||
part 'occupancy_state.dart';
|
||||
|
||||
class OccupancyBloc extends Bloc<OccupancyEvent, OccupancyState> {
|
||||
OccupancyBloc(this._occupacyService) : super(const OccupancyState()) {
|
||||
on<LoadOccupancyEvent>(_onLoadOccupancyEvent);
|
||||
on<ClearOccupancyEvent>(_onClearOccupancyEvent);
|
||||
}
|
||||
|
||||
final OccupacyService _occupacyService;
|
||||
|
||||
Future<void> _onLoadOccupancyEvent(
|
||||
LoadOccupancyEvent event,
|
||||
Emitter<OccupancyState> emit,
|
||||
) async {
|
||||
emit(state.copyWith(status: OccupancyStatus.loading));
|
||||
try {
|
||||
final chartData = await _occupacyService.load(event.param);
|
||||
emit(state.copyWith(chartData: chartData, status: OccupancyStatus.loaded));
|
||||
} on APIException catch (e) {
|
||||
emit(state.copyWith(status: OccupancyStatus.failure, errorMessage: e.message));
|
||||
} catch (e) {
|
||||
emit(state.copyWith(status: OccupancyStatus.failure, errorMessage: '$e'));
|
||||
}
|
||||
}
|
||||
|
||||
void _onClearOccupancyEvent(
|
||||
ClearOccupancyEvent event,
|
||||
Emitter<OccupancyState> emit,
|
||||
) {
|
||||
emit(const OccupancyState());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
part of 'occupancy_bloc.dart';
|
||||
|
||||
sealed class OccupancyEvent extends Equatable {
|
||||
const OccupancyEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
final class LoadOccupancyEvent extends OccupancyEvent {
|
||||
const LoadOccupancyEvent(this.param);
|
||||
|
||||
final GetOccupancyParam param;
|
||||
|
||||
@override
|
||||
List<Object> get props => [param];
|
||||
}
|
||||
|
||||
final class ClearOccupancyEvent extends OccupancyEvent {
|
||||
const ClearOccupancyEvent();
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
part of 'occupancy_bloc.dart';
|
||||
|
||||
enum OccupancyStatus { initial, loading, loaded, failure }
|
||||
|
||||
final class OccupancyState extends Equatable {
|
||||
const OccupancyState({
|
||||
this.chartData = const [],
|
||||
this.status = OccupancyStatus.initial,
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
final List<Occupacy> chartData;
|
||||
final OccupancyStatus status;
|
||||
final String? errorMessage;
|
||||
|
||||
OccupancyState copyWith({
|
||||
List<Occupacy>? chartData,
|
||||
OccupancyStatus? status,
|
||||
String? errorMessage,
|
||||
}) {
|
||||
return OccupancyState(
|
||||
chartData: chartData ?? this.chartData,
|
||||
status: status ?? this.status,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [chartData, status, errorMessage];
|
||||
}
|
||||
@ -0,0 +1,57 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.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/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_state.dart';
|
||||
|
||||
class OccupancyHeatMapBloc
|
||||
extends Bloc<OccupancyHeatMapEvent, OccupancyHeatMapState> {
|
||||
OccupancyHeatMapBloc(
|
||||
this._occupancyHeatMapService,
|
||||
) : super(const OccupancyHeatMapState()) {
|
||||
on<LoadOccupancyHeatMapEvent>(_onLoadOccupancyHeatMapEvent);
|
||||
on<ClearOccupancyHeatMapEvent>(_onClearOccupancyHeatMapEvent);
|
||||
}
|
||||
final OccupancyHeatMapService _occupancyHeatMapService;
|
||||
|
||||
Future<void> _onLoadOccupancyHeatMapEvent(
|
||||
LoadOccupancyHeatMapEvent event,
|
||||
Emitter<OccupancyHeatMapState> emit,
|
||||
) async {
|
||||
emit(state.copyWith(status: OccupancyHeatMapStatus.loading));
|
||||
try {
|
||||
final occupancyHeatMap = await _occupancyHeatMapService.load(event.param);
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: OccupancyHeatMapStatus.loaded,
|
||||
heatMapData: occupancyHeatMap,
|
||||
),
|
||||
);
|
||||
} on APIException catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: OccupancyHeatMapStatus.failure,
|
||||
errorMessage: e.message,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: OccupancyHeatMapStatus.failure,
|
||||
errorMessage: e.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onClearOccupancyHeatMapEvent(
|
||||
ClearOccupancyHeatMapEvent event,
|
||||
Emitter<OccupancyHeatMapState> emit,
|
||||
) {
|
||||
emit(const OccupancyHeatMapState());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
part of 'occupancy_heat_map_bloc.dart';
|
||||
|
||||
sealed class OccupancyHeatMapEvent extends Equatable {
|
||||
const OccupancyHeatMapEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
final class LoadOccupancyHeatMapEvent extends OccupancyHeatMapEvent {
|
||||
const LoadOccupancyHeatMapEvent(this.param);
|
||||
|
||||
final GetOccupancyHeatMapParam param;
|
||||
|
||||
@override
|
||||
List<Object> get props => [param];
|
||||
}
|
||||
|
||||
final class ClearOccupancyHeatMapEvent extends OccupancyHeatMapEvent {
|
||||
const ClearOccupancyHeatMapEvent();
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
part of 'occupancy_heat_map_bloc.dart';
|
||||
|
||||
enum OccupancyHeatMapStatus { initial, loading, loaded, failure }
|
||||
|
||||
final class OccupancyHeatMapState extends Equatable {
|
||||
const OccupancyHeatMapState({
|
||||
this.status = OccupancyHeatMapStatus.initial,
|
||||
this.heatMapData = const [],
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
final OccupancyHeatMapStatus status;
|
||||
final String? errorMessage;
|
||||
final List<OccupancyHeatMapModel> heatMapData;
|
||||
|
||||
OccupancyHeatMapState copyWith({
|
||||
OccupancyHeatMapStatus? status,
|
||||
List<OccupancyHeatMapModel>? heatMapData,
|
||||
String? errorMessage,
|
||||
}) {
|
||||
return OccupancyHeatMapState(
|
||||
status: status ?? this.status,
|
||||
heatMapData: heatMapData ?? this.heatMapData,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [status, errorMessage, heatMapData];
|
||||
}
|
||||
@ -0,0 +1,111 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/analytics_device.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy/occupancy_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy_heat_map/occupancy_heat_map_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_occupancy_heat_map_param.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_occupancy_param.dart';
|
||||
|
||||
abstract final class FetchOccupancyDataHelper {
|
||||
const FetchOccupancyDataHelper._();
|
||||
|
||||
static void loadOccupancyData(
|
||||
BuildContext context, {
|
||||
required String communityId,
|
||||
required String spaceId,
|
||||
}) {
|
||||
if (communityId.isEmpty && spaceId.isEmpty) {
|
||||
clearAllData(context);
|
||||
return;
|
||||
}
|
||||
|
||||
final datePickerState = context.read<AnalyticsDatePickerBloc>().state;
|
||||
|
||||
loadAnalyticsDevices(context, communityUuid: communityId, spaceUuid: spaceId);
|
||||
final selectedDevice = context.read<AnalyticsDevicesBloc>().state.selectedDevice;
|
||||
|
||||
loadOccupancyChartData(
|
||||
context,
|
||||
spaceUuid: spaceId,
|
||||
date: datePickerState.monthlyDate,
|
||||
);
|
||||
loadHeatMapData(context, spaceUuid: spaceId, year: datePickerState.yearlyDate);
|
||||
|
||||
if (selectedDevice case final AnalyticsDevice device) {
|
||||
context.read<RealtimeDeviceChangesBloc>()
|
||||
..add(const RealtimeDeviceChangesClosed())
|
||||
..add(
|
||||
RealtimeDeviceChangesStarted(device.uuid),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static void loadHeatMapData(
|
||||
BuildContext context, {
|
||||
required String spaceUuid,
|
||||
required DateTime year,
|
||||
}) {
|
||||
context.read<OccupancyHeatMapBloc>().add(
|
||||
LoadOccupancyHeatMapEvent(
|
||||
GetOccupancyHeatMapParam(spaceUuid: spaceUuid, year: year),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static void loadOccupancyChartData(
|
||||
BuildContext context, {
|
||||
required String spaceUuid,
|
||||
required DateTime date,
|
||||
}) {
|
||||
context.read<OccupancyBloc>().add(
|
||||
LoadOccupancyEvent(
|
||||
GetOccupancyParam(
|
||||
monthDate: '${date.year}-${date.month.toString().padLeft(2, '0')}',
|
||||
spaceUuid: spaceUuid,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static void loadAnalyticsDevices(
|
||||
BuildContext context, {
|
||||
required String communityUuid,
|
||||
required String spaceUuid,
|
||||
}) {
|
||||
context.read<AnalyticsDevicesBloc>().add(
|
||||
LoadAnalyticsDevicesEvent(
|
||||
param: GetAnalyticsDevicesParam(
|
||||
communityUuid: communityUuid,
|
||||
spaceUuid: spaceUuid,
|
||||
deviceTypes: ['WPS', 'CPS'],
|
||||
requestType: AnalyticsDeviceRequestType.occupancy,
|
||||
),
|
||||
onSuccess: (device) {
|
||||
context.read<RealtimeDeviceChangesBloc>()
|
||||
..add(const RealtimeDeviceChangesClosed())
|
||||
..add(RealtimeDeviceChangesStarted(device.uuid));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static void clearAllData(BuildContext context) {
|
||||
context.read<OccupancyBloc>().add(
|
||||
const ClearOccupancyEvent(),
|
||||
);
|
||||
context.read<OccupancyHeatMapBloc>().add(
|
||||
const ClearOccupancyHeatMapEvent(),
|
||||
);
|
||||
context.read<RealtimeDeviceChangesBloc>().add(
|
||||
const RealtimeDeviceChangesClosed(),
|
||||
);
|
||||
|
||||
context.read<AnalyticsDevicesBloc>().add(
|
||||
const ClearAnalyticsDeviceEvent(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,12 +1,56 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_chart_box.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_end_side_bar.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_box.dart';
|
||||
|
||||
class AnalyticsOccupancyView extends StatelessWidget {
|
||||
const AnalyticsOccupancyView({super.key});
|
||||
|
||||
static const _padding = EdgeInsetsDirectional.all(32);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Center(
|
||||
child: Text('AnalyticsOccupancyView is Working!'),
|
||||
final height = MediaQuery.sizeOf(context).height;
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final isMediumOrLess = constraints.maxWidth <= 900;
|
||||
if (isMediumOrLess) {
|
||||
return SingleChildScrollView(
|
||||
padding: _padding,
|
||||
child: Column(
|
||||
spacing: 32,
|
||||
children: [
|
||||
SizedBox(height: height * 0.46, child: const OccupancyEndSideBar()),
|
||||
SizedBox(height: height * 0.5, child: const OccupancyChartBox()),
|
||||
SizedBox(height: height * 0.5, child: const OccupancyHeatMapBox()),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Container(
|
||||
padding: _padding,
|
||||
height: height * 0.9,
|
||||
child: const Row(
|
||||
spacing: 32,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 5,
|
||||
child: Column(
|
||||
spacing: 20,
|
||||
children: [
|
||||
Expanded(child: OccupancyChartBox()),
|
||||
Expanded(child: OccupancyHeatMapBox()),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(flex: 2, child: OccupancyEndSideBar()),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,54 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class HeatMapTooltip extends StatelessWidget {
|
||||
const HeatMapTooltip({
|
||||
required this.date,
|
||||
required this.value,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final DateTime date;
|
||||
final int value;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.topStart,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.grey700,
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
DateFormat('MMM d, yyyy').format(date),
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: ColorsManager.whiteColors,
|
||||
),
|
||||
),
|
||||
const Divider(height: 2, thickness: 1),
|
||||
Text(
|
||||
'$value Occupants',
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: ColorsManager.whiteColors,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,108 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/heat_map_tooltip.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_painter.dart';
|
||||
|
||||
class InteractiveHeatMap extends StatefulWidget {
|
||||
const InteractiveHeatMap({
|
||||
required this.items,
|
||||
required this.maxValue,
|
||||
required this.cellSize,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final List<OccupancyPaintItem> items;
|
||||
final int maxValue;
|
||||
final double cellSize;
|
||||
|
||||
@override
|
||||
State<InteractiveHeatMap> createState() => _InteractiveHeatMapState();
|
||||
}
|
||||
|
||||
class _InteractiveHeatMapState extends State<InteractiveHeatMap> {
|
||||
OccupancyPaintItem? _hoveredItem;
|
||||
OverlayEntry? _overlayEntry;
|
||||
final LayerLink _layerLink = LayerLink();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_removeOverlay();
|
||||
_overlayEntry?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _removeOverlay() {
|
||||
_overlayEntry?.remove();
|
||||
_overlayEntry = null;
|
||||
}
|
||||
|
||||
void _showTooltip(OccupancyPaintItem item, Offset localPosition) {
|
||||
_removeOverlay();
|
||||
|
||||
final column = item.index ~/ 7;
|
||||
final row = item.index % 7;
|
||||
final x = column * widget.cellSize;
|
||||
final y = row * widget.cellSize;
|
||||
|
||||
_overlayEntry = OverlayEntry(
|
||||
builder: (context) => Positioned(
|
||||
child: CompositedTransformFollower(
|
||||
link: _layerLink,
|
||||
offset: Offset(x + widget.cellSize, y),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Transform.translate(
|
||||
offset: Offset(-(widget.cellSize * 2.5), -50),
|
||||
child: HeatMapTooltip(date: item.date, value: item.value),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Overlay.of(context).insert(_overlayEntry!);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CompositedTransformTarget(
|
||||
link: _layerLink,
|
||||
child: MouseRegion(
|
||||
onHover: (event) {
|
||||
final column = event.localPosition.dx ~/ widget.cellSize;
|
||||
final row = event.localPosition.dy ~/ widget.cellSize;
|
||||
final index = column * 7 + row;
|
||||
|
||||
if (index >= 0 && index < widget.items.length) {
|
||||
final item = widget.items[index];
|
||||
if (_hoveredItem != item) {
|
||||
setState(() => _hoveredItem = item);
|
||||
_showTooltip(item, event.localPosition);
|
||||
}
|
||||
} else {
|
||||
_removeOverlay();
|
||||
setState(() => _hoveredItem = null);
|
||||
}
|
||||
},
|
||||
onExit: (_) {
|
||||
_removeOverlay();
|
||||
setState(() => _hoveredItem = null);
|
||||
},
|
||||
child: CustomPaint(
|
||||
isComplex: true,
|
||||
size: _painterSize,
|
||||
painter: OccupancyPainter(
|
||||
items: widget.items,
|
||||
maxValue: widget.maxValue,
|
||||
hoveredItem: _hoveredItem,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Size get _painterSize {
|
||||
final height = 7 * widget.cellSize;
|
||||
final width = widget.items.length ~/ 7 * widget.cellSize;
|
||||
return Size(width, height);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,153 @@
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/occupacy.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class OccupancyChart extends StatelessWidget {
|
||||
const OccupancyChart({required this.chartData, super.key});
|
||||
|
||||
final List<Occupacy> chartData;
|
||||
|
||||
static const _chartWidth = 16.0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BarChart(
|
||||
BarChartData(
|
||||
maxY: 100.001,
|
||||
gridData: EnergyManagementChartsHelper.gridData().copyWith(
|
||||
checkToShowHorizontalLine: (value) => true,
|
||||
horizontalInterval: 20,
|
||||
),
|
||||
borderData: EnergyManagementChartsHelper.borderData(),
|
||||
barTouchData: _barTouchData(context),
|
||||
titlesData: _titlesData(context).copyWith(
|
||||
leftTitles: _titlesData(context).leftTitles.copyWith(
|
||||
sideTitles: _titlesData(context).leftTitles.sideTitles.copyWith(
|
||||
maxIncluded: true,
|
||||
minIncluded: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
barGroups: List.generate(chartData.length, (index) {
|
||||
final actual = chartData[index];
|
||||
final occupancyValue = double.parse(actual.occupancy);
|
||||
return BarChartGroupData(
|
||||
x: index,
|
||||
barsSpace: 0,
|
||||
groupVertically: true,
|
||||
barRods: [
|
||||
BarChartRodData(
|
||||
toY: 100.0,
|
||||
fromY: occupancyValue == 0 ? occupancyValue : occupancyValue + 2.5,
|
||||
color: ColorsManager.graysColor,
|
||||
width: _chartWidth,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
BarChartRodData(
|
||||
toY: occupancyValue,
|
||||
color: ColorsManager.vividBlue.withValues(alpha: 0.8),
|
||||
width: _chartWidth,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
BarTouchData _barTouchData(BuildContext context) {
|
||||
return BarTouchData(
|
||||
touchTooltipData: BarTouchTooltipData(
|
||||
getTooltipColor: (touchTooltipItem) => ColorsManager.whiteColors,
|
||||
tooltipBorder: const BorderSide(
|
||||
color: ColorsManager.semiTransparentBlack,
|
||||
),
|
||||
tooltipRoundedRadius: 16,
|
||||
tooltipPadding: const EdgeInsets.all(8),
|
||||
getTooltipItem: (group, groupIndex, rod, rodIndex) => getTooltipItem(
|
||||
context: context,
|
||||
group: group,
|
||||
groupIndex: groupIndex,
|
||||
rod: rod,
|
||||
rodIndex: rodIndex,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
BarTooltipItem? getTooltipItem({
|
||||
required BuildContext context,
|
||||
required BarChartGroupData group,
|
||||
required int groupIndex,
|
||||
required BarChartRodData rod,
|
||||
required int rodIndex,
|
||||
}) {
|
||||
final data = chartData;
|
||||
|
||||
final occupancyValue = double.parse(data[group.x.toInt()].occupancy);
|
||||
final percentage = '${(occupancyValue).toStringAsFixed(0)}%';
|
||||
|
||||
return BarTooltipItem(
|
||||
percentage,
|
||||
context.textTheme.bodyMedium!.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
fontSize: 14,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
FlTitlesData _titlesData(BuildContext context) {
|
||||
final titlesData = EnergyManagementChartsHelper.titlesData(
|
||||
context,
|
||||
leftTitlesInterval: 250,
|
||||
);
|
||||
|
||||
final leftTitles = titlesData.leftTitles.copyWith(
|
||||
sideTitles: titlesData.leftTitles.sideTitles.copyWith(
|
||||
reservedSize: 70,
|
||||
interval: 20,
|
||||
getTitlesWidget: (value, meta) => Padding(
|
||||
padding: const EdgeInsetsDirectional.only(end: 12),
|
||||
child: FittedBox(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
'${(value).toStringAsFixed(0)}%',
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
fontSize: 12,
|
||||
color: ColorsManager.greyColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final bottomTitles = AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
getTitlesWidget: (value, _) => FittedBox(
|
||||
alignment: AlignmentDirectional.bottomCenter,
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
chartData[value.toInt()].date.day.toString(),
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.greyColor,
|
||||
fontSize: 8,
|
||||
),
|
||||
),
|
||||
),
|
||||
reservedSize: 36,
|
||||
),
|
||||
);
|
||||
|
||||
return titlesData.copyWith(
|
||||
leftTitles: leftTitles,
|
||||
bottomTitles: bottomTitles,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,77 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_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/widgets/analytics_date_filter_button.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy/occupancy_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart';
|
||||
import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart';
|
||||
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
|
||||
import 'package:syncrow_web/utils/style.dart';
|
||||
|
||||
class OccupancyChartBox extends StatelessWidget {
|
||||
const OccupancyChartBox({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final spaceTreeState = context.watch<SpaceTreeBloc>().state;
|
||||
return BlocBuilder<OccupancyBloc, OccupancyState>(
|
||||
builder: (context, state) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(30),
|
||||
decoration: containerWhiteDecoration,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AnalyticsErrorWidget(state.errorMessage),
|
||||
Row(
|
||||
children: [
|
||||
const Expanded(
|
||||
flex: 3,
|
||||
child: FittedBox(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
fit: BoxFit.scaleDown,
|
||||
child: ChartTitle(title: Text('Occupancy')),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Expanded(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
child: AnalyticsDateFilterButton(
|
||||
onDateSelected: (DateTime value) {
|
||||
context.read<AnalyticsDatePickerBloc>().add(
|
||||
UpdateAnalyticsDatePickerEvent(montlyDate: value),
|
||||
);
|
||||
if (spaceTreeState.selectedSpaces.isNotEmpty) {
|
||||
FetchOccupancyDataHelper.loadOccupancyChartData(
|
||||
context,
|
||||
spaceUuid:
|
||||
spaceTreeState.selectedSpaces.firstOrNull ?? '',
|
||||
date: value,
|
||||
);
|
||||
}
|
||||
},
|
||||
selectedDate: context
|
||||
.watch<AnalyticsDatePickerBloc>()
|
||||
.state
|
||||
.monthlyDate,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Divider(),
|
||||
const SizedBox(height: 20),
|
||||
Expanded(child: OccupancyChart(chartData: state.chartData)),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,79 @@
|
||||
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/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.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/constants/assets.dart';
|
||||
import 'package:syncrow_web/utils/style.dart';
|
||||
|
||||
class OccupancyEndSideBar extends StatelessWidget {
|
||||
const OccupancyEndSideBar({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<RealtimeDeviceChangesBloc, RealtimeDeviceChangesState>(
|
||||
builder: (context, state) {
|
||||
return Container(
|
||||
decoration: subSectionContainerDecoration.copyWith(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
padding: const EdgeInsetsDirectional.all(32),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const AnalyticsSidebarHeader(title: 'Presnce Sensor'),
|
||||
SizedBox(
|
||||
height: MediaQuery.sizeOf(context).height * 0.2,
|
||||
child: PowerClampEnergyStatusWidget(
|
||||
status: [
|
||||
PowerClampEnergyStatus(
|
||||
iconPath: Assets.presenceState,
|
||||
title: 'Presence Status',
|
||||
value: _valueFromCode(
|
||||
'presence_state',
|
||||
state.deviceStatusList,
|
||||
),
|
||||
unit: '',
|
||||
),
|
||||
PowerClampEnergyStatus(
|
||||
iconPath: Assets.presenceTimeIcon,
|
||||
title: 'Presence Time',
|
||||
value:
|
||||
'${_valueFromCode('none_body_time', state.deviceStatusList)} Min',
|
||||
unit: '',
|
||||
),
|
||||
PowerClampEnergyStatus(
|
||||
iconPath: Assets.currentDistanceIcon,
|
||||
title: 'Detection Distance',
|
||||
value:
|
||||
'${_valueFromCode('space_move_val', state.deviceStatusList)} M',
|
||||
unit: '',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
String _valueFromCode(
|
||||
String code,
|
||||
List<Status> status, {
|
||||
String? defaultValue,
|
||||
}) {
|
||||
final value = status
|
||||
.firstWhere(
|
||||
(e) => e.code == code,
|
||||
orElse: () => Status(code: '--', value: '--'),
|
||||
)
|
||||
.value
|
||||
.toString();
|
||||
return value == 'null' ? defaultValue ?? '--' : value;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,83 @@
|
||||
import 'dart:math' as math show max;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/interactive_heat_map.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_days.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_gradient.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_months.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_painter.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class OccupancyHeatMap extends StatelessWidget {
|
||||
const OccupancyHeatMap({required this.heatMapData, super.key});
|
||||
final Map<DateTime, int> heatMapData;
|
||||
|
||||
static const _cellSize = 16.0;
|
||||
static const _totalWeeks = 53;
|
||||
|
||||
int get _maxValue => heatMapData.isNotEmpty
|
||||
? heatMapData.keys.map((key) => heatMapData[key] ?? 0).reduce(math.max)
|
||||
: 0;
|
||||
|
||||
DateTime _getStartingDate() {
|
||||
final jan1 = DateTime(DateTime.now().year, 1, 1);
|
||||
final startOfWeek = jan1.subtract(Duration(days: jan1.weekday - 1));
|
||||
return startOfWeek;
|
||||
}
|
||||
|
||||
List<OccupancyPaintItem> _generatePaintItems(DateTime startDate) {
|
||||
return List.generate(_totalWeeks * 7, (index) {
|
||||
final date = startDate.add(Duration(days: index));
|
||||
final value = heatMapData[date] ?? 0;
|
||||
return OccupancyPaintItem(index: index, value: value, date: date);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final startDate = _getStartingDate();
|
||||
final paintItems = _generatePaintItems(startDate);
|
||||
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
OccupancyHeatMapMonths(startDate: startDate, cellSize: _cellSize),
|
||||
Container(
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(color: ColorsManager.grayBorder),
|
||||
top: BorderSide(color: ColorsManager.grayBorder),
|
||||
),
|
||||
),
|
||||
width: double.infinity,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.fill,
|
||||
child: Row(
|
||||
children: [
|
||||
const OccupancyHeatMapDays(cellSize: _cellSize),
|
||||
SizedBox(
|
||||
width: _totalWeeks * _cellSize,
|
||||
height: 7 * _cellSize,
|
||||
child: InteractiveHeatMap(
|
||||
items: paintItems,
|
||||
maxValue: _maxValue,
|
||||
cellSize: _cellSize,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
OccupancyHeatMapGradient(maxValue: _maxValue),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,87 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_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/widgets/analytics_date_filter_button.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy_heat_map/occupancy_heat_map_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart';
|
||||
import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart';
|
||||
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
|
||||
import 'package:syncrow_web/utils/style.dart';
|
||||
|
||||
class OccupancyHeatMapBox extends StatelessWidget {
|
||||
const OccupancyHeatMapBox({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final spaceTreeState = context.watch<SpaceTreeBloc>().state;
|
||||
return BlocBuilder<OccupancyHeatMapBloc, OccupancyHeatMapState>(
|
||||
builder: (context, state) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(30),
|
||||
decoration: containerWhiteDecoration,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AnalyticsErrorWidget(state.errorMessage),
|
||||
Row(
|
||||
children: [
|
||||
const Expanded(
|
||||
flex: 3,
|
||||
child: FittedBox(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
fit: BoxFit.scaleDown,
|
||||
child: ChartTitle(title: Text('Occupancy Heat Map')),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Expanded(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
child: AnalyticsDateFilterButton(
|
||||
onDateSelected: (DateTime value) {
|
||||
context.read<AnalyticsDatePickerBloc>().add(
|
||||
UpdateAnalyticsDatePickerEvent(yearlyDate: value),
|
||||
);
|
||||
if (spaceTreeState.selectedSpaces.isNotEmpty) {
|
||||
FetchOccupancyDataHelper.loadHeatMapData(
|
||||
context,
|
||||
spaceUuid:
|
||||
spaceTreeState.selectedSpaces.firstOrNull ?? '',
|
||||
year: value,
|
||||
);
|
||||
}
|
||||
},
|
||||
datePickerType: DatePickerType.year,
|
||||
selectedDate: context
|
||||
.watch<AnalyticsDatePickerBloc>()
|
||||
.state
|
||||
.yearlyDate,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Divider(),
|
||||
const SizedBox(height: 20),
|
||||
Expanded(
|
||||
child: OccupancyHeatMap(
|
||||
heatMapData: state.heatMapData.asMap().map(
|
||||
(_, value) => MapEntry(
|
||||
value.eventDate,
|
||||
value.countTotalPresenceDetected,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class OccupancyHeatMapDays extends StatelessWidget {
|
||||
const OccupancyHeatMapDays({
|
||||
required this.cellSize,
|
||||
this.textColor = ColorsManager.blackColor,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final double cellSize;
|
||||
final Color textColor;
|
||||
|
||||
static const _weekDayLabels = [
|
||||
'Monday',
|
||||
'Tuesday',
|
||||
'Wednesday',
|
||||
'Thursday',
|
||||
'Friday',
|
||||
'Saturday',
|
||||
'Sunday',
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: List.generate(7, (i) {
|
||||
final dayLabel = _weekDayLabels[i];
|
||||
return Container(
|
||||
height: cellSize,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
margin: const EdgeInsetsDirectional.all(0.5).add(
|
||||
const EdgeInsetsDirectional.only(end: 4),
|
||||
),
|
||||
padding: const EdgeInsets.only(right: 6),
|
||||
child: Text(
|
||||
dayLabel,
|
||||
textAlign: TextAlign.start,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: textColor,
|
||||
fontSize: 8,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class OccupancyHeatMapGradient extends StatelessWidget {
|
||||
const OccupancyHeatMapGradient({super.key, required this.maxValue});
|
||||
|
||||
final int maxValue;
|
||||
List<Color> _heatMapColors() {
|
||||
if (maxValue == 0) {
|
||||
return [
|
||||
ColorsManager.vividBlue.withValues(alpha: 0),
|
||||
ColorsManager.vividBlue.withValues(alpha: 0),
|
||||
];
|
||||
}
|
||||
return List.generate(
|
||||
maxValue + 1,
|
||||
(index) => ColorsManager.vividBlue.withValues(alpha: index / maxValue),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
const Spacer(),
|
||||
Tooltip(
|
||||
message: 'Min: 0 - Max: $maxValue',
|
||||
child: Container(
|
||||
width: 150,
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: ColorsManager.grayBorder,
|
||||
width: 1,
|
||||
),
|
||||
gradient: LinearGradient(
|
||||
begin: AlignmentDirectional.centerStart,
|
||||
end: AlignmentDirectional.centerEnd,
|
||||
colors: _heatMapColors(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user