mirror of
https://github.com/SyncrowIOT/web.git
synced 2025-07-14 17:25:50 +00:00
Compare commits
100 Commits
SP-1493-FE
...
SP-1519-FE
Author | SHA1 | Date | |
---|---|---|---|
e8f7c29652 | |||
c7fef11aec | |||
ef29d78d70 | |||
cd9941f544 | |||
71aa64ba9e | |||
2262d3b2ba | |||
b7ef9da35d | |||
49e93329c8 | |||
d6f0b53b59 | |||
7154693379 | |||
2e2bc99501 | |||
53222bee81 | |||
bfb9158652 | |||
7f03222c12 | |||
5e6c14efeb | |||
9bbf3e75fa | |||
303b0236f1 | |||
4e3e63723e | |||
38ff20f86a | |||
d539e6266e | |||
7467f8d0ea | |||
a11e20147e | |||
55a6974bdc | |||
f8f58a24b8 | |||
682e69e65f | |||
59a59231ec | |||
ad41a2a87e | |||
974aa8f2a4 | |||
428cd34492 | |||
1a6121c452 | |||
e8f9ae944c | |||
7e37aed026 | |||
d89e227599 | |||
5a68b22f0c | |||
38184ca8b2 | |||
4d5de7bc05 | |||
1a3006fa43 | |||
490ca2057e | |||
06637a16bb | |||
696978a78d | |||
818e4e4d51 | |||
af877d7839 | |||
a33b1e3f49 | |||
c3cce334ab | |||
947e9e404c | |||
cd8264b6ce | |||
7467be6980 | |||
0353c73dac | |||
a050792f32 | |||
464f7b7347 | |||
cd54574279 | |||
18acae3e85 | |||
f081a7fc2d | |||
5996ff3928 | |||
a0d1cb988a | |||
c3ec9000d4 | |||
3d6a60b406 | |||
69c9240641 | |||
098013e5c8 | |||
11fb9e4894 | |||
390da9213d | |||
cae8b029fe | |||
6b883c8bb3 | |||
08c99bcbcb | |||
f6448d3eff | |||
a657a9a25e | |||
f55fa25bdf | |||
7242218b2f | |||
e43de3f64c | |||
9c250986b2 | |||
d8faafd1c0 | |||
24c30ddcb5 | |||
bafd2b4d13 | |||
56f9b1fc9a | |||
a9cc92ff86 | |||
3c7edae88a | |||
56c2d11535 | |||
3aa5bff758 | |||
28d1e5a5a7 | |||
fe036a8190 | |||
82e145de9d | |||
ebeb514a5b | |||
6b7e02ee53 | |||
b01136b6e9 | |||
97f8c6c8c9 | |||
6e527503c1 | |||
d6ef06c1b3 | |||
c9aaf2580f | |||
d9cd5d0438 | |||
3eb87dfde1 | |||
f29ff2551f | |||
67dd59ee9c | |||
bb3c3906d1 | |||
3873deca90 | |||
9431dd4500 | |||
63718185e7 | |||
1f4e82d567 | |||
9f68d171ff | |||
6eba640037 | |||
7a088074e3 |
@ -26,6 +26,7 @@ linter:
|
|||||||
rules:
|
rules:
|
||||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||||
|
prefer_const_constructors: true
|
||||||
|
|
||||||
# Additional information about this file can be found at
|
# Additional information about this file can be found at
|
||||||
# https://dart.dev/guides/language/analysis-options
|
# https://dart.dev/guides/language/analysis-options
|
||||||
|
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;
|
||||||
|
}
|
6
lib/pages/analytics/helpers/format_number_to_kwh.dart
Normal file
6
lib/pages/analytics/helpers/format_number_to_kwh.dart
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
extension FormatNumberToKwh on num {
|
||||||
|
String get formatNumberToKwh {
|
||||||
|
final regExp = RegExp(r'(\d)(?=(\d{3})+$)');
|
||||||
|
return '${toStringAsFixed(0).replaceAllMapped(regExp, (match) => '${match[1]},')} kWh';
|
||||||
|
}
|
||||||
|
}
|
19
lib/pages/analytics/helpers/get_month_name_from_int.dart
Normal file
19
lib/pages/analytics/helpers/get_month_name_from_int.dart
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
extension GetMonthNameFromNumber on num {
|
||||||
|
String get getMonthName {
|
||||||
|
return switch (this) {
|
||||||
|
1 => 'JAN',
|
||||||
|
2 => 'FEB',
|
||||||
|
3 => 'MAR',
|
||||||
|
4 => 'APR',
|
||||||
|
5 => 'MAY',
|
||||||
|
6 => 'JUN',
|
||||||
|
7 => 'JUL',
|
||||||
|
8 => 'AUG',
|
||||||
|
9 => 'SEP',
|
||||||
|
10 => 'OCT',
|
||||||
|
11 => 'NOV',
|
||||||
|
12 => 'DEC',
|
||||||
|
_ => 'N/A'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
32
lib/pages/analytics/models/device_energy_data_model.dart
Normal file
32
lib/pages/analytics/models/device_energy_data_model.dart
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:syncrow_web/pages/analytics/models/energy_data_model.dart';
|
||||||
|
|
||||||
|
class DeviceEnergyDataModel extends Equatable {
|
||||||
|
const DeviceEnergyDataModel({
|
||||||
|
required this.energy,
|
||||||
|
required this.deviceName,
|
||||||
|
required this.deviceId,
|
||||||
|
required this.color,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<EnergyDataModel> energy;
|
||||||
|
final String deviceName;
|
||||||
|
final String deviceId;
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [energy, deviceName, deviceId];
|
||||||
|
|
||||||
|
factory DeviceEnergyDataModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
final energy = (json['energy'] as List<dynamic>? ?? [])
|
||||||
|
.map((e) => EnergyDataModel.fromJson(e))
|
||||||
|
.toList();
|
||||||
|
return DeviceEnergyDataModel(
|
||||||
|
energy: energy,
|
||||||
|
deviceName: json['device_name'] as String? ?? '',
|
||||||
|
deviceId: json['device_id'] as String? ?? '',
|
||||||
|
color: Color(int.parse(json['color'] as String? ?? '0xFF000000')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
21
lib/pages/analytics/models/energy_data_model.dart
Normal file
21
lib/pages/analytics/models/energy_data_model.dart
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
class EnergyDataModel extends Equatable {
|
||||||
|
const EnergyDataModel({
|
||||||
|
required this.date,
|
||||||
|
required this.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
final DateTime date;
|
||||||
|
final double value;
|
||||||
|
|
||||||
|
factory EnergyDataModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
return EnergyDataModel(
|
||||||
|
date: DateTime.parse(json['date'] as String),
|
||||||
|
value: (json['value'] as num).toDouble(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [date, value];
|
||||||
|
}
|
18
lib/pages/analytics/models/occupacy.dart
Normal file
18
lib/pages/analytics/models/occupacy.dart
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
class Occupacy extends Equatable {
|
||||||
|
final String date;
|
||||||
|
final String occupancy;
|
||||||
|
|
||||||
|
const Occupacy({required this.date, required this.occupancy});
|
||||||
|
|
||||||
|
factory Occupacy.fromJson(Map<String, dynamic> json) {
|
||||||
|
return Occupacy(
|
||||||
|
date: json['date'] as String,
|
||||||
|
occupancy: json['occupancy'] as String,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [date, occupancy];
|
||||||
|
}
|
22
lib/pages/analytics/models/occupancy_heat_map_model.dart
Normal file
22
lib/pages/analytics/models/occupancy_heat_map_model.dart
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
class OccupancyHeatMapModel extends Equatable {
|
||||||
|
final DateTime date;
|
||||||
|
|
||||||
|
final int occupancy;
|
||||||
|
|
||||||
|
const OccupancyHeatMapModel({
|
||||||
|
required this.date,
|
||||||
|
required this.occupancy,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory OccupancyHeatMapModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
return OccupancyHeatMapModel(
|
||||||
|
date: DateTime.parse(json['date'] as String),
|
||||||
|
occupancy: json['occupancy'] as int,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [date, occupancy];
|
||||||
|
}
|
27
lib/pages/analytics/models/phases_energy_consumption.dart
Normal file
27
lib/pages/analytics/models/phases_energy_consumption.dart
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
class PhasesEnergyConsumption extends Equatable {
|
||||||
|
final int month;
|
||||||
|
final double phaseA;
|
||||||
|
final double phaseB;
|
||||||
|
final double phaseC;
|
||||||
|
|
||||||
|
const PhasesEnergyConsumption({
|
||||||
|
required this.month,
|
||||||
|
required this.phaseA,
|
||||||
|
required this.phaseB,
|
||||||
|
required this.phaseC,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [month, phaseA, phaseB, phaseC];
|
||||||
|
|
||||||
|
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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
13
lib/pages/analytics/models/power_clamp_energy_status.dart
Normal file
13
lib/pages/analytics/models/power_clamp_energy_status.dart
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
class PowerClampEnergyStatus {
|
||||||
|
final String iconPath;
|
||||||
|
final String title;
|
||||||
|
final String value;
|
||||||
|
final String unit;
|
||||||
|
|
||||||
|
const PowerClampEnergyStatus({
|
||||||
|
required this.iconPath,
|
||||||
|
required this.title,
|
||||||
|
required this.value,
|
||||||
|
required this.unit,
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
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, AnalyticsDatePickerState> {
|
||||||
|
AnalyticsDatePickerBloc() : super(AnalyticsDatePickerState()) {
|
||||||
|
on<UpdateAnalyticsDatePickerEvent>(_onUpdateAnalyticsDatePickerEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onUpdateAnalyticsDatePickerEvent(
|
||||||
|
UpdateAnalyticsDatePickerEvent event,
|
||||||
|
Emitter<AnalyticsDatePickerState> emit,
|
||||||
|
) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
monthlyDate: event.montlyDate ?? state.monthlyDate,
|
||||||
|
yearlyDate: event.yearlyDate ?? state.yearlyDate,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
part of 'analytics_date_picker_bloc.dart';
|
||||||
|
|
||||||
|
sealed class AnalyticsDatePickerEvent extends Equatable {
|
||||||
|
const AnalyticsDatePickerEvent();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [];
|
||||||
|
}
|
||||||
|
|
||||||
|
final class UpdateAnalyticsDatePickerEvent extends AnalyticsDatePickerEvent {
|
||||||
|
const UpdateAnalyticsDatePickerEvent({this.montlyDate, this.yearlyDate});
|
||||||
|
|
||||||
|
final DateTime? montlyDate;
|
||||||
|
final DateTime? yearlyDate;
|
||||||
|
|
||||||
|
@override
|
||||||
|
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];
|
||||||
|
}
|
@ -1,13 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/views/analytics_energy_management_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';
|
import 'package:syncrow_web/pages/analytics/modules/occupancy/views/analytics_occupancy_view.dart';
|
||||||
import 'package:syncrow_web/pages/analytics/modules/overview/views/analytics_overview_view.dart';
|
|
||||||
|
|
||||||
enum AnalyticsPageTab {
|
enum AnalyticsPageTab {
|
||||||
overview(
|
|
||||||
title: 'Overview',
|
|
||||||
child: AnalyticsOverviewView(),
|
|
||||||
),
|
|
||||||
energyManagement(
|
energyManagement(
|
||||||
title: 'Energy Management',
|
title: 'Energy Management',
|
||||||
child: AnalyticsEnergyManagementView(),
|
child: AnalyticsEnergyManagementView(),
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
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 onChildSpaceSelected(
|
||||||
|
BuildContext context,
|
||||||
|
CommunityModel community,
|
||||||
|
SpaceModel child,
|
||||||
|
);
|
||||||
|
void clearData(BuildContext context);
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
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.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(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,65 @@
|
|||||||
|
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,
|
||||||
|
) {
|
||||||
|
context.read<SpaceTreeBloc>().add(
|
||||||
|
OnCommunitySelected(
|
||||||
|
community.uuid,
|
||||||
|
spaces,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
FetchEnergyManagementDataHelper.loadEnergyManagementData(
|
||||||
|
context,
|
||||||
|
communityId: community.uuid,
|
||||||
|
spaceId: spaces.isNotEmpty ? spaces.first.uuid ?? '' : '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onSpaceSelected(
|
||||||
|
BuildContext context,
|
||||||
|
CommunityModel community,
|
||||||
|
SpaceModel space,
|
||||||
|
) {
|
||||||
|
context.read<SpaceTreeBloc>().add(
|
||||||
|
OnSpaceSelected(
|
||||||
|
community,
|
||||||
|
space.uuid ?? '',
|
||||||
|
space.children,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
FetchEnergyManagementDataHelper.loadEnergyManagementData(
|
||||||
|
context,
|
||||||
|
communityId: community.uuid,
|
||||||
|
spaceId: space.uuid ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onChildSpaceSelected(
|
||||||
|
BuildContext context,
|
||||||
|
CommunityModel community,
|
||||||
|
SpaceModel child,
|
||||||
|
) {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void clearData(BuildContext context) {
|
||||||
|
context.read<SpaceTreeBloc>().add(const SpaceTreeClearSelectionEvent());
|
||||||
|
FetchEnergyManagementDataHelper.clearAllData(context);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,71 @@
|
|||||||
|
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,
|
||||||
|
) {
|
||||||
|
context.read<SpaceTreeBloc>().add(
|
||||||
|
OnCommunitySelected(
|
||||||
|
community.uuid,
|
||||||
|
spaces.isNotEmpty ? [spaces.first] : [],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
FetchOccupancyDataHelper.loadOccupancyData(
|
||||||
|
context,
|
||||||
|
communityId: community.uuid,
|
||||||
|
spaceId: spaces.isNotEmpty ? spaces.first.uuid ?? '' : '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onSpaceSelected(
|
||||||
|
BuildContext context,
|
||||||
|
CommunityModel community,
|
||||||
|
SpaceModel space,
|
||||||
|
) {
|
||||||
|
final spaceTreeBloc = context.read<SpaceTreeBloc>();
|
||||||
|
final selectedSpacesIds = spaceTreeBloc.state.selectedSpaces;
|
||||||
|
final isSpaceSelected = selectedSpacesIds.contains(space.uuid);
|
||||||
|
|
||||||
|
if (selectedSpacesIds.isEmpty) {
|
||||||
|
spaceTreeBloc.add(OnCommunitySelected(community.uuid, [space]));
|
||||||
|
} else if (isSpaceSelected) {
|
||||||
|
spaceTreeBloc.add(const SpaceTreeClearSelectionEvent());
|
||||||
|
} else {
|
||||||
|
spaceTreeBloc
|
||||||
|
..add(const SpaceTreeClearSelectionEvent())
|
||||||
|
..add(OnSpaceSelected(community, space.uuid ?? '', []));
|
||||||
|
}
|
||||||
|
|
||||||
|
FetchOccupancyDataHelper.loadOccupancyData(
|
||||||
|
context,
|
||||||
|
communityId: community.uuid,
|
||||||
|
spaceId: space.uuid ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onChildSpaceSelected(
|
||||||
|
BuildContext context,
|
||||||
|
CommunityModel community,
|
||||||
|
SpaceModel child,
|
||||||
|
) {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void clearData(BuildContext context) {
|
||||||
|
context.read<SpaceTreeBloc>().add(const SpaceTreeClearSelectionEvent());
|
||||||
|
// FetchOccupancyDataHelper.clearAllData(context);
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,25 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/bloc/analytics_tab_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_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_communities_sidebar.dart';
|
||||||
import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/analytics_page_tabs_and_children.dart';
|
import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/analytics_page_tabs_and_children.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/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/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/services/occupacy/fake_occupacy_service.dart';
|
||||||
|
import 'package:syncrow_web/pages/analytics/services/occupancy_heat_map/fake_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/realtime_device_service/firebase_realtime_device_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/device_managment/shared/navigate_home_grid_view.dart';
|
||||||
|
import 'package:syncrow_web/services/api/http_service.dart';
|
||||||
import 'package:syncrow_web/utils/theme/responsive_text_theme.dart';
|
import 'package:syncrow_web/utils/theme/responsive_text_theme.dart';
|
||||||
import 'package:syncrow_web/web_layout/web_scaffold.dart';
|
import 'package:syncrow_web/web_layout/web_scaffold.dart';
|
||||||
|
|
||||||
@ -12,8 +28,42 @@ class AnalyticsPage extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider<AnalyticsTabBloc>(
|
return MultiBlocProvider(
|
||||||
create: (context) => AnalyticsTabBloc(),
|
providers: [
|
||||||
|
BlocProvider<AnalyticsTabBloc>(
|
||||||
|
create: (context) => AnalyticsTabBloc(),
|
||||||
|
),
|
||||||
|
BlocProvider(
|
||||||
|
create: (context) => TotalEnergyConsumptionBloc(
|
||||||
|
RemoteTotalEnergyConsumptionService(HTTPService()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
BlocProvider(
|
||||||
|
create: (context) => EnergyConsumptionByPhasesBloc(
|
||||||
|
FakeEnergyConsumptionByPhasesService(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
BlocProvider(
|
||||||
|
create: (context) => EnergyConsumptionPerDeviceBloc(
|
||||||
|
FakeEnergyConsumptionPerDeviceService(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
BlocProvider(
|
||||||
|
create: (context) => PowerClampInfoBloc(
|
||||||
|
RemotePowerClampInfoService(HTTPService()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
BlocProvider<RealtimeDeviceChangesBloc>(
|
||||||
|
create: (context) => RealtimeDeviceChangesBloc(
|
||||||
|
FirebaseRealtimeDeviceService(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
BlocProvider(create: (context) => OccupancyBloc(FakeOccupacyService())),
|
||||||
|
BlocProvider(
|
||||||
|
create: (context) => OccupancyHeatMapBloc(FakeOccupancyHeatMapService()),
|
||||||
|
),
|
||||||
|
BlocProvider(create: (context) => AnalyticsDatePickerBloc()),
|
||||||
|
],
|
||||||
child: const AnalyticsPageForm(),
|
child: const AnalyticsPageForm(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,28 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:syncrow_web/pages/space_tree/view/space_tree_view.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/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 {
|
class AnalyticsCommunitiesSidebar extends StatelessWidget {
|
||||||
const AnalyticsCommunitiesSidebar({super.key});
|
const AnalyticsCommunitiesSidebar({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final selectedTab = context.watch<AnalyticsTabBloc>().state;
|
||||||
|
final strategy = AnalyticsDataLoadingStrategyFactory.getStrategy(selectedTab);
|
||||||
|
|
||||||
return Expanded(
|
return Expanded(
|
||||||
child: SpaceTreeView(
|
child: AnalyticsSpaceTreeView(
|
||||||
title: const Text('Communities'),
|
onSelectCommunity: (community, spaces) {
|
||||||
shouldDisableDeselectingChildrenOfSelectedParent: true,
|
strategy.onCommunitySelected(context, community, spaces);
|
||||||
onSelect: () {},
|
},
|
||||||
isSide: false,
|
onSelectSpace: (community, space) {
|
||||||
|
strategy.onSpaceSelected(context, community, space);
|
||||||
|
},
|
||||||
|
onSelectChildSpace: (community, child) {
|
||||||
|
strategy.onChildSpaceSelected(context, community, child);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,38 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_svg/svg.dart';
|
import 'package:flutter_svg/svg.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/month_picker_widget.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/color_manager.dart';
|
||||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||||
|
|
||||||
class AnalyticsDateFilterButton extends StatelessWidget {
|
enum DatePickerType { month, year }
|
||||||
const AnalyticsDateFilterButton({super.key});
|
|
||||||
|
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 = ColorsManager.blackColor.withValues(alpha: 0.8);
|
static final _color = ColorsManager.blackColor.withValues(alpha: 0.8);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AnalyticsDateFilterButton> createState() =>
|
||||||
|
_AnalyticsDateFilterButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AnalyticsDateFilterButtonState extends State<AnalyticsDateFilterButton> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return TextButton.icon(
|
return TextButton.icon(
|
||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
foregroundColor: _color,
|
foregroundColor: AnalyticsDateFilterButton._color,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
side: const BorderSide(
|
side: const BorderSide(
|
||||||
@ -31,27 +50,47 @@ class AnalyticsDateFilterButton extends StatelessWidget {
|
|||||||
Assets.blankCalendar,
|
Assets.blankCalendar,
|
||||||
height: 20,
|
height: 20,
|
||||||
width: 20,
|
width: 20,
|
||||||
colorFilter: ColorFilter.mode(_color, BlendMode.srcIn),
|
colorFilter:
|
||||||
|
ColorFilter.mode(AnalyticsDateFilterButton._color, BlendMode.srcIn),
|
||||||
),
|
),
|
||||||
label: Text(
|
label: Text(
|
||||||
_concatenateDate(DateTime(2024, 1), DateTime(2024, 12)),
|
_formatDate(widget.selectedDate),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed: () {},
|
onPressed: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (_) {
|
||||||
|
return switch (widget.datePickerType) {
|
||||||
|
DatePickerType.month => MonthPickerWidget(
|
||||||
|
selectedDate: widget.selectedDate,
|
||||||
|
onDateSelected: (value) {
|
||||||
|
widget.onDateSelected?.call(value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
DatePickerType.year => YearPickerWidget(
|
||||||
|
selectedDate: widget.selectedDate,
|
||||||
|
onDateSelected: (value) {
|
||||||
|
widget.onDateSelected?.call(value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String _formatDate(DateTime date) {
|
String _formatDate(DateTime? date) {
|
||||||
final formatter = DateFormat('MMM yyyy');
|
final formatterBasedOnDatePickerType = switch (widget.datePickerType) {
|
||||||
final formattedDate = formatter.format(date);
|
DatePickerType.month => DateFormat('MMMM yyyy'),
|
||||||
|
DatePickerType.year => DateFormat('yyyy'),
|
||||||
|
};
|
||||||
|
final formattedDate = formatterBasedOnDatePickerType.format(
|
||||||
|
date ?? DateTime.now(),
|
||||||
|
);
|
||||||
return formattedDate;
|
return formattedDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
String _concatenateDate(DateTime startDate, DateTime endDate) {
|
|
||||||
final formattedStartDate = _formatDate(startDate);
|
|
||||||
final formattedEndDate = _formatDate(endDate);
|
|
||||||
return '$formattedStartDate - $formattedEndDate';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/bloc/analytics_tab_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/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';
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
|
||||||
class AnalyticsPageTabButton extends StatelessWidget {
|
class AnalyticsPageTabButton extends StatelessWidget {
|
||||||
@ -17,9 +18,12 @@ class AnalyticsPageTabButton extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return TextButton(
|
return TextButton(
|
||||||
onPressed: () => context.read<AnalyticsTabBloc>().add(
|
onPressed: () {
|
||||||
|
AnalyticsDataLoadingStrategyFactory.getStrategy(tab).clearData(context);
|
||||||
|
context.read<AnalyticsTabBloc>().add(
|
||||||
UpdateAnalyticsTabEvent(tab),
|
UpdateAnalyticsTabEvent(tab),
|
||||||
),
|
);
|
||||||
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
tab.title,
|
tab.title,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/bloc/analytics_tab_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_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/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_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/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';
|
import 'package:syncrow_web/utils/style.dart';
|
||||||
|
|
||||||
class AnalyticsPageTabsAndChildren extends StatelessWidget {
|
class AnalyticsPageTabsAndChildren extends StatelessWidget {
|
||||||
@ -11,6 +14,7 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final spaceTreeState = context.read<SpaceTreeBloc>().state;
|
||||||
return BlocBuilder<AnalyticsTabBloc, AnalyticsPageTab>(
|
return BlocBuilder<AnalyticsTabBloc, AnalyticsPageTab>(
|
||||||
buildWhen: (previous, current) => previous != current,
|
buildWhen: (previous, current) => previous != current,
|
||||||
builder: (context, selectedTab) => Column(
|
builder: (context, selectedTab) => Column(
|
||||||
@ -19,7 +23,6 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Container(
|
child: Container(
|
||||||
width: MediaQuery.sizeOf(context).width * 1,
|
|
||||||
decoration: subSectionContainerDecoration,
|
decoration: subSectionContainerDecoration,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||||
child: Row(
|
child: Row(
|
||||||
@ -39,9 +42,7 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
...AnalyticsPageTab.values.map(
|
...AnalyticsPageTab.values.map(
|
||||||
(tab) => AnimatedSwitcher(
|
(tab) => _buildAnimation(
|
||||||
switchInCurve: Curves.easeIn,
|
|
||||||
duration: const Duration(milliseconds: 200),
|
|
||||||
child: AnalyticsPageTabButton(
|
child: AnalyticsPageTabButton(
|
||||||
key: ValueKey(selectedTab),
|
key: ValueKey(selectedTab),
|
||||||
tab: tab,
|
tab: tab,
|
||||||
@ -54,12 +55,35 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
const Expanded(
|
Visibility(
|
||||||
flex: 2,
|
key: ValueKey(selectedTab),
|
||||||
child: FittedBox(
|
visible: selectedTab == AnalyticsPageTab.energyManagement,
|
||||||
fit: BoxFit.scaleDown,
|
child: Expanded(
|
||||||
alignment: AlignmentDirectional.centerEnd,
|
flex: 2,
|
||||||
child: AnalyticsDateFilterButton(),
|
child: FittedBox(
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
alignment: AlignmentDirectional.centerEnd,
|
||||||
|
child: AnalyticsDateFilterButton(
|
||||||
|
onDateSelected: (DateTime value) {
|
||||||
|
context.read<AnalyticsDatePickerBloc>().add(
|
||||||
|
UpdateAnalyticsDatePickerEvent(montlyDate: value),
|
||||||
|
);
|
||||||
|
FetchEnergyManagementDataHelper.loadEnergyManagementData(
|
||||||
|
context,
|
||||||
|
selectedDate: value,
|
||||||
|
communityId:
|
||||||
|
spaceTreeState.selectedCommunities.firstOrNull ??
|
||||||
|
'',
|
||||||
|
spaceId:
|
||||||
|
spaceTreeState.selectedSpaces.firstOrNull ?? '',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
selectedDate: context
|
||||||
|
.watch<AnalyticsDatePickerBloc>()
|
||||||
|
.state
|
||||||
|
.monthlyDate,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -68,17 +92,18 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 8,
|
flex: 8,
|
||||||
child: SizedBox(
|
child: _buildAnimation(child: selectedTab.child),
|
||||||
width: MediaQuery.sizeOf(context).width,
|
|
||||||
child: AnimatedSwitcher(
|
|
||||||
switchInCurve: Curves.easeIn,
|
|
||||||
duration: const Duration(milliseconds: 200),
|
|
||||||
child: selectedTab.child,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildAnimation({required Widget child}) {
|
||||||
|
return AnimatedSwitcher(
|
||||||
|
switchInCurve: Curves.easeIn,
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,194 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
|
|
||||||
|
class MonthPickerWidget extends StatefulWidget {
|
||||||
|
const MonthPickerWidget({
|
||||||
|
super.key,
|
||||||
|
required this.selectedDate,
|
||||||
|
required this.onDateSelected,
|
||||||
|
});
|
||||||
|
|
||||||
|
final DateTime selectedDate;
|
||||||
|
final ValueChanged<DateTime>? onDateSelected;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MonthPickerWidget> createState() => _MonthPickerWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MonthPickerWidgetState extends State<MonthPickerWidget> {
|
||||||
|
late int _currentYear;
|
||||||
|
int? _selectedMonth;
|
||||||
|
|
||||||
|
static const _monthNames = [
|
||||||
|
"January",
|
||||||
|
"February",
|
||||||
|
"March",
|
||||||
|
"April",
|
||||||
|
"May",
|
||||||
|
"June",
|
||||||
|
"July",
|
||||||
|
"August",
|
||||||
|
"September",
|
||||||
|
"October",
|
||||||
|
"November",
|
||||||
|
"December",
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_currentYear = widget.selectedDate.year;
|
||||||
|
_selectedMonth = widget.selectedDate.month - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Dialog(
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsetsDirectional.all(20),
|
||||||
|
width: 320,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
_buildYearSelector(),
|
||||||
|
_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,
|
||||||
|
_selectedMonth! + 1,
|
||||||
|
);
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Row _buildYearSelector() {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'$_currentYear',
|
||||||
|
style: context.textTheme.titleSmall?.copyWith(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: ColorsManager.grey700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => setState(() => _currentYear = _currentYear - 1),
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.chevron_left,
|
||||||
|
color: ColorsManager.grey700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => setState(() => _currentYear = _currentYear + 1),
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.chevron_right,
|
||||||
|
color: ColorsManager.grey700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMonthsGrid() {
|
||||||
|
return GridView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: 12,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 3,
|
||||||
|
childAspectRatio: 2.5,
|
||||||
|
mainAxisSpacing: 8,
|
||||||
|
mainAxisExtent: 30,
|
||||||
|
),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final isSelected = _selectedMonth == index;
|
||||||
|
return InkWell(
|
||||||
|
onTap: () => setState(() => _selectedMonth = index),
|
||||||
|
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(
|
||||||
|
_monthNames[index],
|
||||||
|
style: context.textTheme.titleSmall?.copyWith(
|
||||||
|
fontSize: 12,
|
||||||
|
color: isSelected
|
||||||
|
? ColorsManager.whiteColors
|
||||||
|
: 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,147 @@
|
|||||||
|
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 - 2020 + 1,
|
||||||
|
(index) => (2020 + index),
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_currentYear = widget.selectedDate.year;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Dialog(
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
|
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(),
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
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: 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
import 'package:bloc/bloc.dart';
|
||||||
|
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';
|
||||||
|
|
||||||
|
part 'energy_consumption_by_phases_event.dart';
|
||||||
|
part 'energy_consumption_by_phases_state.dart';
|
||||||
|
|
||||||
|
class EnergyConsumptionByPhasesBloc
|
||||||
|
extends Bloc<EnergyConsumptionByPhasesEvent, EnergyConsumptionByPhasesState> {
|
||||||
|
EnergyConsumptionByPhasesBloc(
|
||||||
|
this._energyConsumptionByPhasesService,
|
||||||
|
) : super(const EnergyConsumptionByPhasesState()) {
|
||||||
|
on<LoadEnergyConsumptionByPhasesEvent>(_onLoadEnergyConsumptionByPhasesEvent);
|
||||||
|
on<ClearEnergyConsumptionByPhasesEvent>(_onClearEnergyConsumptionByPhasesEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
final EnergyConsumptionByPhasesService _energyConsumptionByPhasesService;
|
||||||
|
|
||||||
|
Future<void> _onLoadEnergyConsumptionByPhasesEvent(
|
||||||
|
LoadEnergyConsumptionByPhasesEvent event,
|
||||||
|
Emitter<EnergyConsumptionByPhasesState> emit,
|
||||||
|
) async {
|
||||||
|
emit(state.copyWith(status: EnergyConsumptionByPhasesStatus.loading));
|
||||||
|
try {
|
||||||
|
final chartData = await _energyConsumptionByPhasesService.load(event.param);
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: EnergyConsumptionByPhasesStatus.loaded,
|
||||||
|
chartData: chartData,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: EnergyConsumptionByPhasesStatus.failure,
|
||||||
|
errorMessage: e.toString(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onClearEnergyConsumptionByPhasesEvent(
|
||||||
|
ClearEnergyConsumptionByPhasesEvent event,
|
||||||
|
Emitter<EnergyConsumptionByPhasesState> emit,
|
||||||
|
) async {
|
||||||
|
emit(const EnergyConsumptionByPhasesState());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
part of 'energy_consumption_by_phases_bloc.dart';
|
||||||
|
|
||||||
|
sealed class EnergyConsumptionByPhasesEvent extends Equatable {
|
||||||
|
const EnergyConsumptionByPhasesEvent();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [];
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoadEnergyConsumptionByPhasesEvent extends EnergyConsumptionByPhasesEvent {
|
||||||
|
const LoadEnergyConsumptionByPhasesEvent({
|
||||||
|
required this.param,
|
||||||
|
});
|
||||||
|
|
||||||
|
final GetEnergyConsumptionByPhasesParam param;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [param];
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ClearEnergyConsumptionByPhasesEvent extends EnergyConsumptionByPhasesEvent {
|
||||||
|
const ClearEnergyConsumptionByPhasesEvent();
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
part of 'energy_consumption_by_phases_bloc.dart';
|
||||||
|
|
||||||
|
enum EnergyConsumptionByPhasesStatus {
|
||||||
|
initial,
|
||||||
|
loading,
|
||||||
|
loaded,
|
||||||
|
failure,
|
||||||
|
}
|
||||||
|
|
||||||
|
final class EnergyConsumptionByPhasesState extends Equatable {
|
||||||
|
const EnergyConsumptionByPhasesState({
|
||||||
|
this.status = EnergyConsumptionByPhasesStatus.initial,
|
||||||
|
this.chartData = const <PhasesEnergyConsumption>[],
|
||||||
|
this.errorMessage,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<PhasesEnergyConsumption> chartData;
|
||||||
|
final EnergyConsumptionByPhasesStatus status;
|
||||||
|
final String? errorMessage;
|
||||||
|
|
||||||
|
EnergyConsumptionByPhasesState copyWith({
|
||||||
|
List<PhasesEnergyConsumption>? chartData,
|
||||||
|
EnergyConsumptionByPhasesStatus? status,
|
||||||
|
String? errorMessage,
|
||||||
|
}) {
|
||||||
|
return EnergyConsumptionByPhasesState(
|
||||||
|
chartData: chartData ?? this.chartData,
|
||||||
|
status: status ?? this.status,
|
||||||
|
errorMessage: errorMessage ?? this.errorMessage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [chartData, status, errorMessage];
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
import 'package:bloc/bloc.dart';
|
||||||
|
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';
|
||||||
|
|
||||||
|
part 'energy_consumption_per_device_event.dart';
|
||||||
|
part 'energy_consumption_per_device_state.dart';
|
||||||
|
|
||||||
|
class EnergyConsumptionPerDeviceBloc
|
||||||
|
extends Bloc<EnergyConsumptionPerDeviceEvent, EnergyConsumptionPerDeviceState> {
|
||||||
|
EnergyConsumptionPerDeviceBloc(
|
||||||
|
this._energyConsumptionPerDeviceService,
|
||||||
|
) : super(const EnergyConsumptionPerDeviceState()) {
|
||||||
|
on<LoadEnergyConsumptionPerDeviceEvent>(_onLoadEnergyConsumptionPerDeviceEvent);
|
||||||
|
on<ClearEnergyConsumptionPerDeviceEvent>(_onClearEnergyConsumptionPerDeviceEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
final EnergyConsumptionPerDeviceService _energyConsumptionPerDeviceService;
|
||||||
|
|
||||||
|
Future<void> _onLoadEnergyConsumptionPerDeviceEvent(
|
||||||
|
LoadEnergyConsumptionPerDeviceEvent event,
|
||||||
|
Emitter<EnergyConsumptionPerDeviceState> emit,
|
||||||
|
) async {
|
||||||
|
emit(state.copyWith(status: EnergyConsumptionPerDeviceStatus.loading));
|
||||||
|
try {
|
||||||
|
final chartData = await _energyConsumptionPerDeviceService.load(event.param);
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: EnergyConsumptionPerDeviceStatus.loaded,
|
||||||
|
chartData: chartData,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: EnergyConsumptionPerDeviceStatus.failure,
|
||||||
|
errorMessage: e.toString(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onClearEnergyConsumptionPerDeviceEvent(
|
||||||
|
ClearEnergyConsumptionPerDeviceEvent event,
|
||||||
|
Emitter<EnergyConsumptionPerDeviceState> emit,
|
||||||
|
) async {
|
||||||
|
emit(const EnergyConsumptionPerDeviceState());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
part of 'energy_consumption_per_device_bloc.dart';
|
||||||
|
|
||||||
|
sealed class EnergyConsumptionPerDeviceEvent extends Equatable {
|
||||||
|
const EnergyConsumptionPerDeviceEvent();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [];
|
||||||
|
}
|
||||||
|
|
||||||
|
final class LoadEnergyConsumptionPerDeviceEvent
|
||||||
|
extends EnergyConsumptionPerDeviceEvent {
|
||||||
|
const LoadEnergyConsumptionPerDeviceEvent(this.param);
|
||||||
|
|
||||||
|
final GetEnergyConsumptionPerDeviceParam param;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [param];
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ClearEnergyConsumptionPerDeviceEvent
|
||||||
|
extends EnergyConsumptionPerDeviceEvent {
|
||||||
|
const ClearEnergyConsumptionPerDeviceEvent();
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
part of 'energy_consumption_per_device_bloc.dart';
|
||||||
|
|
||||||
|
enum EnergyConsumptionPerDeviceStatus { initial, loading, loaded, failure }
|
||||||
|
|
||||||
|
final class EnergyConsumptionPerDeviceState extends Equatable {
|
||||||
|
const EnergyConsumptionPerDeviceState({
|
||||||
|
this.status = EnergyConsumptionPerDeviceStatus.initial,
|
||||||
|
this.chartData = const <DeviceEnergyDataModel>[],
|
||||||
|
this.errorMessage,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<DeviceEnergyDataModel> chartData;
|
||||||
|
final EnergyConsumptionPerDeviceStatus status;
|
||||||
|
final String? errorMessage;
|
||||||
|
|
||||||
|
EnergyConsumptionPerDeviceState copyWith({
|
||||||
|
List<DeviceEnergyDataModel>? chartData,
|
||||||
|
EnergyConsumptionPerDeviceStatus? status,
|
||||||
|
String? errorMessage,
|
||||||
|
}) {
|
||||||
|
return EnergyConsumptionPerDeviceState(
|
||||||
|
chartData: chartData ?? this.chartData,
|
||||||
|
status: status ?? this.status,
|
||||||
|
errorMessage: errorMessage ?? this.errorMessage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [chartData, status, errorMessage];
|
||||||
|
}
|
@ -0,0 +1,63 @@
|
|||||||
|
import 'package:bloc/bloc.dart';
|
||||||
|
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';
|
||||||
|
|
||||||
|
part 'power_clamp_info_event.dart';
|
||||||
|
part 'power_clamp_info_state.dart';
|
||||||
|
|
||||||
|
class PowerClampInfoBloc extends Bloc<PowerClampInfoEvent, PowerClampInfoState> {
|
||||||
|
PowerClampInfoBloc(
|
||||||
|
this._powerClampInfoService,
|
||||||
|
) : super(const PowerClampInfoState()) {
|
||||||
|
on<LoadPowerClampInfoEvent>(_onLoadPowerClampInfoEvent);
|
||||||
|
on<UpdatePowerClampStatusEvent>(_onUpdatePowerClampStatusEvent);
|
||||||
|
on<ClearPowerClampInfoEvent>(_onClearPowerClampInfoEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
final PowerClampInfoService _powerClampInfoService;
|
||||||
|
|
||||||
|
Future<void> _onLoadPowerClampInfoEvent(
|
||||||
|
LoadPowerClampInfoEvent event,
|
||||||
|
Emitter<PowerClampInfoState> emit,
|
||||||
|
) async {
|
||||||
|
emit(state.copyWith(status: PowerClampInfoStatus.loading));
|
||||||
|
try {
|
||||||
|
final powerClampModel = await _powerClampInfoService.getInfo(event.deviceId);
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: PowerClampInfoStatus.loaded,
|
||||||
|
powerClampModel: powerClampModel,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: PowerClampInfoStatus.error,
|
||||||
|
errorMessage: e.toString(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onUpdatePowerClampStatusEvent(
|
||||||
|
UpdatePowerClampStatusEvent event,
|
||||||
|
Emitter<PowerClampInfoState> emit,
|
||||||
|
) async {
|
||||||
|
final currentModel = state.powerClampModel;
|
||||||
|
if (currentModel == null) return;
|
||||||
|
|
||||||
|
final updatedStatus = PowerStatus.fromStatusList(event.statusList);
|
||||||
|
final updatedModel = currentModel.copyWith(statusPower: updatedStatus);
|
||||||
|
|
||||||
|
emit(state.copyWith(powerClampModel: updatedModel));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onClearPowerClampInfoEvent(
|
||||||
|
ClearPowerClampInfoEvent event,
|
||||||
|
Emitter<PowerClampInfoState> emit,
|
||||||
|
) {
|
||||||
|
emit(const PowerClampInfoState());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
part of 'power_clamp_info_bloc.dart';
|
||||||
|
|
||||||
|
sealed class PowerClampInfoEvent extends Equatable {
|
||||||
|
const PowerClampInfoEvent();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [];
|
||||||
|
}
|
||||||
|
|
||||||
|
final class LoadPowerClampInfoEvent extends PowerClampInfoEvent {
|
||||||
|
const LoadPowerClampInfoEvent(this.deviceId);
|
||||||
|
|
||||||
|
final String deviceId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [deviceId];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
final class UpdatePowerClampStatusEvent extends PowerClampInfoEvent {
|
||||||
|
const UpdatePowerClampStatusEvent(this.statusList);
|
||||||
|
|
||||||
|
final List<Status> statusList;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [statusList];
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ClearPowerClampInfoEvent extends PowerClampInfoEvent {
|
||||||
|
const ClearPowerClampInfoEvent();
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
part of 'power_clamp_info_bloc.dart';
|
||||||
|
|
||||||
|
enum PowerClampInfoStatus { initial, loading, loaded, error }
|
||||||
|
|
||||||
|
final class PowerClampInfoState extends Equatable {
|
||||||
|
const PowerClampInfoState({
|
||||||
|
this.status = PowerClampInfoStatus.initial,
|
||||||
|
this.powerClampModel,
|
||||||
|
this.errorMessage,
|
||||||
|
});
|
||||||
|
|
||||||
|
final PowerClampInfoStatus status;
|
||||||
|
final PowerClampModel? powerClampModel;
|
||||||
|
final String? errorMessage;
|
||||||
|
|
||||||
|
PowerClampInfoState copyWith({
|
||||||
|
PowerClampInfoStatus? status,
|
||||||
|
PowerClampModel? powerClampModel,
|
||||||
|
String? errorMessage,
|
||||||
|
}) {
|
||||||
|
return PowerClampInfoState(
|
||||||
|
status: status ?? this.status,
|
||||||
|
powerClampModel: powerClampModel ?? this.powerClampModel,
|
||||||
|
errorMessage: errorMessage ?? this.errorMessage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [status, powerClampModel, errorMessage];
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:syncrow_web/pages/analytics/services/realtime_device_service/realtime_device_service.dart';
|
||||||
|
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
|
||||||
|
|
||||||
|
part 'realtime_device_changes_event.dart';
|
||||||
|
part 'realtime_device_changes_state.dart';
|
||||||
|
|
||||||
|
class RealtimeDeviceChangesBloc
|
||||||
|
extends Bloc<RealtimeDeviceChangesEvent, RealtimeDeviceChangesState> {
|
||||||
|
RealtimeDeviceChangesBloc(
|
||||||
|
this._realtimeDeviceService,
|
||||||
|
) : super(const RealtimeDeviceChangesState()) {
|
||||||
|
on<RealtimeDeviceChangesStarted>(_onRealtimeDeviceChangesStarted);
|
||||||
|
on<RealtimeDeviceChangesClosed>(_onRealtimeDeviceChangesClosed);
|
||||||
|
on<_RealtimeDeviceChangesUpdated>(_onRealtimeDeviceChangesUpdated);
|
||||||
|
}
|
||||||
|
|
||||||
|
final RealtimeDeviceService _realtimeDeviceService;
|
||||||
|
StreamSubscription<List<Status>>? _subscription;
|
||||||
|
|
||||||
|
Future<void> _onRealtimeDeviceChangesStarted(
|
||||||
|
RealtimeDeviceChangesStarted event,
|
||||||
|
Emitter<RealtimeDeviceChangesState> emit,
|
||||||
|
) async {
|
||||||
|
await _subscription?.cancel();
|
||||||
|
_subscription = _realtimeDeviceService.subscribe(event.deviceId).listen(
|
||||||
|
(data) {
|
||||||
|
add(_RealtimeDeviceChangesUpdated(data));
|
||||||
|
},
|
||||||
|
onError: (error) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: RealtimeDeviceChangesStatus.failure,
|
||||||
|
errorMessage: '$error',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onRealtimeDeviceChangesClosed(
|
||||||
|
RealtimeDeviceChangesClosed event,
|
||||||
|
Emitter<RealtimeDeviceChangesState> emit,
|
||||||
|
) async {
|
||||||
|
add(const _RealtimeDeviceChangesUpdated([]));
|
||||||
|
await _subscription?.cancel();
|
||||||
|
_subscription = null;
|
||||||
|
emit(const RealtimeDeviceChangesState());
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onRealtimeDeviceChangesUpdated(
|
||||||
|
_RealtimeDeviceChangesUpdated event,
|
||||||
|
Emitter<RealtimeDeviceChangesState> emit,
|
||||||
|
) {
|
||||||
|
final currentState = state;
|
||||||
|
final updatedList = [
|
||||||
|
...currentState.deviceStatusList.where(
|
||||||
|
(device) => !event.deviceStatusList
|
||||||
|
.any((newDevice) => newDevice.code == device.code),
|
||||||
|
),
|
||||||
|
...event.deviceStatusList,
|
||||||
|
];
|
||||||
|
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: RealtimeDeviceChangesStatus.loaded,
|
||||||
|
deviceStatusList: updatedList,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() async {
|
||||||
|
await _subscription?.cancel();
|
||||||
|
return super.close();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
part of 'realtime_device_changes_bloc.dart';
|
||||||
|
|
||||||
|
sealed class RealtimeDeviceChangesEvent extends Equatable {
|
||||||
|
const RealtimeDeviceChangesEvent();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [];
|
||||||
|
}
|
||||||
|
|
||||||
|
final class RealtimeDeviceChangesStarted extends RealtimeDeviceChangesEvent {
|
||||||
|
const RealtimeDeviceChangesStarted(this.deviceId);
|
||||||
|
|
||||||
|
final String deviceId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [deviceId];
|
||||||
|
}
|
||||||
|
|
||||||
|
final class RealtimeDeviceChangesClosed extends RealtimeDeviceChangesEvent {
|
||||||
|
const RealtimeDeviceChangesClosed();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RealtimeDeviceChangesUpdated extends RealtimeDeviceChangesEvent {
|
||||||
|
final List<Status> deviceStatusList;
|
||||||
|
|
||||||
|
const _RealtimeDeviceChangesUpdated(this.deviceStatusList);
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
part of 'realtime_device_changes_bloc.dart';
|
||||||
|
|
||||||
|
enum RealtimeDeviceChangesStatus { initial, loaded, failure }
|
||||||
|
|
||||||
|
final class RealtimeDeviceChangesState extends Equatable {
|
||||||
|
const RealtimeDeviceChangesState({
|
||||||
|
this.status = RealtimeDeviceChangesStatus.initial,
|
||||||
|
this.deviceStatusList = const <Status>[],
|
||||||
|
this.errorMessage,
|
||||||
|
});
|
||||||
|
|
||||||
|
final RealtimeDeviceChangesStatus status;
|
||||||
|
final List<Status> deviceStatusList;
|
||||||
|
final String? errorMessage;
|
||||||
|
|
||||||
|
RealtimeDeviceChangesState copyWith({
|
||||||
|
RealtimeDeviceChangesStatus? status,
|
||||||
|
List<Status>? deviceStatusList,
|
||||||
|
String? errorMessage,
|
||||||
|
}) {
|
||||||
|
return RealtimeDeviceChangesState(
|
||||||
|
status: status ?? this.status,
|
||||||
|
deviceStatusList: deviceStatusList ?? this.deviceStatusList,
|
||||||
|
errorMessage: errorMessage ?? this.errorMessage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [status, deviceStatusList, errorMessage];
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
import 'package:bloc/bloc.dart';
|
||||||
|
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';
|
||||||
|
|
||||||
|
part 'total_energy_consumption_event.dart';
|
||||||
|
part 'total_energy_consumption_state.dart';
|
||||||
|
|
||||||
|
class TotalEnergyConsumptionBloc
|
||||||
|
extends Bloc<TotalEnergyConsumptionEvent, TotalEnergyConsumptionState> {
|
||||||
|
TotalEnergyConsumptionBloc(
|
||||||
|
this._totalEnergyConsumptionService,
|
||||||
|
) : super(const TotalEnergyConsumptionState()) {
|
||||||
|
on<TotalEnergyConsumptionLoadEvent>(_onTotalEnergyConsumptionLoadEvent);
|
||||||
|
on<ClearTotalEnergyConsumptionEvent>(_onClearTotalEnergyConsumptionEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
final TotalEnergyConsumptionService _totalEnergyConsumptionService;
|
||||||
|
|
||||||
|
Future<void> _onTotalEnergyConsumptionLoadEvent(
|
||||||
|
TotalEnergyConsumptionLoadEvent event,
|
||||||
|
Emitter<TotalEnergyConsumptionState> emit,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
emit(state.copyWith(status: TotalEnergyConsumptionStatus.loading));
|
||||||
|
final chartData = await _totalEnergyConsumptionService.load(event.param);
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
chartData: chartData,
|
||||||
|
status: TotalEnergyConsumptionStatus.loaded,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
errorMessage: e.toString(),
|
||||||
|
status: TotalEnergyConsumptionStatus.failure,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onClearTotalEnergyConsumptionEvent(
|
||||||
|
ClearTotalEnergyConsumptionEvent event,
|
||||||
|
Emitter<TotalEnergyConsumptionState> emit,
|
||||||
|
) async {
|
||||||
|
emit(const TotalEnergyConsumptionState());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
part of 'total_energy_consumption_bloc.dart';
|
||||||
|
|
||||||
|
sealed class TotalEnergyConsumptionEvent extends Equatable {
|
||||||
|
const TotalEnergyConsumptionEvent();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [];
|
||||||
|
}
|
||||||
|
|
||||||
|
final class TotalEnergyConsumptionLoadEvent extends TotalEnergyConsumptionEvent {
|
||||||
|
const TotalEnergyConsumptionLoadEvent({required this.param});
|
||||||
|
|
||||||
|
final GetTotalEnergyConsumptionParam param;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [param];
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ClearTotalEnergyConsumptionEvent extends TotalEnergyConsumptionEvent {
|
||||||
|
const ClearTotalEnergyConsumptionEvent();
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
part of 'total_energy_consumption_bloc.dart';
|
||||||
|
|
||||||
|
enum TotalEnergyConsumptionStatus {
|
||||||
|
initial,
|
||||||
|
loading,
|
||||||
|
loaded,
|
||||||
|
failure,
|
||||||
|
}
|
||||||
|
|
||||||
|
final class TotalEnergyConsumptionState extends Equatable {
|
||||||
|
const TotalEnergyConsumptionState({
|
||||||
|
this.status = TotalEnergyConsumptionStatus.initial,
|
||||||
|
this.chartData = const <EnergyDataModel>[],
|
||||||
|
this.errorMessage,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<EnergyDataModel> chartData;
|
||||||
|
final TotalEnergyConsumptionStatus status;
|
||||||
|
final String? errorMessage;
|
||||||
|
|
||||||
|
TotalEnergyConsumptionState copyWith({
|
||||||
|
List<EnergyDataModel>? chartData,
|
||||||
|
TotalEnergyConsumptionStatus? status,
|
||||||
|
String? errorMessage,
|
||||||
|
}) {
|
||||||
|
return TotalEnergyConsumptionState(
|
||||||
|
chartData: chartData ?? this.chartData,
|
||||||
|
status: status ?? this.status,
|
||||||
|
errorMessage: errorMessage ?? this.errorMessage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [chartData, status, errorMessage];
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
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),
|
||||||
|
];
|
||||||
|
}
|
@ -0,0 +1,122 @@
|
|||||||
|
import 'package:fl_chart/fl_chart.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:syncrow_web/pages/analytics/helpers/format_number_to_kwh.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
|
|
||||||
|
abstract final class EnergyManagementChartsHelper {
|
||||||
|
const EnergyManagementChartsHelper._();
|
||||||
|
|
||||||
|
static FlTitlesData titlesData(
|
||||||
|
BuildContext context, {
|
||||||
|
double? leftTitlesInterval,
|
||||||
|
}) {
|
||||||
|
const emptyTitle = AxisTitles(sideTitles: SideTitles(showTitles: false));
|
||||||
|
return FlTitlesData(
|
||||||
|
show: true,
|
||||||
|
bottomTitles: AxisTitles(
|
||||||
|
drawBelowEverything: true,
|
||||||
|
sideTitles: SideTitles(
|
||||||
|
interval: 1,
|
||||||
|
reservedSize: 32,
|
||||||
|
showTitles: true,
|
||||||
|
maxIncluded: true,
|
||||||
|
getTitlesWidget: (value, meta) => Padding(
|
||||||
|
padding: const EdgeInsetsDirectional.only(top: 20.0),
|
||||||
|
child: Text(
|
||||||
|
(value + 1).toString(),
|
||||||
|
style: context.textTheme.bodySmall?.copyWith(
|
||||||
|
color: ColorsManager.greyColor,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
leftTitles: AxisTitles(
|
||||||
|
sideTitles: SideTitles(
|
||||||
|
showTitles: true,
|
||||||
|
maxIncluded: true,
|
||||||
|
interval: leftTitlesInterval,
|
||||||
|
reservedSize: 110,
|
||||||
|
getTitlesWidget: (value, meta) => Padding(
|
||||||
|
padding: const EdgeInsetsDirectional.only(end: 12),
|
||||||
|
child: FittedBox(
|
||||||
|
alignment: AlignmentDirectional.centerStart,
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
child: Text(
|
||||||
|
value.formatNumberToKwh,
|
||||||
|
style: context.textTheme.bodySmall?.copyWith(
|
||||||
|
fontSize: 12,
|
||||||
|
color: ColorsManager.greyColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
rightTitles: emptyTitle,
|
||||||
|
topTitles: emptyTitle,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String getToolTipLabel(num month, double value) {
|
||||||
|
final monthLabel = month.toString();
|
||||||
|
final valueLabel = value.formatNumberToKwh;
|
||||||
|
final labels = [monthLabel, valueLabel];
|
||||||
|
return labels.where((element) => element.isNotEmpty).join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<LineTooltipItem?> getTooltipItems(List<LineBarSpot> touchedSpots) {
|
||||||
|
return touchedSpots.map((spot) {
|
||||||
|
return LineTooltipItem(
|
||||||
|
getToolTipLabel(spot.x + 1, spot.y),
|
||||||
|
const TextStyle(
|
||||||
|
color: ColorsManager.textPrimaryColor,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
static LineTouchTooltipData lineTouchTooltipData() {
|
||||||
|
return LineTouchTooltipData(
|
||||||
|
getTooltipColor: (touchTooltipItem) => ColorsManager.whiteColors,
|
||||||
|
tooltipBorder: const BorderSide(color: ColorsManager.semiTransparentBlack),
|
||||||
|
tooltipRoundedRadius: 16,
|
||||||
|
showOnTopOfTheChartBoxArea: false,
|
||||||
|
tooltipPadding: const EdgeInsets.all(8),
|
||||||
|
getTooltipItems: getTooltipItems,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
show: true,
|
||||||
|
drawVerticalLine: false,
|
||||||
|
drawHorizontalLine: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static LineTouchData lineTouchData() {
|
||||||
|
return LineTouchData(
|
||||||
|
handleBuiltInTouches: true,
|
||||||
|
touchSpotThreshold: 2,
|
||||||
|
touchTooltipData: EnergyManagementChartsHelper.lineTouchTooltipData(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,115 @@
|
|||||||
|
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/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_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';
|
||||||
|
|
||||||
|
abstract final class FetchEnergyManagementDataHelper {
|
||||||
|
const FetchEnergyManagementDataHelper._();
|
||||||
|
|
||||||
|
static const String _powerClampId = 'cb71d6ad-6e29-4eaa-ae3e-1a0d1c5f60fa';
|
||||||
|
|
||||||
|
static void loadEnergyManagementData(
|
||||||
|
BuildContext context, {
|
||||||
|
required String communityId,
|
||||||
|
required String spaceId,
|
||||||
|
DateTime? selectedDate,
|
||||||
|
}) {
|
||||||
|
if (communityId.isEmpty && spaceId.isEmpty) {
|
||||||
|
clearAllData(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final datePickerState = context.read<AnalyticsDatePickerBloc>().state;
|
||||||
|
final selectedDate0 = selectedDate ?? datePickerState.monthlyDate;
|
||||||
|
|
||||||
|
loadTotalEnergyConsumption(
|
||||||
|
context,
|
||||||
|
selectedDate: selectedDate0,
|
||||||
|
communityId: communityId,
|
||||||
|
spaceId: spaceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
loadEnergyConsumptionByPhases(context, selectedDate: selectedDate);
|
||||||
|
|
||||||
|
loadEnergyConsumptionPerDevice(context);
|
||||||
|
loadRealtimeDeviceChanges(context);
|
||||||
|
loadPowerClampInfo(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void loadEnergyConsumptionByPhases(
|
||||||
|
BuildContext context, {
|
||||||
|
DateTime? selectedDate,
|
||||||
|
}) {
|
||||||
|
final param = GetEnergyConsumptionByPhasesParam(
|
||||||
|
startDate: selectedDate,
|
||||||
|
spaceId: '',
|
||||||
|
);
|
||||||
|
context.read<EnergyConsumptionByPhasesBloc>().add(
|
||||||
|
LoadEnergyConsumptionByPhasesEvent(param: param),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void loadTotalEnergyConsumption(
|
||||||
|
BuildContext context, {
|
||||||
|
DateTime? selectedDate,
|
||||||
|
required String communityId,
|
||||||
|
required String spaceId,
|
||||||
|
}) {
|
||||||
|
final param = GetTotalEnergyConsumptionParam(
|
||||||
|
spaceId: spaceId,
|
||||||
|
communityId: communityId,
|
||||||
|
monthDate: selectedDate,
|
||||||
|
);
|
||||||
|
context.read<TotalEnergyConsumptionBloc>().add(
|
||||||
|
TotalEnergyConsumptionLoadEvent(param: param),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void loadEnergyConsumptionPerDevice(BuildContext context) {
|
||||||
|
const param = GetEnergyConsumptionPerDeviceParam();
|
||||||
|
context.read<EnergyConsumptionPerDeviceBloc>().add(
|
||||||
|
const LoadEnergyConsumptionPerDeviceEvent(param),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void loadPowerClampInfo(BuildContext context) {
|
||||||
|
context.read<PowerClampInfoBloc>().add(
|
||||||
|
const LoadPowerClampInfoEvent(_powerClampId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void loadRealtimeDeviceChanges(BuildContext context) {
|
||||||
|
context.read<RealtimeDeviceChangesBloc>().add(
|
||||||
|
const RealtimeDeviceChangesStarted(_powerClampId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void clearAllData(BuildContext context) {
|
||||||
|
context.read<RealtimeDeviceChangesBloc>().add(
|
||||||
|
const RealtimeDeviceChangesClosed(),
|
||||||
|
);
|
||||||
|
|
||||||
|
context.read<PowerClampInfoBloc>().add(
|
||||||
|
const ClearPowerClampInfoEvent(),
|
||||||
|
);
|
||||||
|
|
||||||
|
context.read<EnergyConsumptionPerDeviceBloc>().add(
|
||||||
|
const ClearEnergyConsumptionPerDeviceEvent(),
|
||||||
|
);
|
||||||
|
|
||||||
|
context.read<TotalEnergyConsumptionBloc>().add(
|
||||||
|
const ClearTotalEnergyConsumptionEvent(),
|
||||||
|
);
|
||||||
|
|
||||||
|
context.read<EnergyConsumptionByPhasesBloc>().add(
|
||||||
|
const ClearEnergyConsumptionByPhasesEvent(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,12 +1,92 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_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_per_device_chart_box.dart';
|
||||||
|
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/power_clamp_energy_data_widget.dart';
|
||||||
|
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart_box.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
|
||||||
|
|
||||||
class AnalyticsEnergyManagementView extends StatelessWidget {
|
class AnalyticsEnergyManagementView extends StatefulWidget {
|
||||||
const AnalyticsEnergyManagementView({super.key});
|
const AnalyticsEnergyManagementView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AnalyticsEnergyManagementView> createState() =>
|
||||||
|
_AnalyticsEnergyManagementViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AnalyticsEnergyManagementViewState
|
||||||
|
extends State<AnalyticsEnergyManagementView> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
final spaceTreeBloc = context.read<SpaceTreeBloc>();
|
||||||
|
final communityId = spaceTreeBloc.state.selectedCommunities.firstOrNull;
|
||||||
|
final spaceId = spaceTreeBloc.state.selectedSpaces.firstOrNull;
|
||||||
|
FetchEnergyManagementDataHelper.loadEnergyManagementData(
|
||||||
|
context,
|
||||||
|
communityId: communityId ?? '',
|
||||||
|
spaceId: spaceId ?? '',
|
||||||
|
);
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
static const _padding = EdgeInsetsDirectional.all(32);
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return const Center(
|
return LayoutBuilder(
|
||||||
child: Text('EnergyManagementView is Working!'),
|
builder: (context, constraints) {
|
||||||
|
final isMediumOrLess = constraints.maxWidth <= 900;
|
||||||
|
if (isMediumOrLess) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
padding: _padding,
|
||||||
|
child: Column(
|
||||||
|
spacing: 32,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
height: MediaQuery.sizeOf(context).height * 1.2,
|
||||||
|
child: const PowerClampEnergyDataWidget(),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: MediaQuery.sizeOf(context).height * 0.5,
|
||||||
|
child: const TotalEnergyConsumptionChartBox(),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: MediaQuery.sizeOf(context).height * 0.5,
|
||||||
|
child: const EnergyConsumptionPerDeviceChartBox(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: Container(
|
||||||
|
padding: _padding,
|
||||||
|
height: MediaQuery.sizeOf(context).height * 1,
|
||||||
|
child: const Column(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Row(
|
||||||
|
spacing: 32,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: Column(
|
||||||
|
spacing: 20,
|
||||||
|
children: [
|
||||||
|
Expanded(child: TotalEnergyConsumptionChartBox()),
|
||||||
|
Expanded(child: EnergyConsumptionPerDeviceChartBox()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(child: PowerClampEnergyDataWidget()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
|
|
||||||
|
class ChartTitle extends StatelessWidget {
|
||||||
|
const ChartTitle({super.key, required this.title});
|
||||||
|
|
||||||
|
final Widget title;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return DefaultTextStyle(
|
||||||
|
style: context.textTheme.titleLarge!.copyWith(
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: ColorsManager.blackColor,
|
||||||
|
),
|
||||||
|
child: title,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,172 @@
|
|||||||
|
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: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';
|
||||||
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
|
|
||||||
|
class EnergyConsumptionByPhasesChart extends StatelessWidget {
|
||||||
|
const EnergyConsumptionByPhasesChart({
|
||||||
|
super.key,
|
||||||
|
required this.energyData,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<PhasesEnergyConsumption> energyData;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BarChart(
|
||||||
|
BarChartData(
|
||||||
|
gridData: EnergyManagementChartsHelper.gridData(),
|
||||||
|
borderData: EnergyManagementChartsHelper.borderData(),
|
||||||
|
barTouchData: _barTouchData(context),
|
||||||
|
titlesData: _titlesData(context),
|
||||||
|
barGroups: energyData.asMap().entries.map((entry) {
|
||||||
|
final index = entry.key;
|
||||||
|
final data = entry.value;
|
||||||
|
|
||||||
|
return BarChartGroupData(
|
||||||
|
x: index,
|
||||||
|
barRods: [
|
||||||
|
BarChartRodData(
|
||||||
|
color: ColorsManager.vividBlue.withValues(alpha: 0.1),
|
||||||
|
toY: data.phaseA + data.phaseB + data.phaseC,
|
||||||
|
rodStackItems: [
|
||||||
|
BarChartRodStackItem(
|
||||||
|
0,
|
||||||
|
data.phaseA,
|
||||||
|
ColorsManager.vividBlue.withValues(alpha: 0.8),
|
||||||
|
),
|
||||||
|
BarChartRodStackItem(
|
||||||
|
data.phaseA,
|
||||||
|
data.phaseA + data.phaseB,
|
||||||
|
ColorsManager.vividBlue.withValues(alpha: 0.4),
|
||||||
|
),
|
||||||
|
BarChartRodStackItem(
|
||||||
|
data.phaseA + data.phaseB,
|
||||||
|
data.phaseA + data.phaseB + data.phaseC,
|
||||||
|
ColorsManager.vividBlue.withValues(alpha: 0.15),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
width: 16,
|
||||||
|
borderRadius: const BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(8),
|
||||||
|
topRight: Radius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = 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;
|
||||||
|
|
||||||
|
return BarTooltipItem(
|
||||||
|
'$month\n',
|
||||||
|
context.textTheme.bodyMedium!.copyWith(
|
||||||
|
color: ColorsManager.blackColor,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
TextSpan(
|
||||||
|
text: 'Phase A: $phaseA\n',
|
||||||
|
style: context.textTheme.bodySmall?.copyWith(
|
||||||
|
color: ColorsManager.blackColor,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: 'Phase B: $phaseB\n',
|
||||||
|
style: context.textTheme.bodySmall?.copyWith(
|
||||||
|
color: ColorsManager.blackColor,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: 'Phase C: $phaseC',
|
||||||
|
style: context.textTheme.bodySmall?.copyWith(
|
||||||
|
color: ColorsManager.blackColor,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
FlTitlesData _titlesData(BuildContext context) {
|
||||||
|
final titlesData = EnergyManagementChartsHelper.titlesData(
|
||||||
|
context,
|
||||||
|
leftTitlesInterval: 250,
|
||||||
|
);
|
||||||
|
|
||||||
|
final leftTitles = titlesData.leftTitles.copyWith(
|
||||||
|
sideTitles: titlesData.leftTitles.sideTitles.copyWith(
|
||||||
|
reservedSize: 70,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final bottomTitles = AxisTitles(
|
||||||
|
sideTitles: SideTitles(
|
||||||
|
showTitles: true,
|
||||||
|
getTitlesWidget: (value, _) {
|
||||||
|
final month = energyData[value.toInt()].month.getMonthName;
|
||||||
|
return FittedBox(
|
||||||
|
alignment: AlignmentDirectional.bottomCenter,
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
child: RotatedBox(
|
||||||
|
quarterTurns: 3,
|
||||||
|
child: Text(
|
||||||
|
month,
|
||||||
|
style: context.textTheme.bodySmall?.copyWith(
|
||||||
|
color: ColorsManager.greyColor,
|
||||||
|
fontSize: 11,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
reservedSize: 36,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return titlesData.copyWith(
|
||||||
|
leftTitles: leftTitles,
|
||||||
|
bottomTitles: bottomTitles,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_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/widgets/energy_consumption_by_phases_chart.dart';
|
||||||
|
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/energy_consumption_by_phases_title.dart';
|
||||||
|
import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart';
|
||||||
|
import 'package:syncrow_web/utils/style.dart';
|
||||||
|
|
||||||
|
class EnergyConsumptionByPhasesChartBox extends StatelessWidget {
|
||||||
|
const EnergyConsumptionByPhasesChartBox({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocBuilder<EnergyConsumptionByPhasesBloc,
|
||||||
|
EnergyConsumptionByPhasesState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsetsDirectional.all(20),
|
||||||
|
decoration: secondarySection,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
spacing: 20,
|
||||||
|
children: [
|
||||||
|
AnalyticsErrorWidget(state.errorMessage),
|
||||||
|
EnergyConsumptionByPhasesTitle(isLoading: state.status == EnergyConsumptionByPhasesStatus.loading,),
|
||||||
|
Expanded(
|
||||||
|
child: EnergyConsumptionByPhasesChart(
|
||||||
|
energyData: state.chartData,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
import 'package:flutter/material.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/utils/color_manager.dart';
|
||||||
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
|
|
||||||
|
class EnergyConsumptionByPhasesTitle extends StatelessWidget {
|
||||||
|
const EnergyConsumptionByPhasesTitle({super.key, required this.isLoading});
|
||||||
|
final bool isLoading;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
ChartsLoadingWidget(isLoading: isLoading),
|
||||||
|
Expanded(
|
||||||
|
flex: 4,
|
||||||
|
child: FittedBox(
|
||||||
|
alignment: AlignmentDirectional.centerStart,
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
child: ChartTitle(
|
||||||
|
title: Text(
|
||||||
|
'Energy Consumption by Phases',
|
||||||
|
style: context.textTheme.titleLarge?.copyWith(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
color: ColorsManager.textPrimaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
...<(String title, double opacity)>[
|
||||||
|
('A', 0.8),
|
||||||
|
('B', 0.4),
|
||||||
|
('C', 0.15),
|
||||||
|
].map((phase) => _buildPhaseCell(context, phase)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPhaseCell(
|
||||||
|
BuildContext context,
|
||||||
|
(String title, double colorOpacity) phase,
|
||||||
|
) {
|
||||||
|
final (title, colorOpacity) = phase;
|
||||||
|
|
||||||
|
return Expanded(
|
||||||
|
child: FittedBox(
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
spacing: 4,
|
||||||
|
children: [
|
||||||
|
CircleAvatar(
|
||||||
|
backgroundColor: ColorsManager.vividBlue.withValues(
|
||||||
|
alpha: colorOpacity,
|
||||||
|
),
|
||||||
|
radius: 4,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'Phase $title',
|
||||||
|
style: context.textTheme.labelSmall?.copyWith(
|
||||||
|
fontSize: 8,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
color: ColorsManager.lightGreyColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
import 'package:fl_chart/fl_chart.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:syncrow_web/pages/analytics/models/device_energy_data_model.dart';
|
||||||
|
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart';
|
||||||
|
|
||||||
|
class EnergyConsumptionPerDeviceChart extends StatelessWidget {
|
||||||
|
const EnergyConsumptionPerDeviceChart({super.key, required this.chartData});
|
||||||
|
|
||||||
|
final List<DeviceEnergyDataModel> chartData;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return LineChart(
|
||||||
|
LineChartData(
|
||||||
|
titlesData: EnergyManagementChartsHelper.titlesData(
|
||||||
|
context,
|
||||||
|
leftTitlesInterval: 250,
|
||||||
|
),
|
||||||
|
gridData: EnergyManagementChartsHelper.gridData(),
|
||||||
|
borderData: EnergyManagementChartsHelper.borderData(),
|
||||||
|
lineTouchData: EnergyManagementChartsHelper.lineTouchData(),
|
||||||
|
lineBarsData: chartData.map((e) {
|
||||||
|
return _buildChartBar(
|
||||||
|
color: e.color,
|
||||||
|
spots: e.energy
|
||||||
|
.map(
|
||||||
|
(energy) => FlSpot(
|
||||||
|
energy.date.day.toDouble(),
|
||||||
|
energy.value,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
duration: Durations.extralong1,
|
||||||
|
curve: Curves.easeIn,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
LineChartBarData _buildChartBar({
|
||||||
|
required Color color,
|
||||||
|
required List<FlSpot> spots,
|
||||||
|
}) {
|
||||||
|
return LineChartBarData(
|
||||||
|
spots: spots,
|
||||||
|
dashArray: [12, 18],
|
||||||
|
isCurved: true,
|
||||||
|
color: color,
|
||||||
|
barWidth: 3,
|
||||||
|
isStrokeCapRound: true,
|
||||||
|
dotData: const FlDotData(show: false),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,63 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_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';
|
||||||
|
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_devices_list.dart';
|
||||||
|
import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart';
|
||||||
|
import 'package:syncrow_web/pages/analytics/widgets/charts_loading_widget.dart';
|
||||||
|
import 'package:syncrow_web/utils/style.dart';
|
||||||
|
|
||||||
|
class EnergyConsumptionPerDeviceChartBox extends StatelessWidget {
|
||||||
|
const EnergyConsumptionPerDeviceChartBox({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocBuilder<EnergyConsumptionPerDeviceBloc,
|
||||||
|
EnergyConsumptionPerDeviceState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
return Container(
|
||||||
|
decoration: subSectionContainerDecoration.copyWith(
|
||||||
|
borderRadius: BorderRadius.circular(30),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.all(30),
|
||||||
|
child: Column(
|
||||||
|
spacing: 20,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
AnalyticsErrorWidget(state.errorMessage),
|
||||||
|
Row(
|
||||||
|
spacing: 32,
|
||||||
|
children: [
|
||||||
|
if (state.status == EnergyConsumptionPerDeviceStatus.loading)
|
||||||
|
const ChartsLoadingWidget(isLoading: true),
|
||||||
|
const Expanded(
|
||||||
|
flex: 3,
|
||||||
|
child: FittedBox(
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
alignment: AlignmentDirectional.centerStart,
|
||||||
|
child: ChartTitle(
|
||||||
|
title: Text('Energy Consumption per Device'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: EnergyConsumptionPerDeviceDevicesList(
|
||||||
|
chartData: state.chartData,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Divider(height: 0),
|
||||||
|
Expanded(
|
||||||
|
child: EnergyConsumptionPerDeviceChart(chartData: state.chartData),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,62 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:syncrow_web/pages/analytics/models/device_energy_data_model.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
|
||||||
|
class EnergyConsumptionPerDeviceDevicesList extends StatelessWidget {
|
||||||
|
const EnergyConsumptionPerDeviceDevicesList({required this.chartData, super.key});
|
||||||
|
|
||||||
|
final List<DeviceEnergyDataModel> chartData;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: Row(
|
||||||
|
spacing: 16,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: chartData.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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
|
|
||||||
|
class PowerClampEnergyDataDeviceDropdown extends StatelessWidget {
|
||||||
|
const PowerClampEnergyDataDeviceDropdown({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(
|
||||||
|
color: ColorsManager.greyColor,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: DropdownButton<String>(
|
||||||
|
value: 'Device 1',
|
||||||
|
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: context.textTheme.labelSmall?.copyWith(
|
||||||
|
color: ColorsManager.textPrimaryColor,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
padding: const EdgeInsetsDirectional.symmetric(
|
||||||
|
horizontal: 20,
|
||||||
|
vertical: 2,
|
||||||
|
),
|
||||||
|
items: [
|
||||||
|
for (var i = 1; i < 10; i++)
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: 'Device $i',
|
||||||
|
child: Text(
|
||||||
|
'Device $i',
|
||||||
|
style: context.textTheme.labelSmall?.copyWith(
|
||||||
|
color: ColorsManager.textPrimaryColor,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onChanged: (value) {},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,142 @@
|
|||||||
|
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/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/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/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 {
|
||||||
|
const PowerClampEnergyDataWidget({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocListener<RealtimeDeviceChangesBloc, RealtimeDeviceChangesState>(
|
||||||
|
listenWhen: (previous, current) =>
|
||||||
|
previous.deviceStatusList != current.deviceStatusList ||
|
||||||
|
previous.status != current.status,
|
||||||
|
listener: (context, state) => context.read<PowerClampInfoBloc>().add(
|
||||||
|
UpdatePowerClampStatusEvent(state.deviceStatusList),
|
||||||
|
),
|
||||||
|
child: BlocBuilder<PowerClampInfoBloc, PowerClampInfoState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
final generalDataPoints =
|
||||||
|
state.powerClampModel?.status.general.dataPoints ?? [];
|
||||||
|
return Container(
|
||||||
|
decoration: subSectionContainerDecoration.copyWith(
|
||||||
|
borderRadius: BorderRadius.circular(30),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsetsDirectional.all(32),
|
||||||
|
child: Column(
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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(
|
||||||
|
status: [
|
||||||
|
PowerClampEnergyStatus(
|
||||||
|
iconPath: Assets.powerActiveIcon,
|
||||||
|
title: 'Active',
|
||||||
|
value: _valueFromCode('EnergyConsumed', generalDataPoints),
|
||||||
|
unit: 'W',
|
||||||
|
),
|
||||||
|
PowerClampEnergyStatus(
|
||||||
|
iconPath: Assets.voltMeterIcon,
|
||||||
|
title: 'Current',
|
||||||
|
value: _valueFromCode('Current', generalDataPoints),
|
||||||
|
unit: 'A',
|
||||||
|
),
|
||||||
|
PowerClampEnergyStatus(
|
||||||
|
iconPath: Assets.frequencyIcon,
|
||||||
|
title: 'Frequency',
|
||||||
|
value: _valueFromCode('Frequency', generalDataPoints),
|
||||||
|
unit: 'Hz',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
Expanded(
|
||||||
|
flex: 4,
|
||||||
|
child: PowerClampPhasesDataWidget(
|
||||||
|
phaseA: state.powerClampModel?.status.phaseA,
|
||||||
|
phaseB: state.powerClampModel?.status.phaseB,
|
||||||
|
phaseC: state.powerClampModel?.status.phaseC,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
const Expanded(flex: 3, child: EnergyConsumptionByPhasesChartBox()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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: '--'))
|
||||||
|
.value
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,95 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
import 'package:syncrow_web/pages/analytics/models/power_clamp_energy_status.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
|
import 'package:syncrow_web/utils/style.dart';
|
||||||
|
|
||||||
|
class PowerClampEnergyStatusWidget extends StatelessWidget {
|
||||||
|
const PowerClampEnergyStatusWidget({
|
||||||
|
super.key,
|
||||||
|
required this.status,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<PowerClampEnergyStatus> status;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
decoration: secondarySection.copyWith(boxShadow: const []),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: List.generate(
|
||||||
|
status.length * 2 - 1,
|
||||||
|
(index) => index.isEven
|
||||||
|
? Expanded(child: _buildItem(context, status[index ~/ 2]))
|
||||||
|
: _buildDivider(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildItem(BuildContext context, PowerClampEnergyStatus item) {
|
||||||
|
return Center(
|
||||||
|
child: ListTile(
|
||||||
|
titleAlignment: ListTileTitleAlignment.center,
|
||||||
|
leading: SvgPicture.asset(
|
||||||
|
item.iconPath,
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
alignment: AlignmentDirectional.centerEnd,
|
||||||
|
height: 18,
|
||||||
|
width: 18,
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
title: Text(
|
||||||
|
item.title,
|
||||||
|
style: context.textTheme.bodySmall?.copyWith(
|
||||||
|
color: ColorsManager.textPrimaryColor.withValues(alpha: 0.83),
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
style: context.textTheme.bodySmall?.copyWith(
|
||||||
|
color: ColorsManager.textPrimaryColor.withValues(alpha: 0.83),
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
TextSpan(text: '${item.value} '),
|
||||||
|
TextSpan(
|
||||||
|
text: item.unit,
|
||||||
|
style: context.textTheme.bodySmall?.copyWith(
|
||||||
|
color: ColorsManager.textPrimaryColor.withValues(alpha: 0.83),
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
fontSize: 8,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDivider() {
|
||||||
|
return Container(
|
||||||
|
height: 1,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Color.fromARGB(20, 0, 0, 0),
|
||||||
|
offset: Offset(0, 1),
|
||||||
|
blurRadius: 1,
|
||||||
|
),
|
||||||
|
BoxShadow(
|
||||||
|
color: Color.fromARGB(30, 0, 0, 0),
|
||||||
|
offset: Offset(0, -2),
|
||||||
|
blurRadius: 3,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,115 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_svg/svg.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
|
import 'package:syncrow_web/utils/style.dart';
|
||||||
|
|
||||||
|
class PowerClampPhase extends StatelessWidget {
|
||||||
|
const PowerClampPhase({
|
||||||
|
super.key,
|
||||||
|
required this.iconPath,
|
||||||
|
required this.title,
|
||||||
|
required this.value,
|
||||||
|
this.unit,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String iconPath;
|
||||||
|
final String title;
|
||||||
|
final String value;
|
||||||
|
final String? unit;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Expanded(
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 4),
|
||||||
|
decoration: containerWhiteDecoration.copyWith(boxShadow: const []),
|
||||||
|
padding: const EdgeInsetsDirectional.all(8),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
spacing: 10,
|
||||||
|
children: [
|
||||||
|
_buildIcon(),
|
||||||
|
Expanded(
|
||||||
|
flex: 3,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
spacing: 4,
|
||||||
|
children: [
|
||||||
|
_buildTitle(context),
|
||||||
|
_buildValue(context),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildValue(BuildContext context) {
|
||||||
|
final textStyle = context.textTheme.bodySmall?.copyWith(
|
||||||
|
color: ColorsManager.textPrimaryColor.withValues(
|
||||||
|
alpha: 0.83,
|
||||||
|
),
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
fontSize: 15,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: FittedBox(
|
||||||
|
alignment: AlignmentDirectional.topCenter,
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
child: Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
style: textStyle,
|
||||||
|
children: [
|
||||||
|
TextSpan(text: '$value '),
|
||||||
|
if (unit != null)
|
||||||
|
TextSpan(
|
||||||
|
text: unit,
|
||||||
|
style: textStyle?.copyWith(
|
||||||
|
color: ColorsManager.textPrimaryColor.withValues(
|
||||||
|
alpha: 0.83,
|
||||||
|
),
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
fontSize: 8,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTitle(BuildContext context) {
|
||||||
|
return Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: FittedBox(
|
||||||
|
alignment: AlignmentDirectional.centerStart,
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
child: Text(
|
||||||
|
title,
|
||||||
|
style: context.textTheme.titleSmall?.copyWith(
|
||||||
|
color: ColorsManager.lightGreyColor,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
fontSize: 8,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildIcon() {
|
||||||
|
return Expanded(
|
||||||
|
child: FittedBox(
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
child: SvgPicture.asset(iconPath),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,131 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/power_clamp_phase.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 PowerClampPhasesDataWidget extends StatelessWidget {
|
||||||
|
const PowerClampPhasesDataWidget({
|
||||||
|
required this.phaseA,
|
||||||
|
required this.phaseB,
|
||||||
|
required this.phaseC,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Phase? phaseA;
|
||||||
|
final Phase? phaseB;
|
||||||
|
final Phase? phaseC;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final phases = [phaseA, phaseB, phaseC];
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
decoration: secondarySection.copyWith(boxShadow: const []),
|
||||||
|
child: Row(
|
||||||
|
children: List.generate(5, (index) {
|
||||||
|
if (index.isOdd) return _buildSeparator();
|
||||||
|
final phaseIndex = index ~/ 2;
|
||||||
|
final phase = phases[phaseIndex];
|
||||||
|
final phaseSuffix = ['A', 'B', 'C'][phaseIndex];
|
||||||
|
|
||||||
|
return Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsetsDirectional.symmetric(horizontal: 14),
|
||||||
|
child: Column(
|
||||||
|
spacing: 4,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
FittedBox(
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
alignment: AlignmentDirectional.center,
|
||||||
|
child: Text(
|
||||||
|
'Phase ${phaseIndex + 1}',
|
||||||
|
style: context.textTheme.titleLarge?.copyWith(
|
||||||
|
color: ColorsManager.textPrimaryColor,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
fontSize: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PowerClampPhase(
|
||||||
|
iconPath: Assets.powerActiveIcon,
|
||||||
|
title: 'Active Power',
|
||||||
|
value: _valueFromCode(
|
||||||
|
code: 'ReactivePower$phaseSuffix',
|
||||||
|
points: phase?.dataPoints,
|
||||||
|
),
|
||||||
|
unit: 'W',
|
||||||
|
),
|
||||||
|
PowerClampPhase(
|
||||||
|
iconPath: Assets.voltageIcon,
|
||||||
|
title: 'Voltage',
|
||||||
|
value: _valueFromCode(
|
||||||
|
code: 'Voltage$phaseSuffix',
|
||||||
|
points: phase?.dataPoints,
|
||||||
|
),
|
||||||
|
unit: 'V',
|
||||||
|
),
|
||||||
|
PowerClampPhase(
|
||||||
|
iconPath: Assets.voltMeterIcon,
|
||||||
|
title: 'Current',
|
||||||
|
value: _valueFromCode(
|
||||||
|
code: 'Current$phaseSuffix',
|
||||||
|
points: phase?.dataPoints,
|
||||||
|
),
|
||||||
|
unit: 'A',
|
||||||
|
),
|
||||||
|
PowerClampPhase(
|
||||||
|
iconPath: Assets.speedoMeter,
|
||||||
|
title: 'Power Factor',
|
||||||
|
value: _valueFromCode(
|
||||||
|
code: 'PowerFactor$phaseSuffix',
|
||||||
|
points: phase?.dataPoints,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSeparator() {
|
||||||
|
return Container(
|
||||||
|
height: double.infinity,
|
||||||
|
width: 1,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Color.fromARGB(20, 0, 0, 0),
|
||||||
|
offset: Offset(1, 0),
|
||||||
|
blurRadius: 1,
|
||||||
|
),
|
||||||
|
BoxShadow(
|
||||||
|
color: Color.fromARGB(30, 0, 0, 0),
|
||||||
|
offset: Offset(-2, 0),
|
||||||
|
blurRadius: 1,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _valueFromCode({
|
||||||
|
required String code,
|
||||||
|
required List<DataPoint>? points,
|
||||||
|
}) {
|
||||||
|
final element = points?.firstWhere(
|
||||||
|
(e) => e.code == code,
|
||||||
|
orElse: () => DataPoint(value: '--'),
|
||||||
|
);
|
||||||
|
|
||||||
|
return element?.value.toString() ?? '--';
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,77 @@
|
|||||||
|
import 'package:fl_chart/fl_chart.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
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});
|
||||||
|
|
||||||
|
final List<EnergyDataModel> chartData;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Expanded(
|
||||||
|
child: LineChart(
|
||||||
|
LineChartData(
|
||||||
|
titlesData: EnergyManagementChartsHelper.titlesData(context),
|
||||||
|
gridData: EnergyManagementChartsHelper.gridData(),
|
||||||
|
borderData: EnergyManagementChartsHelper.borderData(),
|
||||||
|
lineTouchData: EnergyManagementChartsHelper.lineTouchData(),
|
||||||
|
lineBarsData: _lineBarsData,
|
||||||
|
),
|
||||||
|
duration: Durations.extralong1,
|
||||||
|
curve: Curves.easeIn,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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.value,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
color: ColorsManager.blueColor.withValues(alpha: 0.6),
|
||||||
|
shadow: const Shadow(color: Colors.black12),
|
||||||
|
show: true,
|
||||||
|
isCurved: true,
|
||||||
|
belowBarData: BarAreaData(
|
||||||
|
show: true,
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
ColorsManager.vividBlue.withValues(alpha: 0.3),
|
||||||
|
ColorsManager.vividBlue.withValues(alpha: 0.2),
|
||||||
|
ColorsManager.vividBlue.withValues(alpha: 0.1),
|
||||||
|
Colors.transparent,
|
||||||
|
],
|
||||||
|
begin: Alignment.center,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
dotData: const FlDotData(show: false),
|
||||||
|
isStrokeCapRound: true,
|
||||||
|
barWidth: 3,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,49 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/total_energy_consumption/total_energy_consumption_bloc.dart';
|
||||||
|
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart';
|
||||||
|
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart.dart';
|
||||||
|
import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart';
|
||||||
|
import 'package:syncrow_web/pages/analytics/widgets/charts_loading_widget.dart';
|
||||||
|
import 'package:syncrow_web/utils/style.dart';
|
||||||
|
|
||||||
|
class TotalEnergyConsumptionChartBox extends StatelessWidget {
|
||||||
|
const TotalEnergyConsumptionChartBox({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocBuilder<TotalEnergyConsumptionBloc, TotalEnergyConsumptionState>(
|
||||||
|
builder: (context, state) => Container(
|
||||||
|
decoration: subSectionContainerDecoration.copyWith(
|
||||||
|
borderRadius: BorderRadius.circular(30),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.all(30),
|
||||||
|
child: Column(
|
||||||
|
spacing: 20,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
AnalyticsErrorWidget(state.errorMessage),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
ChartsLoadingWidget(
|
||||||
|
isLoading: state.status == TotalEnergyConsumptionStatus.loading,
|
||||||
|
),
|
||||||
|
const Expanded(
|
||||||
|
flex: 3,
|
||||||
|
child: FittedBox(
|
||||||
|
alignment: AlignmentDirectional.centerStart,
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
child: ChartTitle(title: Text('Total Energy Consumption')),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(flex: 4),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
TotalEnergyConsumptionChart(chartData: state.chartData),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
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));
|
||||||
|
} 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,49 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} 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,63 @@
|
|||||||
|
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/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_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);
|
||||||
|
}
|
||||||
|
|
||||||
|
final datePickerState = context.read<AnalyticsDatePickerBloc>().state;
|
||||||
|
|
||||||
|
context.read<OccupancyBloc>().add(
|
||||||
|
LoadOccupancyEvent(
|
||||||
|
GetOccupancyParam(
|
||||||
|
monthDate:
|
||||||
|
'${datePickerState.monthlyDate.year}-${datePickerState.monthlyDate.month}',
|
||||||
|
spaceUuid: spaceId,
|
||||||
|
communityUuid: communityId,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
context.read<OccupancyHeatMapBloc>().add(
|
||||||
|
LoadOccupancyHeatMapEvent(
|
||||||
|
GetOccupancyHeatMapParam(
|
||||||
|
spaceId: spaceId,
|
||||||
|
communityId: communityId,
|
||||||
|
year: datePickerState.yearlyDate,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
context.read<RealtimeDeviceChangesBloc>()
|
||||||
|
..add(const RealtimeDeviceChangesClosed())
|
||||||
|
..add(
|
||||||
|
const RealtimeDeviceChangesStarted('14fe6e7e-47af-4a07-ae0a-7c4a26ef8135'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void clearAllData(BuildContext context) {
|
||||||
|
context.read<OccupancyBloc>().add(
|
||||||
|
const ClearOccupancyEvent(),
|
||||||
|
);
|
||||||
|
context.read<OccupancyHeatMapBloc>().add(
|
||||||
|
const ClearOccupancyHeatMapEvent(),
|
||||||
|
);
|
||||||
|
context.read<RealtimeDeviceChangesBloc>().add(
|
||||||
|
const RealtimeDeviceChangesClosed(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,12 +1,56 @@
|
|||||||
import 'package:flutter/material.dart';
|
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 {
|
class AnalyticsOccupancyView extends StatelessWidget {
|
||||||
const AnalyticsOccupancyView({super.key});
|
const AnalyticsOccupancyView({super.key});
|
||||||
|
|
||||||
|
static const _padding = EdgeInsetsDirectional.all(32);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return const Center(
|
final height = MediaQuery.sizeOf(context).height;
|
||||||
child: Text('AnalyticsOccupancyView is Working!'),
|
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.45, child: const OccupancyEndSideBar()),
|
||||||
|
SizedBox(height: height * 0.5, child: const OccupancyChartBox()),
|
||||||
|
SizedBox(height: height * 0.5, child: const Placeholder()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,145 @@
|
|||||||
|
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: 1.0,
|
||||||
|
gridData: EnergyManagementChartsHelper.gridData().copyWith(
|
||||||
|
checkToShowHorizontalLine: (value) => true,
|
||||||
|
horizontalInterval: 0.25,
|
||||||
|
),
|
||||||
|
borderData: EnergyManagementChartsHelper.borderData(),
|
||||||
|
barTouchData: _barTouchData(context),
|
||||||
|
titlesData: _titlesData(context),
|
||||||
|
barGroups: List.generate(chartData.length, (index) {
|
||||||
|
final actual = chartData[index];
|
||||||
|
return BarChartGroupData(
|
||||||
|
x: index,
|
||||||
|
barsSpace: 0,
|
||||||
|
groupVertically: true,
|
||||||
|
barRods: [
|
||||||
|
BarChartRodData(
|
||||||
|
toY: 1.0,
|
||||||
|
fromY: double.parse(actual.occupancy) + 0.025,
|
||||||
|
color: ColorsManager.graysColor,
|
||||||
|
width: _chartWidth,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
BarChartRodData(
|
||||||
|
toY: double.parse(actual.occupancy),
|
||||||
|
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 * 100).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: 0.25,
|
||||||
|
getTitlesWidget: (value, meta) => Padding(
|
||||||
|
padding: const EdgeInsetsDirectional.only(end: 12),
|
||||||
|
child: FittedBox(
|
||||||
|
alignment: AlignmentDirectional.centerStart,
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
child: Text(
|
||||||
|
'${(value * 100).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(
|
||||||
|
(value + 1).toString(),
|
||||||
|
style: context.textTheme.bodySmall?.copyWith(
|
||||||
|
color: ColorsManager.greyColor,
|
||||||
|
fontSize: 8,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
reservedSize: 36,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return titlesData.copyWith(
|
||||||
|
leftTitles: leftTitles,
|
||||||
|
bottomTitles: bottomTitles,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,73 @@
|
|||||||
|
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(
|
||||||
|
spacing: 20,
|
||||||
|
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),
|
||||||
|
);
|
||||||
|
FetchOccupancyDataHelper.loadOccupancyData(
|
||||||
|
context,
|
||||||
|
communityId: spaceTreeState.selectedCommunities.firstOrNull ?? '',
|
||||||
|
spaceId: spaceTreeState.selectedSpaces.firstOrNull ?? '',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
selectedDate: context
|
||||||
|
.watch<AnalyticsDatePickerBloc>()
|
||||||
|
.state
|
||||||
|
.monthlyDate,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Divider(height: 0),
|
||||||
|
Expanded(child: OccupancyChart(chartData: state.chartData)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,133 @@
|
|||||||
|
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_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/device_managment/all_devices/models/device_status.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||||
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
|
import 'package:syncrow_web/utils/style.dart';
|
||||||
|
import 'package:uuid/uuid.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: [
|
||||||
|
_buildHeader(context),
|
||||||
|
Text(
|
||||||
|
'Device ID:',
|
||||||
|
style: context.textTheme.bodySmall?.copyWith(
|
||||||
|
color: ColorsManager.textPrimaryColor,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
SelectableText(
|
||||||
|
(const Uuid().v4()),
|
||||||
|
style: context.textTheme.bodySmall?.copyWith(
|
||||||
|
color: ColorsManager.blackColor,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
const Divider(height: 1, color: ColorsManager.greyColor),
|
||||||
|
const SizedBox(height: 50),
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildHeader(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: FittedBox(
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
alignment: AlignmentDirectional.centerStart,
|
||||||
|
child: SelectableText(
|
||||||
|
'Presnce Sensor',
|
||||||
|
style: context.textTheme.headlineSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: ColorsManager.vividBlue.withValues(alpha: 0.6),
|
||||||
|
fontSize: 18,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
const Expanded(
|
||||||
|
child: FittedBox(
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
alignment: AlignmentDirectional.centerEnd,
|
||||||
|
child: PowerClampEnergyDataDeviceDropdown(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,84 @@
|
|||||||
|
import 'dart:math' as math show max;
|
||||||
|
|
||||||
|
import 'package:flutter/material.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]!).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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@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),
|
||||||
|
CustomPaint(
|
||||||
|
size: const Size(_totalWeeks * _cellSize, 7 * _cellSize),
|
||||||
|
child: CustomPaint(
|
||||||
|
isComplex: true,
|
||||||
|
size: const Size(_totalWeeks * _cellSize, 7 * _cellSize),
|
||||||
|
painter: OccupancyPainter(
|
||||||
|
items: paintItems,
|
||||||
|
maxValue: _maxValue,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
OccupancyHeatMapGradient(maxValue: _maxValue),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,81 @@
|
|||||||
|
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(
|
||||||
|
spacing: 20,
|
||||||
|
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),
|
||||||
|
);
|
||||||
|
FetchOccupancyDataHelper.loadOccupancyData(
|
||||||
|
context,
|
||||||
|
communityId:
|
||||||
|
spaceTreeState.selectedCommunities.firstOrNull ?? '',
|
||||||
|
spaceId: spaceTreeState.selectedSpaces.firstOrNull ?? '',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
datePickerType: DatePickerType.year,
|
||||||
|
selectedDate: context
|
||||||
|
.watch<AnalyticsDatePickerBloc>()
|
||||||
|
.state
|
||||||
|
.yearlyDate,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Divider(height: 0),
|
||||||
|
Expanded(
|
||||||
|
child: OccupancyHeatMap(
|
||||||
|
heatMapData: state.heatMapData.asMap().map(
|
||||||
|
(_, value) => MapEntry(value.date, value.occupancy),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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.centerEnd,
|
||||||
|
end: AlignmentDirectional.centerStart,
|
||||||
|
colors: _heatMapColors(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,60 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_days.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
|
||||||
|
class OccupancyHeatMapMonths extends StatelessWidget {
|
||||||
|
const OccupancyHeatMapMonths({
|
||||||
|
required this.startDate,
|
||||||
|
required this.cellSize,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final DateTime startDate;
|
||||||
|
final double cellSize;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
height: 48,
|
||||||
|
width: double.infinity,
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
OccupancyHeatMapDays(
|
||||||
|
cellSize: cellSize / 3,
|
||||||
|
textColor: Colors.transparent,
|
||||||
|
),
|
||||||
|
...List.generate(12, (monthIndex) {
|
||||||
|
final monthStartDate = DateTime(startDate.year, monthIndex + 1, 1);
|
||||||
|
final monthName = DateFormat.MMM().format(monthStartDate);
|
||||||
|
return Expanded(
|
||||||
|
child: RotatedBox(
|
||||||
|
quarterTurns: 3,
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsetsDirectional.zero,
|
||||||
|
margin: EdgeInsetsDirectional.zero,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
top: BorderSide(color: ColorsManager.borderColor),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
width: cellSize * 4,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 4, top: 2),
|
||||||
|
child: Text(
|
||||||
|
monthName,
|
||||||
|
style: const TextStyle(fontSize: 8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,84 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
|
||||||
|
class OccupancyPaintItem {
|
||||||
|
final int index;
|
||||||
|
final int value;
|
||||||
|
|
||||||
|
const OccupancyPaintItem({required this.index, required this.value});
|
||||||
|
}
|
||||||
|
|
||||||
|
class OccupancyPainter extends CustomPainter {
|
||||||
|
OccupancyPainter({
|
||||||
|
required this.items,
|
||||||
|
required this.maxValue,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<OccupancyPaintItem> items;
|
||||||
|
final int maxValue;
|
||||||
|
|
||||||
|
static const double cellSize = 16.0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final Paint fillPaint = Paint();
|
||||||
|
final Paint borderPaint = Paint()
|
||||||
|
..color = ColorsManager.grayBorder.withValues(alpha: 0.4)
|
||||||
|
..style = PaintingStyle.stroke;
|
||||||
|
|
||||||
|
for (final item in items) {
|
||||||
|
final column = item.index ~/ 7;
|
||||||
|
final row = item.index % 7;
|
||||||
|
|
||||||
|
final x = column * cellSize;
|
||||||
|
final y = row * cellSize;
|
||||||
|
|
||||||
|
fillPaint.color = _getColor(item.value);
|
||||||
|
final rect = Rect.fromLTWH(x, y, cellSize, cellSize);
|
||||||
|
canvas.drawRect(rect, fillPaint);
|
||||||
|
|
||||||
|
_drawDashedLine(
|
||||||
|
canvas,
|
||||||
|
Offset(x, y),
|
||||||
|
Offset(x + cellSize, y),
|
||||||
|
borderPaint,
|
||||||
|
);
|
||||||
|
_drawDashedLine(
|
||||||
|
canvas,
|
||||||
|
Offset(x, y + cellSize),
|
||||||
|
Offset(x + cellSize, y + cellSize),
|
||||||
|
borderPaint,
|
||||||
|
);
|
||||||
|
|
||||||
|
canvas.drawLine(Offset(x, y), Offset(x, y + cellSize), borderPaint);
|
||||||
|
canvas.drawLine(
|
||||||
|
Offset(x + cellSize, y), Offset(x + cellSize, y + cellSize), borderPaint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _drawDashedLine(Canvas canvas, Offset start, Offset end, Paint paint) {
|
||||||
|
const double dashWidth = 2.0;
|
||||||
|
const double dashSpace = 4.0;
|
||||||
|
final double totalLength = (end - start).distance;
|
||||||
|
final Offset direction = (end - start) / (end - start).distance;
|
||||||
|
|
||||||
|
double currentLength = 0.0;
|
||||||
|
while (currentLength < totalLength) {
|
||||||
|
final Offset dashStart = start + direction * currentLength;
|
||||||
|
final double nextLength = currentLength + dashWidth;
|
||||||
|
final Offset dashEnd =
|
||||||
|
start + direction * (nextLength < totalLength ? nextLength : totalLength);
|
||||||
|
canvas.drawLine(dashStart, dashEnd, paint);
|
||||||
|
currentLength = nextLength + dashSpace;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _getColor(int value) {
|
||||||
|
if (maxValue == 0) return ColorsManager.vividBlue.withValues(alpha: 0);
|
||||||
|
final opacity = value.clamp(0, maxValue) / maxValue;
|
||||||
|
return ColorsManager.vividBlue.withValues(alpha: opacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||||
|
}
|
@ -1,12 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class AnalyticsOverviewView extends StatelessWidget {
|
|
||||||
const AnalyticsOverviewView({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return const Center(
|
|
||||||
child: Text('Coming Soon!'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,24 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
class GetEnergyConsumptionByPhasesParam extends Equatable {
|
||||||
|
final DateTime? startDate;
|
||||||
|
final DateTime? endDate;
|
||||||
|
final String? spaceId;
|
||||||
|
|
||||||
|
const GetEnergyConsumptionByPhasesParam({
|
||||||
|
this.startDate,
|
||||||
|
this.endDate,
|
||||||
|
this.spaceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'startDate': startDate?.toIso8601String(),
|
||||||
|
'endDate': endDate?.toIso8601String(),
|
||||||
|
'spaceId': spaceId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [startDate, endDate, spaceId];
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
class GetEnergyConsumptionPerDeviceParam {
|
||||||
|
const GetEnergyConsumptionPerDeviceParam();
|
||||||
|
}
|
19
lib/pages/analytics/params/get_occupancy_heat_map_param.dart
Normal file
19
lib/pages/analytics/params/get_occupancy_heat_map_param.dart
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
class GetOccupancyHeatMapParam {
|
||||||
|
final DateTime year;
|
||||||
|
final String communityId;
|
||||||
|
final String spaceId;
|
||||||
|
|
||||||
|
const GetOccupancyHeatMapParam({
|
||||||
|
required this.year,
|
||||||
|
required this.communityId,
|
||||||
|
required this.spaceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'year': year.toIso8601String(),
|
||||||
|
'communityId': communityId,
|
||||||
|
'spaceId': spaceId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
19
lib/pages/analytics/params/get_occupancy_param.dart
Normal file
19
lib/pages/analytics/params/get_occupancy_param.dart
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
class GetOccupancyParam {
|
||||||
|
final String monthDate;
|
||||||
|
final String? spaceUuid;
|
||||||
|
final String communityUuid;
|
||||||
|
|
||||||
|
GetOccupancyParam({
|
||||||
|
required this.monthDate,
|
||||||
|
required this.spaceUuid,
|
||||||
|
required this.communityUuid,
|
||||||
|
});
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'monthDate': monthDate,
|
||||||
|
'spaceUuid': spaceUuid,
|
||||||
|
'communityUuid': communityUuid,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
class GetTotalEnergyConsumptionParam {
|
||||||
|
final DateTime? monthDate;
|
||||||
|
final String? spaceId;
|
||||||
|
final String? communityId;
|
||||||
|
|
||||||
|
const GetTotalEnergyConsumptionParam({
|
||||||
|
this.monthDate,
|
||||||
|
this.spaceId,
|
||||||
|
this.communityId,
|
||||||
|
});
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'monthDate':
|
||||||
|
'${monthDate?.year}-${monthDate?.month.toString().padLeft(2, '0')}',
|
||||||
|
if (communityId == null || communityId!.isEmpty) 'spaceUuid': spaceId,
|
||||||
|
'communityUuid': communityId,
|
||||||
|
'groupByDevice': false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
abstract interface class EnergyConsumptionByPhasesService {
|
||||||
|
Future<List<PhasesEnergyConsumption>> load(
|
||||||
|
GetEnergyConsumptionByPhasesParam param,
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
class FakeEnergyConsumptionByPhasesService
|
||||||
|
implements EnergyConsumptionByPhasesService {
|
||||||
|
@override
|
||||||
|
Future<List<PhasesEnergyConsumption>> load(
|
||||||
|
GetEnergyConsumptionByPhasesParam param,
|
||||||
|
) {
|
||||||
|
return Future.delayed(
|
||||||
|
const Duration(milliseconds: 500),
|
||||||
|
() => const [
|
||||||
|
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: 100, phaseC: 100),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
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/http_service.dart';
|
||||||
|
|
||||||
|
final class RemoteEnergyConsumptionByPhasesService
|
||||||
|
implements EnergyConsumptionByPhasesService {
|
||||||
|
const RemoteEnergyConsumptionByPhasesService(this._httpService);
|
||||||
|
|
||||||
|
final HTTPService _httpService;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<PhasesEnergyConsumption>> load(
|
||||||
|
GetEnergyConsumptionByPhasesParam param,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
final response = await _httpService.get(
|
||||||
|
path: 'endpoint',
|
||||||
|
showServerMessage: true,
|
||||||
|
expectedResponseModel: (data) {
|
||||||
|
final json = data as Map<String, dynamic>? ?? {};
|
||||||
|
final mappedData = json['data'] as List<dynamic>? ?? [];
|
||||||
|
return mappedData.map((e) {
|
||||||
|
final jsonData = e as Map<String, dynamic>;
|
||||||
|
return PhasesEnergyConsumption.fromJson(jsonData);
|
||||||
|
}).toList();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return response;
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception('Failed to load energy consumption per device: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
abstract interface class EnergyConsumptionPerDeviceService {
|
||||||
|
Future<List<DeviceEnergyDataModel>> load(
|
||||||
|
GetEnergyConsumptionPerDeviceParam param,
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
import 'dart:math' as math show Random;
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:syncrow_web/pages/analytics/models/device_energy_data_model.dart';
|
||||||
|
import 'package:syncrow_web/pages/analytics/models/energy_data_model.dart';
|
||||||
|
import 'package:syncrow_web/pages/analytics/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';
|
||||||
|
|
||||||
|
class FakeEnergyConsumptionPerDeviceService
|
||||||
|
implements EnergyConsumptionPerDeviceService {
|
||||||
|
@override
|
||||||
|
Future<List<DeviceEnergyDataModel>> load(
|
||||||
|
GetEnergyConsumptionPerDeviceParam param,
|
||||||
|
) {
|
||||||
|
final random = math.Random();
|
||||||
|
return Future.delayed(const Duration(milliseconds: 500), () {
|
||||||
|
return [
|
||||||
|
(Colors.redAccent, 1),
|
||||||
|
(Colors.lightBlueAccent, 2),
|
||||||
|
(Colors.purpleAccent, 3),
|
||||||
|
].map((e) {
|
||||||
|
final (color, index) = e;
|
||||||
|
return DeviceEnergyDataModel(
|
||||||
|
color: color,
|
||||||
|
energy: List.generate(30, (i) => i)
|
||||||
|
.map(
|
||||||
|
(index) => EnergyDataModel(
|
||||||
|
date: DateTime(2025, 1, index + 1),
|
||||||
|
value: random.nextInt(100) + (index * 100),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
deviceName: 'Device $index',
|
||||||
|
deviceId: 'device_$index',
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
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/http_service.dart';
|
||||||
|
|
||||||
|
class RemoteEnergyConsumptionPerDeviceService
|
||||||
|
implements EnergyConsumptionPerDeviceService {
|
||||||
|
const RemoteEnergyConsumptionPerDeviceService(this._httpService);
|
||||||
|
|
||||||
|
final HTTPService _httpService;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<DeviceEnergyDataModel>> load(
|
||||||
|
GetEnergyConsumptionPerDeviceParam param,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
final response = await _httpService.get(
|
||||||
|
path: 'endpoint',
|
||||||
|
showServerMessage: true,
|
||||||
|
expectedResponseModel: (data) {
|
||||||
|
final json = data as Map<String, dynamic>? ?? {};
|
||||||
|
final mappedData = json['data'] as List<dynamic>? ?? [];
|
||||||
|
return mappedData.map((e) {
|
||||||
|
final jsonData = e as Map<String, dynamic>;
|
||||||
|
return DeviceEnergyDataModel.fromJson(jsonData);
|
||||||
|
}).toList();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return response;
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception('Failed to load energy consumption per device: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
class FakeOccupacyService implements OccupacyService {
|
||||||
|
@override
|
||||||
|
Future<List<Occupacy>> load(GetOccupancyParam param) async {
|
||||||
|
return await Future.delayed(
|
||||||
|
const Duration(seconds: 1),
|
||||||
|
() => List.generate(
|
||||||
|
30,
|
||||||
|
(index) => Occupacy(
|
||||||
|
date: DateTime.now().subtract(Duration(days: index)).toString(),
|
||||||
|
occupancy: ((index / 100)).toString(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
import 'package:syncrow_web/pages/analytics/models/occupacy.dart';
|
||||||
|
import 'package:syncrow_web/pages/analytics/params/get_occupancy_param.dart';
|
||||||
|
|
||||||
|
abstract interface class OccupacyService {
|
||||||
|
Future<List<Occupacy>> load(GetOccupancyParam param);
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
class FakeOccupancyHeatMapService implements OccupancyHeatMapService {
|
||||||
|
@override
|
||||||
|
Future<List<OccupancyHeatMapModel>> load(GetOccupancyHeatMapParam param) {
|
||||||
|
return Future.delayed(const Duration(milliseconds: 200), () {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final startOfYear = DateTime(now.year, 1, 1);
|
||||||
|
final endOfYear = DateTime(now.year, 12, 31);
|
||||||
|
final daysInYear = endOfYear.difference(startOfYear).inDays + 1;
|
||||||
|
|
||||||
|
final List<OccupancyHeatMapModel> data = List.generate(
|
||||||
|
daysInYear,
|
||||||
|
(index) => OccupancyHeatMapModel(
|
||||||
|
date: startOfYear.add(Duration(days: index)),
|
||||||
|
occupancy: ((index + 1) * 10) % 100,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
abstract interface class OccupancyHeatMapService {
|
||||||
|
Future<List<OccupancyHeatMapModel>> load(GetOccupancyHeatMapParam param);
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
import 'package:syncrow_web/pages/device_managment/power_clamp/models/power_clamp_model.dart';
|
||||||
|
|
||||||
|
abstract interface class PowerClampInfoService {
|
||||||
|
Future<PowerClampModel> getInfo(String deviceId);
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
import 'package:syncrow_web/pages/analytics/services/power_clamp_info/power_clamp_info_service.dart';
|
||||||
|
import 'package:syncrow_web/pages/device_managment/power_clamp/models/power_clamp_model.dart';
|
||||||
|
import 'package:syncrow_web/services/api/http_service.dart';
|
||||||
|
|
||||||
|
final class RemotePowerClampInfoService implements PowerClampInfoService {
|
||||||
|
const RemotePowerClampInfoService(this._httpService);
|
||||||
|
|
||||||
|
final HTTPService _httpService;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<PowerClampModel> getInfo(String deviceId) async {
|
||||||
|
try {
|
||||||
|
final response = await _httpService.get(
|
||||||
|
path: '/devices/$deviceId/functions/status',
|
||||||
|
showServerMessage: true,
|
||||||
|
expectedResponseModel: (data) {
|
||||||
|
final json = data as Map<String, Object?>? ?? {};
|
||||||
|
final mappedData = json['data'] as Map<String, Object?>? ?? {};
|
||||||
|
return PowerClampModel.fromJson(mappedData);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return response;
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception('Failed to fetch power clamp info: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
import 'package:firebase_database/firebase_database.dart';
|
||||||
|
import 'package:syncrow_web/pages/analytics/services/realtime_device_service/realtime_device_service.dart';
|
||||||
|
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
|
||||||
|
|
||||||
|
class FirebaseRealtimeDeviceService implements RealtimeDeviceService {
|
||||||
|
@override
|
||||||
|
Stream<List<Status>> subscribe(String deviceId) {
|
||||||
|
try {
|
||||||
|
final ref = FirebaseDatabase.instance.ref('device-status/$deviceId');
|
||||||
|
|
||||||
|
return ref.onValue.asyncMap((event) {
|
||||||
|
final data = event.snapshot.value as Map<dynamic, dynamic>?;
|
||||||
|
|
||||||
|
if (data == null || data['status'] == null) {
|
||||||
|
throw Exception('Invalid data received from Firebase');
|
||||||
|
}
|
||||||
|
|
||||||
|
final statusMap = data['status'] as List<dynamic>;
|
||||||
|
return statusMap.map((status) {
|
||||||
|
if (status is! Map<dynamic, dynamic>) {
|
||||||
|
throw Exception('Invalid status format');
|
||||||
|
}
|
||||||
|
|
||||||
|
return Status(
|
||||||
|
code: status['code']?.toString() ?? '',
|
||||||
|
value: status['value']?.toString() ?? '',
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception('Error subscribing to device status: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
|
||||||
|
|
||||||
|
abstract interface class RealtimeDeviceService {
|
||||||
|
Stream<List<Status>> subscribe(String deviceId);
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
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/http_service.dart';
|
||||||
|
|
||||||
|
class RemoteTotalEnergyConsumptionService implements TotalEnergyConsumptionService {
|
||||||
|
const RemoteTotalEnergyConsumptionService(this._httpService);
|
||||||
|
|
||||||
|
final HTTPService _httpService;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<EnergyDataModel>> load(
|
||||||
|
GetTotalEnergyConsumptionParam param,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
final response = await _httpService.get(
|
||||||
|
path: '/power-clamp/historical',
|
||||||
|
showServerMessage: true,
|
||||||
|
queryParameters: param.toJson(),
|
||||||
|
expectedResponseModel: _TotalEnergyConsumptionResponseMapper.map,
|
||||||
|
);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception('Failed to load total energy consumption: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract final class _TotalEnergyConsumptionResponseMapper {
|
||||||
|
const _TotalEnergyConsumptionResponseMapper._();
|
||||||
|
|
||||||
|
static List<EnergyDataModel> map(dynamic data) {
|
||||||
|
final json = data as Map<String, dynamic>? ?? {};
|
||||||
|
final dailyData = json['data'] as List<dynamic>? ?? [];
|
||||||
|
|
||||||
|
return dailyData.map((dayData) {
|
||||||
|
final date = dayData['date'] as String;
|
||||||
|
final energyValue = double.tryParse(
|
||||||
|
dayData['total_energy_consumed_kw'] as String? ?? '0',
|
||||||
|
) ??
|
||||||
|
0.0;
|
||||||
|
|
||||||
|
return EnergyDataModel(
|
||||||
|
date: DateTime.parse(date),
|
||||||
|
value: energyValue,
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
import 'package:syncrow_web/pages/analytics/models/energy_data_model.dart';
|
||||||
|
import 'package:syncrow_web/pages/analytics/params/get_total_energy_consumption_param.dart';
|
||||||
|
|
||||||
|
abstract interface class TotalEnergyConsumptionService {
|
||||||
|
Future<List<EnergyDataModel>> load(
|
||||||
|
GetTotalEnergyConsumptionParam param,
|
||||||
|
);
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user