mirror of
https://github.com/SyncrowIOT/web.git
synced 2025-07-15 09:45:25 +00:00
Compare commits
45 Commits
SP-1448-FE
...
SP-1475-FE
Author | SHA1 | Date | |
---|---|---|---|
06b320a75d | |||
a15b5439f0 | |||
4ded7d5202 | |||
17aad13b2a | |||
a849c1dafb | |||
3e3e17019a | |||
b1bae3cb15 | |||
051bf657ed | |||
5191c1e456 | |||
7a073f10aa | |||
900d47faae | |||
e35a7fdc70 | |||
d80f5e1f3a | |||
c07b53107e | |||
39d125ac7e | |||
ad15d0e138 | |||
e6d272a60d | |||
8dfe8d10d4 | |||
5279020d08 | |||
da481536c4 | |||
f21366268a | |||
c3aef736fd | |||
887ac58f40 | |||
c709477500 | |||
63e7b3faa2 | |||
0e61e52bf8 | |||
7515b347ce | |||
3dfbcb5935 | |||
4fd4a9b5bf | |||
14fa1b355e | |||
78d4e58996 | |||
23b9cb5b78 | |||
401d0a9788 | |||
ac2b0d3fac | |||
3be7a377c0 | |||
e4ee456384 | |||
f02c5d71ba | |||
d45ff262c7 | |||
ad227febc1 | |||
a9d6c6f4ee | |||
4d9e57c8b5 | |||
d1bb8da484 | |||
300f9ae358 | |||
c1dab3400b | |||
46815585cb |
13
lib/pages/analytics/models/analytics_device.dart
Normal file
13
lib/pages/analytics/models/analytics_device.dart
Normal file
@ -0,0 +1,13 @@
|
||||
class AnalyticsDevice {
|
||||
const AnalyticsDevice({required this.name, required this.uuid});
|
||||
|
||||
final String uuid;
|
||||
final String name;
|
||||
|
||||
factory AnalyticsDevice.fromJson(Map<String, dynamic> json) {
|
||||
return AnalyticsDevice(
|
||||
uuid: json['uuid'] as String? ?? '',
|
||||
name: json['name'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
}
|
@ -1,22 +1,28 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class OccupancyHeatMapModel extends Equatable {
|
||||
final DateTime date;
|
||||
final String uuid;
|
||||
|
||||
final int occupancy;
|
||||
final DateTime eventDate;
|
||||
|
||||
final int countTotalPresenceDetected;
|
||||
|
||||
const OccupancyHeatMapModel({
|
||||
required this.date,
|
||||
required this.occupancy,
|
||||
required this.uuid,
|
||||
required this.eventDate,
|
||||
required this.countTotalPresenceDetected,
|
||||
});
|
||||
|
||||
factory OccupancyHeatMapModel.fromJson(Map<String, dynamic> json) {
|
||||
return OccupancyHeatMapModel(
|
||||
date: DateTime.parse(json['date'] as String),
|
||||
occupancy: json['occupancy'] as int,
|
||||
uuid: json['uuid'] as String? ?? '',
|
||||
eventDate: DateTime.parse(
|
||||
json['event_date'] as String? ?? '${DateTime.now()}',
|
||||
),
|
||||
countTotalPresenceDetected: json['count_total_presence_detected'] as int? ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [date, occupancy];
|
||||
List<Object?> get props => [uuid, eventDate, countTotalPresenceDetected];
|
||||
}
|
||||
|
@ -1,27 +1,66 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class PhasesEnergyConsumption extends Equatable {
|
||||
final int month;
|
||||
final double phaseA;
|
||||
final double phaseB;
|
||||
final double phaseC;
|
||||
final String uuid;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
final String deviceUuid;
|
||||
final DateTime date;
|
||||
final double energyConsumedKw;
|
||||
final double energyConsumedA;
|
||||
final double energyConsumedB;
|
||||
final double energyConsumedC;
|
||||
|
||||
const PhasesEnergyConsumption({
|
||||
required this.month,
|
||||
required this.phaseA,
|
||||
required this.phaseB,
|
||||
required this.phaseC,
|
||||
required this.uuid,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.deviceUuid,
|
||||
required this.date,
|
||||
required this.energyConsumedKw,
|
||||
required this.energyConsumedA,
|
||||
required this.energyConsumedB,
|
||||
required this.energyConsumedC,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [month, phaseA, phaseB, phaseC];
|
||||
List<Object?> get props => [
|
||||
uuid,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
deviceUuid,
|
||||
date,
|
||||
energyConsumedKw,
|
||||
energyConsumedA,
|
||||
energyConsumedB,
|
||||
energyConsumedC,
|
||||
];
|
||||
|
||||
factory PhasesEnergyConsumption.fromJson(Map<String, dynamic> json) {
|
||||
return PhasesEnergyConsumption(
|
||||
month: json['month'] as int,
|
||||
phaseA: (json['phaseA'] as num).toDouble(),
|
||||
phaseB: (json['phaseB'] as num).toDouble(),
|
||||
phaseC: (json['phaseC'] as num).toDouble(),
|
||||
uuid: json['uuid'] as String,
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
updatedAt: DateTime.parse(json['updatedAt'] as String),
|
||||
deviceUuid: json['deviceUuid'] as String,
|
||||
date: DateTime.parse(json['date'] as String),
|
||||
energyConsumedKw: double.parse(json['energyConsumedKw']),
|
||||
energyConsumedA: double.parse(json['energyConsumedA']),
|
||||
energyConsumedB: double.parse(json['energyConsumedB']),
|
||||
energyConsumedC: double.parse(json['energyConsumedC']),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'uuid': uuid,
|
||||
'createdAt': createdAt.toIso8601String(),
|
||||
'updatedAt': updatedAt.toIso8601String(),
|
||||
'deviceUuid': deviceUuid,
|
||||
'date': date.toIso8601String().split('T')[0],
|
||||
'energyConsumedKw': energyConsumedKw.toString(),
|
||||
'energyConsumedA': energyConsumedA.toString(),
|
||||
'energyConsumedB': energyConsumedB.toString(),
|
||||
'energyConsumedC': energyConsumedC.toString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,69 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/analytics_device.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/analytics_devices/analytics_devices_service.dart';
|
||||
|
||||
part 'analytics_devices_event.dart';
|
||||
part 'analytics_devices_state.dart';
|
||||
|
||||
class AnalyticsDevicesBloc
|
||||
extends Bloc<AnalyticsDevicesEvent, AnalyticsDevicesState> {
|
||||
AnalyticsDevicesBloc(
|
||||
this._analyticsDevicesService,
|
||||
) : super(const AnalyticsDevicesState()) {
|
||||
on<LoadAnalyticsDevicesEvent>(_onLoadAnalyticsDevices);
|
||||
on<SelectAnalyticsDeviceEvent>(_onSelectAnalyticsDevice);
|
||||
on<ClearAnalyticsDeviceEvent>(_onClearAnalyticsDevice);
|
||||
}
|
||||
final AnalyticsDevicesService _analyticsDevicesService;
|
||||
|
||||
Future<void> _onLoadAnalyticsDevices(
|
||||
LoadAnalyticsDevicesEvent event,
|
||||
Emitter<AnalyticsDevicesState> emit,
|
||||
) async {
|
||||
emit(const AnalyticsDevicesState(status: AnalyticsDevicesStatus.loading));
|
||||
|
||||
try {
|
||||
final devices = await _analyticsDevicesService.getDevices(event.param);
|
||||
emit(
|
||||
AnalyticsDevicesState(
|
||||
status: AnalyticsDevicesStatus.loaded,
|
||||
devices: devices,
|
||||
selectedDevice: devices.firstOrNull,
|
||||
),
|
||||
);
|
||||
if (devices.isNotEmpty) {
|
||||
event.onSuccess(devices.first);
|
||||
}
|
||||
} catch (e) {
|
||||
emit(
|
||||
AnalyticsDevicesState(
|
||||
status: AnalyticsDevicesStatus.failure,
|
||||
errorMessage: e.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onSelectAnalyticsDevice(
|
||||
SelectAnalyticsDeviceEvent event,
|
||||
Emitter<AnalyticsDevicesState> emit,
|
||||
) {
|
||||
emit(
|
||||
AnalyticsDevicesState(
|
||||
selectedDevice: event.device,
|
||||
devices: state.devices,
|
||||
errorMessage: state.errorMessage,
|
||||
status: state.status,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onClearAnalyticsDevice(
|
||||
ClearAnalyticsDeviceEvent event,
|
||||
Emitter<AnalyticsDevicesState> emit,
|
||||
) {
|
||||
emit(const AnalyticsDevicesState());
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
part of 'analytics_devices_bloc.dart';
|
||||
|
||||
sealed class AnalyticsDevicesEvent extends Equatable {
|
||||
const AnalyticsDevicesEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
final class LoadAnalyticsDevicesEvent extends AnalyticsDevicesEvent {
|
||||
const LoadAnalyticsDevicesEvent({required this.param, required this.onSuccess});
|
||||
|
||||
final GetAnalyticsDevicesParam param;
|
||||
final void Function(AnalyticsDevice device) onSuccess;
|
||||
|
||||
@override
|
||||
List<Object> get props => [param];
|
||||
}
|
||||
|
||||
final class SelectAnalyticsDeviceEvent extends AnalyticsDevicesEvent {
|
||||
const SelectAnalyticsDeviceEvent(this.device);
|
||||
|
||||
final AnalyticsDevice device;
|
||||
|
||||
@override
|
||||
List<Object> get props => [device];
|
||||
}
|
||||
|
||||
final class ClearAnalyticsDeviceEvent extends AnalyticsDevicesEvent {
|
||||
const ClearAnalyticsDeviceEvent();
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
part of 'analytics_devices_bloc.dart';
|
||||
|
||||
enum AnalyticsDevicesStatus { initial, loading, loaded, failure }
|
||||
|
||||
final class AnalyticsDevicesState extends Equatable {
|
||||
const AnalyticsDevicesState({
|
||||
this.status = AnalyticsDevicesStatus.initial,
|
||||
this.devices = const [],
|
||||
this.errorMessage,
|
||||
this.selectedDevice,
|
||||
});
|
||||
|
||||
final AnalyticsDevicesStatus status;
|
||||
final List<AnalyticsDevice> devices;
|
||||
final AnalyticsDevice? selectedDevice;
|
||||
final String? errorMessage;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [status, devices, errorMessage, selectedDevice];
|
||||
}
|
@ -14,12 +14,20 @@ class EnergyManagementDataLoadingStrategy implements AnalyticsDataLoadingStrateg
|
||||
CommunityModel community,
|
||||
List<SpaceModel> spaces,
|
||||
) {
|
||||
// Add to space tree bloc first
|
||||
context.read<SpaceTreeBloc>().add(
|
||||
OnCommunitySelected(
|
||||
community.uuid,
|
||||
spaces,
|
||||
),
|
||||
);
|
||||
|
||||
final spaceTreeState = context.read<SpaceTreeBloc>().state;
|
||||
if (spaceTreeState.selectedCommunities.contains(community.uuid)) {
|
||||
clearData(context);
|
||||
return;
|
||||
}
|
||||
|
||||
FetchEnergyManagementDataHelper.loadEnergyManagementData(
|
||||
context,
|
||||
communityId: community.uuid,
|
||||
@ -41,6 +49,13 @@ class EnergyManagementDataLoadingStrategy implements AnalyticsDataLoadingStrateg
|
||||
),
|
||||
);
|
||||
|
||||
final spaceTreeState = context.read<SpaceTreeBloc>().state;
|
||||
if (spaceTreeState.selectedCommunities.contains(community.uuid) ||
|
||||
spaceTreeState.selectedSpaces.contains(space.uuid)) {
|
||||
clearData(context);
|
||||
return;
|
||||
}
|
||||
|
||||
FetchEnergyManagementDataHelper.loadEnergyManagementData(
|
||||
context,
|
||||
communityId: community.uuid,
|
||||
@ -54,7 +69,7 @@ class EnergyManagementDataLoadingStrategy implements AnalyticsDataLoadingStrateg
|
||||
CommunityModel community,
|
||||
SpaceModel child,
|
||||
) {
|
||||
// Do nothing
|
||||
// Do nothing else as per original implementation
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -20,6 +20,12 @@ class OccupancyDataLoadingStrategy implements AnalyticsDataLoadingStrategy {
|
||||
spaces.isNotEmpty ? [spaces.first] : [],
|
||||
),
|
||||
);
|
||||
|
||||
final spaceTreeState = context.read<SpaceTreeBloc>().state;
|
||||
if (spaceTreeState.selectedCommunities.contains(community.uuid)) {
|
||||
clearData(context);
|
||||
return;
|
||||
}
|
||||
FetchOccupancyDataHelper.loadOccupancyData(
|
||||
context,
|
||||
communityId: community.uuid,
|
||||
@ -47,6 +53,13 @@ class OccupancyDataLoadingStrategy implements AnalyticsDataLoadingStrategy {
|
||||
..add(OnSpaceSelected(community, space.uuid ?? '', []));
|
||||
}
|
||||
|
||||
final spaceTreeState = context.read<SpaceTreeBloc>().state;
|
||||
if (spaceTreeState.selectedCommunities.contains(community.uuid) ||
|
||||
spaceTreeState.selectedSpaces.contains(space.uuid)) {
|
||||
clearData(context);
|
||||
return;
|
||||
}
|
||||
|
||||
FetchOccupancyDataHelper.loadOccupancyData(
|
||||
context,
|
||||
communityId: community.uuid,
|
||||
@ -66,6 +79,6 @@ class OccupancyDataLoadingStrategy implements AnalyticsDataLoadingStrategy {
|
||||
@override
|
||||
void clearData(BuildContext context) {
|
||||
context.read<SpaceTreeBloc>().add(const SpaceTreeClearSelectionEvent());
|
||||
// FetchOccupancyDataHelper.clearAllData(context);
|
||||
FetchOccupancyDataHelper.clearAllData(context);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
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/blocs/analytics_devices/analytics_devices_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_tab/analytics_tab_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/analytics_communities_sidebar.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/analytics_page_tabs_and_children.dart';
|
||||
@ -11,10 +12,13 @@ import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/real
|
||||
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/analytics_devices/analytics_devices_service_delagate.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/analytics_devices/remote_energy_management_analytics_devices_service.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/analytics_devices/remote_occupancy_analytics_devices_service.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/energy_consumption_by_phases/remote_energy_consumption_by_phases_service.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/energy_consumption_per_device/remote_energy_consumption_per_device_service.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/occupacy/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/occupancy_heat_map/remote_occupancy_heat_map_service.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/power_clamp_info/remote_power_clamp_info_service.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/realtime_device_service/firebase_realtime_device_service.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/total_energy_consumption/remote_total_energy_consumption_service.dart';
|
||||
@ -23,9 +27,22 @@ import 'package:syncrow_web/services/api/http_service.dart';
|
||||
import 'package:syncrow_web/utils/theme/responsive_text_theme.dart';
|
||||
import 'package:syncrow_web/web_layout/web_scaffold.dart';
|
||||
|
||||
class AnalyticsPage extends StatelessWidget {
|
||||
class AnalyticsPage extends StatefulWidget {
|
||||
const AnalyticsPage({super.key});
|
||||
|
||||
@override
|
||||
State<AnalyticsPage> createState() => _AnalyticsPageState();
|
||||
}
|
||||
|
||||
class _AnalyticsPageState extends State<AnalyticsPage> {
|
||||
late final HTTPService _httpService;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_httpService = HTTPService();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
@ -35,22 +52,22 @@ class AnalyticsPage extends StatelessWidget {
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => TotalEnergyConsumptionBloc(
|
||||
RemoteTotalEnergyConsumptionService(HTTPService()),
|
||||
RemoteTotalEnergyConsumptionService(_httpService),
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => EnergyConsumptionByPhasesBloc(
|
||||
FakeEnergyConsumptionByPhasesService(),
|
||||
RemoteEnergyConsumptionByPhasesService(_httpService),
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => EnergyConsumptionPerDeviceBloc(
|
||||
FakeEnergyConsumptionPerDeviceService(),
|
||||
RemoteEnergyConsumptionPerDeviceService(_httpService),
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => PowerClampInfoBloc(
|
||||
RemotePowerClampInfoService(HTTPService()),
|
||||
RemotePowerClampInfoService(_httpService),
|
||||
),
|
||||
),
|
||||
BlocProvider<RealtimeDeviceChangesBloc>(
|
||||
@ -60,9 +77,19 @@ class AnalyticsPage extends StatelessWidget {
|
||||
),
|
||||
BlocProvider(create: (context) => OccupancyBloc(FakeOccupacyService())),
|
||||
BlocProvider(
|
||||
create: (context) => OccupancyHeatMapBloc(FakeOccupancyHeatMapService()),
|
||||
create: (context) => OccupancyHeatMapBloc(
|
||||
RemoteOccupancyHeatMapService(_httpService),
|
||||
),
|
||||
),
|
||||
BlocProvider(create: (context) => AnalyticsDatePickerBloc()),
|
||||
BlocProvider(
|
||||
create: (context) => AnalyticsDevicesBloc(
|
||||
AnalyticsDevicesServiceDelegate(
|
||||
RemoteOccupancyAnalyticsDevicesService(_httpService),
|
||||
RemoteEnergyManagementAnalyticsDevicesService(_httpService),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: const AnalyticsPageForm(),
|
||||
);
|
||||
|
@ -14,7 +14,6 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final spaceTreeState = context.read<SpaceTreeBloc>().state;
|
||||
return BlocBuilder<AnalyticsTabBloc, AnalyticsPageTab>(
|
||||
buildWhen: (previous, current) => previous != current,
|
||||
builder: (context, selectedTab) => Column(
|
||||
@ -68,7 +67,12 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget {
|
||||
context.read<AnalyticsDatePickerBloc>().add(
|
||||
UpdateAnalyticsDatePickerEvent(montlyDate: value),
|
||||
);
|
||||
FetchEnergyManagementDataHelper.loadEnergyManagementData(
|
||||
|
||||
final spaceTreeState =
|
||||
context.read<SpaceTreeBloc>().state;
|
||||
if (spaceTreeState.selectedSpaces.isNotEmpty) {
|
||||
FetchEnergyManagementDataHelper
|
||||
.loadEnergyManagementData(
|
||||
context,
|
||||
selectedDate: value,
|
||||
communityId:
|
||||
@ -77,6 +81,7 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget {
|
||||
spaceId:
|
||||
spaceTreeState.selectedSpaces.firstOrNull ?? '',
|
||||
);
|
||||
}
|
||||
},
|
||||
selectedDate: context
|
||||
.watch<AnalyticsDatePickerBloc>()
|
||||
|
@ -45,7 +45,7 @@ class _MonthPickerWidgetState extends State<MonthPickerWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
backgroundColor: ColorsManager.whiteColors,
|
||||
child: Container(
|
||||
padding: const EdgeInsetsDirectional.all(20),
|
||||
width: 320,
|
||||
@ -121,6 +121,7 @@ class _MonthPickerWidgetState extends State<MonthPickerWidget> {
|
||||
}
|
||||
|
||||
Row _buildYearSelector() {
|
||||
final currentYear = DateTime.now().year;
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
@ -134,17 +135,35 @@ class _MonthPickerWidgetState extends State<MonthPickerWidget> {
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
onPressed: () => setState(() => _currentYear = _currentYear - 1),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_currentYear = _currentYear - 1;
|
||||
});
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.chevron_left,
|
||||
color: ColorsManager.grey700,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => setState(() => _currentYear = _currentYear + 1),
|
||||
icon: const Icon(
|
||||
onPressed: _currentYear < currentYear
|
||||
? () {
|
||||
setState(() {
|
||||
_currentYear = _currentYear + 1;
|
||||
// Clear selected month if it becomes invalid in the new year
|
||||
if (_currentYear == currentYear &&
|
||||
_selectedMonth != null &&
|
||||
_selectedMonth! > DateTime.now().month - 1) {
|
||||
_selectedMonth = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
: null,
|
||||
icon: Icon(
|
||||
Icons.chevron_right,
|
||||
color: ColorsManager.grey700,
|
||||
color: _currentYear < currentYear
|
||||
? ColorsManager.grey700
|
||||
: ColorsManager.grey700.withValues(alpha: 0.3),
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -152,6 +171,9 @@ class _MonthPickerWidgetState extends State<MonthPickerWidget> {
|
||||
}
|
||||
|
||||
Widget _buildMonthsGrid() {
|
||||
final currentDate = DateTime.now();
|
||||
final isCurrentYear = _currentYear == currentDate.year;
|
||||
|
||||
return GridView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: 12,
|
||||
@ -165,13 +187,17 @@ class _MonthPickerWidgetState extends State<MonthPickerWidget> {
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final isSelected = _selectedMonth == index;
|
||||
final isFutureMonth = isCurrentYear && index > currentDate.month - 1;
|
||||
|
||||
return InkWell(
|
||||
onTap: () => setState(() => _selectedMonth = index),
|
||||
onTap: isFutureMonth ? null : () => setState(() => _selectedMonth = index),
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? ColorsManager.vividBlue.withValues(alpha: 0.7)
|
||||
: isFutureMonth
|
||||
? ColorsManager.grey700.withValues(alpha: 0.1)
|
||||
: const Color(0xFFEDF2F7),
|
||||
borderRadius:
|
||||
isSelected ? BorderRadius.circular(15) : BorderRadius.zero,
|
||||
@ -182,6 +208,8 @@ class _MonthPickerWidgetState extends State<MonthPickerWidget> {
|
||||
fontSize: 12,
|
||||
color: isSelected
|
||||
? ColorsManager.whiteColors
|
||||
: isFutureMonth
|
||||
? ColorsManager.blackColor.withValues(alpha: 0.3)
|
||||
: ColorsManager.blackColor.withValues(alpha: 0.8),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
|
@ -20,9 +20,9 @@ class _YearPickerWidgetState extends State<YearPickerWidget> {
|
||||
late int _currentYear;
|
||||
|
||||
static final years = List.generate(
|
||||
DateTime.now().year - 2020 + 1,
|
||||
DateTime.now().year - (DateTime.now().year - 5) + 1,
|
||||
(index) => (2020 + index),
|
||||
);
|
||||
).where((year) => year <= DateTime.now().year).toList();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -33,7 +33,7 @@ class _YearPickerWidgetState extends State<YearPickerWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
backgroundColor: ColorsManager.whiteColors,
|
||||
child: Container(
|
||||
padding: const EdgeInsetsDirectional.all(20),
|
||||
width: 320,
|
||||
|
@ -1,20 +0,0 @@
|
||||
import 'package:syncrow_web/pages/analytics/models/phases_energy_consumption.dart';
|
||||
|
||||
abstract final class EnergyConsumptionByPhasesChartHelper {
|
||||
const EnergyConsumptionByPhasesChartHelper._();
|
||||
|
||||
static const fakeData = <PhasesEnergyConsumption>[
|
||||
PhasesEnergyConsumption(month: 1, phaseA: 200, phaseB: 300, phaseC: 400),
|
||||
PhasesEnergyConsumption(month: 2, phaseA: 300, phaseB: 400, phaseC: 500),
|
||||
PhasesEnergyConsumption(month: 3, phaseA: 400, phaseB: 500, phaseC: 600),
|
||||
PhasesEnergyConsumption(month: 4, phaseA: 100, phaseB: 100, phaseC: 100),
|
||||
PhasesEnergyConsumption(month: 5, phaseA: 300, phaseB: 400, phaseC: 500),
|
||||
PhasesEnergyConsumption(month: 6, phaseA: 300, phaseB: 100, phaseC: 400),
|
||||
PhasesEnergyConsumption(month: 7, phaseA: 300, phaseB: 100, phaseC: 400),
|
||||
PhasesEnergyConsumption(month: 8, phaseA: 500, phaseB: 100, phaseC: 100),
|
||||
PhasesEnergyConsumption(month: 9, phaseA: 500, phaseB: 100, phaseC: 200),
|
||||
PhasesEnergyConsumption(month: 10, phaseA: 100, phaseB: 50, phaseC: 50),
|
||||
PhasesEnergyConsumption(month: 11, phaseA: 600, phaseB: 750, phaseC: 130),
|
||||
PhasesEnergyConsumption(month: 12, phaseA: 100, phaseB: 80, phaseC: 100),
|
||||
];
|
||||
}
|
@ -20,7 +20,8 @@ abstract final class EnergyManagementChartsHelper {
|
||||
interval: 1,
|
||||
reservedSize: 32,
|
||||
showTitles: true,
|
||||
maxIncluded: true,
|
||||
maxIncluded: false,
|
||||
minIncluded: false,
|
||||
getTitlesWidget: (value, meta) => Padding(
|
||||
padding: const EdgeInsetsDirectional.only(top: 20.0),
|
||||
child: Text(
|
||||
@ -36,7 +37,8 @@ abstract final class EnergyManagementChartsHelper {
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
maxIncluded: true,
|
||||
maxIncluded: false,
|
||||
minIncluded: false,
|
||||
interval: leftTitlesInterval,
|
||||
reservedSize: 110,
|
||||
getTitlesWidget: (value, meta) => Padding(
|
||||
|
@ -1,11 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/analytics_device.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/energy_consumption_by_phases/energy_consumption_by_phases_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/energy_consumption_per_device/energy_consumption_per_device_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/power_clamp_info/power_clamp_info_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/total_energy_consumption/total_energy_consumption_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_energy_consumption_by_phases_param.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_energy_consumption_per_device_param.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_total_energy_consumption_param.dart';
|
||||
@ -13,7 +16,10 @@ import 'package:syncrow_web/pages/analytics/params/get_total_energy_consumption_
|
||||
abstract final class FetchEnergyManagementDataHelper {
|
||||
const FetchEnergyManagementDataHelper._();
|
||||
|
||||
static const String _powerClampId = 'cb71d6ad-6e29-4eaa-ae3e-1a0d1c5f60fa';
|
||||
// static const String _powerClampId = 'cb71d6ad-6e29-4eaa-ae3e-1a0d1c5f60fa';
|
||||
static AnalyticsDevice? getSelectedDevice(BuildContext context) {
|
||||
return context.read<AnalyticsDevicesBloc>().state.selectedDevice;
|
||||
}
|
||||
|
||||
static void loadEnergyManagementData(
|
||||
BuildContext context, {
|
||||
@ -28,28 +34,44 @@ abstract final class FetchEnergyManagementDataHelper {
|
||||
|
||||
final datePickerState = context.read<AnalyticsDatePickerBloc>().state;
|
||||
final selectedDate0 = selectedDate ?? datePickerState.monthlyDate;
|
||||
|
||||
loadAnalyticsDevices(
|
||||
context,
|
||||
communityUuid: communityId,
|
||||
spaceUuid: spaceId,
|
||||
selectedDate: selectedDate0,
|
||||
);
|
||||
loadTotalEnergyConsumption(
|
||||
context,
|
||||
selectedDate: selectedDate0,
|
||||
communityId: communityId,
|
||||
spaceId: spaceId,
|
||||
);
|
||||
|
||||
loadEnergyConsumptionByPhases(context, selectedDate: selectedDate);
|
||||
|
||||
loadEnergyConsumptionPerDevice(context);
|
||||
final selectedDevice = getSelectedDevice(context);
|
||||
if (selectedDevice case final AnalyticsDevice device) {
|
||||
loadEnergyConsumptionByPhases(
|
||||
context,
|
||||
powerClampUuid: device.uuid,
|
||||
selectedDate: selectedDate0,
|
||||
);
|
||||
}
|
||||
loadEnergyConsumptionPerDevice(
|
||||
context,
|
||||
communityId: communityId,
|
||||
spaceId: spaceId,
|
||||
selectedDate: selectedDate0,
|
||||
);
|
||||
loadRealtimeDeviceChanges(context);
|
||||
loadPowerClampInfo(context);
|
||||
}
|
||||
|
||||
static void loadEnergyConsumptionByPhases(
|
||||
BuildContext context, {
|
||||
required String powerClampUuid,
|
||||
DateTime? selectedDate,
|
||||
}) {
|
||||
final param = GetEnergyConsumptionByPhasesParam(
|
||||
startDate: selectedDate,
|
||||
spaceId: '',
|
||||
date: selectedDate,
|
||||
powerClampUuid: powerClampUuid,
|
||||
);
|
||||
context.read<EnergyConsumptionByPhasesBloc>().add(
|
||||
LoadEnergyConsumptionByPhasesEvent(param: param),
|
||||
@ -72,33 +94,80 @@ abstract final class FetchEnergyManagementDataHelper {
|
||||
);
|
||||
}
|
||||
|
||||
static void loadEnergyConsumptionPerDevice(BuildContext context) {
|
||||
const param = GetEnergyConsumptionPerDeviceParam();
|
||||
static void loadEnergyConsumptionPerDevice(
|
||||
BuildContext context, {
|
||||
DateTime? selectedDate,
|
||||
required String communityId,
|
||||
required String spaceId,
|
||||
}) {
|
||||
final param = GetEnergyConsumptionPerDeviceParam(
|
||||
spaceId: spaceId,
|
||||
communityId: communityId,
|
||||
monthDate: selectedDate,
|
||||
);
|
||||
context.read<EnergyConsumptionPerDeviceBloc>().add(
|
||||
const LoadEnergyConsumptionPerDeviceEvent(param),
|
||||
LoadEnergyConsumptionPerDeviceEvent(param),
|
||||
);
|
||||
}
|
||||
|
||||
static void loadPowerClampInfo(BuildContext context) {
|
||||
final selectedDevice = getSelectedDevice(context);
|
||||
if (selectedDevice case final AnalyticsDevice device) {
|
||||
context.read<PowerClampInfoBloc>().add(
|
||||
const LoadPowerClampInfoEvent(_powerClampId),
|
||||
LoadPowerClampInfoEvent(device.uuid),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static void loadRealtimeDeviceChanges(
|
||||
BuildContext context, {
|
||||
String? deviceUuid,
|
||||
}) {
|
||||
final selectedDevice = getSelectedDevice(context);
|
||||
|
||||
context.read<RealtimeDeviceChangesBloc>().add(
|
||||
RealtimeDeviceChangesStarted(deviceUuid ?? selectedDevice?.uuid ?? ''),
|
||||
);
|
||||
}
|
||||
|
||||
static void loadRealtimeDeviceChanges(BuildContext context) {
|
||||
static void loadAnalyticsDevices(
|
||||
BuildContext context, {
|
||||
required String communityUuid,
|
||||
required String spaceUuid,
|
||||
required DateTime selectedDate,
|
||||
}) {
|
||||
context.read<AnalyticsDevicesBloc>().add(
|
||||
LoadAnalyticsDevicesEvent(
|
||||
onSuccess: (device) {
|
||||
context.read<PowerClampInfoBloc>().add(
|
||||
LoadPowerClampInfoEvent(device.uuid),
|
||||
);
|
||||
loadEnergyConsumptionByPhases(
|
||||
context,
|
||||
powerClampUuid: device.uuid,
|
||||
selectedDate: selectedDate,
|
||||
);
|
||||
context.read<RealtimeDeviceChangesBloc>().add(
|
||||
const RealtimeDeviceChangesStarted(_powerClampId),
|
||||
RealtimeDeviceChangesStarted(device.uuid),
|
||||
);
|
||||
},
|
||||
param: GetAnalyticsDevicesParam(
|
||||
communityUuid: communityUuid,
|
||||
spaceUuid: spaceUuid,
|
||||
deviceTypes: ['PC'],
|
||||
requestType: AnalyticsDeviceRequestType.energyManagement,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static void clearAllData(BuildContext context) {
|
||||
context.read<RealtimeDeviceChangesBloc>().add(
|
||||
const RealtimeDeviceChangesClosed(),
|
||||
);
|
||||
|
||||
context.read<PowerClampInfoBloc>().add(
|
||||
const ClearPowerClampInfoEvent(),
|
||||
);
|
||||
context.read<RealtimeDeviceChangesBloc>().add(
|
||||
const RealtimeDeviceChangesClosed(),
|
||||
);
|
||||
|
||||
context.read<EnergyConsumptionPerDeviceBloc>().add(
|
||||
const ClearEnergyConsumptionPerDeviceEvent(),
|
||||
@ -111,5 +180,6 @@ abstract final class FetchEnergyManagementDataHelper {
|
||||
context.read<EnergyConsumptionByPhasesBloc>().add(
|
||||
const ClearEnergyConsumptionByPhasesEvent(),
|
||||
);
|
||||
context.read<AnalyticsDevicesBloc>().add(const ClearAnalyticsDeviceEvent());
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,87 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/analytics_device.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class AnalyticsDeviceDropdown extends StatelessWidget {
|
||||
const AnalyticsDeviceDropdown({required this.onChanged, super.key});
|
||||
|
||||
final ValueChanged<AnalyticsDevice> onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<AnalyticsDevicesBloc, AnalyticsDevicesState>(
|
||||
builder: (context, state) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: ColorsManager.greyColor,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Visibility(
|
||||
visible: state.devices.isNotEmpty,
|
||||
replacement: _buildNoDevicesFound(context),
|
||||
child: _buildDevicesDropdown(context, state),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static const _defaultPadding = EdgeInsetsDirectional.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 2,
|
||||
);
|
||||
|
||||
Widget _buildNoDevicesFound(BuildContext context) {
|
||||
return Padding(
|
||||
padding: _defaultPadding,
|
||||
child: Text(
|
||||
'no devices found',
|
||||
style: _getTextStyle(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDevicesDropdown(BuildContext context, AnalyticsDevicesState state) {
|
||||
return DropdownButton<AnalyticsDevice?>(
|
||||
value: state.selectedDevice,
|
||||
isDense: true,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
dropdownColor: ColorsManager.whiteColors,
|
||||
underline: const SizedBox.shrink(),
|
||||
icon: const RotatedBox(
|
||||
quarterTurns: 1,
|
||||
child: Icon(Icons.chevron_right, size: 16),
|
||||
),
|
||||
style: _getTextStyle(context),
|
||||
padding: _defaultPadding,
|
||||
items: state.devices.map((e) {
|
||||
return DropdownMenuItem(
|
||||
value: e,
|
||||
child: Text(e.name),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
if (value case final AnalyticsDevice device) {
|
||||
context.read<AnalyticsDevicesBloc>().add(
|
||||
SelectAnalyticsDeviceEvent(device),
|
||||
);
|
||||
onChanged.call(device);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
TextStyle? _getTextStyle(BuildContext context) {
|
||||
return context.textTheme.labelSmall?.copyWith(
|
||||
color: ColorsManager.textPrimaryColor,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 14,
|
||||
);
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/analytics/helpers/get_month_name_from_int.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/phases_energy_consumption.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
@ -18,7 +18,10 @@ class EnergyConsumptionByPhasesChart extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return BarChart(
|
||||
BarChartData(
|
||||
gridData: EnergyManagementChartsHelper.gridData(),
|
||||
gridData: EnergyManagementChartsHelper.gridData().copyWith(
|
||||
checkToShowHorizontalLine: (value) => true,
|
||||
horizontalInterval: 250,
|
||||
),
|
||||
borderData: EnergyManagementChartsHelper.borderData(),
|
||||
barTouchData: _barTouchData(context),
|
||||
titlesData: _titlesData(context),
|
||||
@ -31,21 +34,25 @@ class EnergyConsumptionByPhasesChart extends StatelessWidget {
|
||||
barRods: [
|
||||
BarChartRodData(
|
||||
color: ColorsManager.vividBlue.withValues(alpha: 0.1),
|
||||
toY: data.phaseA + data.phaseB + data.phaseC,
|
||||
toY: data.energyConsumedA +
|
||||
data.energyConsumedB +
|
||||
data.energyConsumedC,
|
||||
rodStackItems: [
|
||||
BarChartRodStackItem(
|
||||
0,
|
||||
data.phaseA,
|
||||
data.energyConsumedA,
|
||||
ColorsManager.vividBlue.withValues(alpha: 0.8),
|
||||
),
|
||||
BarChartRodStackItem(
|
||||
data.phaseA,
|
||||
data.phaseA + data.phaseB,
|
||||
data.energyConsumedA,
|
||||
data.energyConsumedA + data.energyConsumedB,
|
||||
ColorsManager.vividBlue.withValues(alpha: 0.4),
|
||||
),
|
||||
BarChartRodStackItem(
|
||||
data.phaseA + data.phaseB,
|
||||
data.phaseA + data.phaseB + data.phaseC,
|
||||
data.energyConsumedA + data.energyConsumedB,
|
||||
data.energyConsumedA +
|
||||
data.energyConsumedB +
|
||||
data.energyConsumedC,
|
||||
ColorsManager.vividBlue.withValues(alpha: 0.15),
|
||||
),
|
||||
],
|
||||
@ -91,18 +98,27 @@ class EnergyConsumptionByPhasesChart extends StatelessWidget {
|
||||
}) {
|
||||
final data = energyData;
|
||||
|
||||
final month = data[group.x.toInt()].month.getMonthName;
|
||||
final phaseA = data[group.x.toInt()].phaseA;
|
||||
final phaseB = data[group.x.toInt()].phaseB;
|
||||
final phaseC = data[group.x.toInt()].phaseC;
|
||||
final date = DateFormat('dd/MM/yyyy').format(data[group.x.toInt()].date);
|
||||
final phaseA = data[group.x.toInt()].energyConsumedA;
|
||||
final phaseB = data[group.x.toInt()].energyConsumedB;
|
||||
final phaseC = data[group.x.toInt()].energyConsumedC;
|
||||
final total = data[group.x.toInt()].energyConsumedKw;
|
||||
|
||||
return BarTooltipItem(
|
||||
'$month\n',
|
||||
'$date\n',
|
||||
context.textTheme.bodyMedium!.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
fontSize: 14,
|
||||
),
|
||||
textAlign: TextAlign.start,
|
||||
children: [
|
||||
TextSpan(
|
||||
text: 'Total: $total\n',
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: 'Phase A: $phaseA\n',
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
@ -144,7 +160,7 @@ class EnergyConsumptionByPhasesChart extends StatelessWidget {
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
getTitlesWidget: (value, _) {
|
||||
final month = energyData[value.toInt()].month.getMonthName;
|
||||
final month = DateFormat('dd/MM').format(energyData[value.toInt()].date);
|
||||
return FittedBox(
|
||||
alignment: AlignmentDirectional.bottomCenter,
|
||||
fit: BoxFit.scaleDown,
|
||||
|
@ -16,7 +16,10 @@ class EnergyConsumptionPerDeviceChart extends StatelessWidget {
|
||||
context,
|
||||
leftTitlesInterval: 250,
|
||||
),
|
||||
gridData: EnergyManagementChartsHelper.gridData(),
|
||||
gridData: EnergyManagementChartsHelper.gridData().copyWith(
|
||||
checkToShowHorizontalLine: (value) => true,
|
||||
horizontalInterval: 250,
|
||||
),
|
||||
borderData: EnergyManagementChartsHelper.borderData(),
|
||||
lineTouchData: EnergyManagementChartsHelper.lineTouchData(),
|
||||
lineBarsData: chartData.map((e) {
|
||||
|
@ -1,55 +0,0 @@
|
||||
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) {},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,10 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/power_clamp_energy_status.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/power_clamp_info/power_clamp_info_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/fetch_energy_management_data_helper.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/analytics_device_dropdown.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/energy_consumption_by_phases_chart_box.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/power_clamp_energy_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';
|
||||
@ -50,7 +53,8 @@ class PowerClampEnergyDataWidget extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
SelectableText(
|
||||
state.powerClampModel?.productUuid ?? 'N/A',
|
||||
context.watch<AnalyticsDevicesBloc>().state.selectedDevice?.uuid ??
|
||||
'N/A',
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
@ -107,7 +111,7 @@ class PowerClampEnergyDataWidget extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
flex: 3,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
@ -122,11 +126,25 @@ class PowerClampEnergyDataWidget extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
const Expanded(
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
child: PowerClampEnergyDataDeviceDropdown(),
|
||||
child: AnalyticsDeviceDropdown(
|
||||
onChanged: (value) {
|
||||
FetchEnergyManagementDataHelper.loadEnergyConsumptionByPhases(
|
||||
context,
|
||||
powerClampUuid: value.uuid,
|
||||
selectedDate:
|
||||
context.read<AnalyticsDatePickerBloc>().state.monthlyDate,
|
||||
);
|
||||
FetchEnergyManagementDataHelper.loadRealtimeDeviceChanges(
|
||||
context,
|
||||
deviceUuid: value.uuid,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -4,15 +4,6 @@ import 'package:syncrow_web/pages/analytics/models/energy_data_model.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
// energy_consumption_chart will return id, name and consumption
|
||||
const phasesJson = {
|
||||
"1": {
|
||||
"phaseOne": 1000,
|
||||
"phaseTwo": 2000,
|
||||
"phaseThree": 3000,
|
||||
}
|
||||
};
|
||||
|
||||
class TotalEnergyConsumptionChart extends StatelessWidget {
|
||||
const TotalEnergyConsumptionChart({required this.chartData, super.key});
|
||||
|
||||
@ -23,8 +14,14 @@ class TotalEnergyConsumptionChart extends StatelessWidget {
|
||||
return Expanded(
|
||||
child: LineChart(
|
||||
LineChartData(
|
||||
titlesData: EnergyManagementChartsHelper.titlesData(context),
|
||||
gridData: EnergyManagementChartsHelper.gridData(),
|
||||
titlesData: EnergyManagementChartsHelper.titlesData(
|
||||
context,
|
||||
leftTitlesInterval: 250,
|
||||
),
|
||||
gridData: EnergyManagementChartsHelper.gridData().copyWith(
|
||||
checkToShowHorizontalLine: (value) => true,
|
||||
horizontalInterval: 250,
|
||||
),
|
||||
borderData: EnergyManagementChartsHelper.borderData(),
|
||||
lineTouchData: EnergyManagementChartsHelper.lineTouchData(),
|
||||
lineBarsData: _lineBarsData,
|
||||
|
@ -1,9 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/analytics_device.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy/occupancy_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy_heat_map/occupancy_heat_map_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_occupancy_heat_map_param.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_occupancy_param.dart';
|
||||
|
||||
@ -17,35 +20,79 @@ abstract final class FetchOccupancyDataHelper {
|
||||
}) {
|
||||
if (communityId.isEmpty && spaceId.isEmpty) {
|
||||
clearAllData(context);
|
||||
return;
|
||||
}
|
||||
|
||||
final datePickerState = context.read<AnalyticsDatePickerBloc>().state;
|
||||
|
||||
context.read<OccupancyBloc>().add(
|
||||
LoadOccupancyEvent(
|
||||
GetOccupancyParam(
|
||||
monthDate:
|
||||
'${datePickerState.monthlyDate.year}-${datePickerState.monthlyDate.month}',
|
||||
spaceUuid: spaceId,
|
||||
loadAnalyticsDevices(context, communityUuid: communityId, spaceUuid: spaceId);
|
||||
final selectedDevice = context.read<AnalyticsDevicesBloc>().state.selectedDevice;
|
||||
|
||||
loadOccupancyChartData(
|
||||
context,
|
||||
communityUuid: communityId,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
context.read<OccupancyHeatMapBloc>().add(
|
||||
LoadOccupancyHeatMapEvent(
|
||||
GetOccupancyHeatMapParam(
|
||||
spaceId: spaceId,
|
||||
communityId: communityId,
|
||||
year: datePickerState.yearlyDate,
|
||||
),
|
||||
),
|
||||
spaceUuid: spaceId,
|
||||
date: datePickerState.monthlyDate,
|
||||
);
|
||||
loadHeatMapData(context, spaceUuid: spaceId, year: datePickerState.yearlyDate);
|
||||
|
||||
if (selectedDevice case final AnalyticsDevice device) {
|
||||
context.read<RealtimeDeviceChangesBloc>()
|
||||
..add(const RealtimeDeviceChangesClosed())
|
||||
..add(
|
||||
const RealtimeDeviceChangesStarted('14fe6e7e-47af-4a07-ae0a-7c4a26ef8135'),
|
||||
RealtimeDeviceChangesStarted(device.uuid),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static void loadHeatMapData(
|
||||
BuildContext context, {
|
||||
required String spaceUuid,
|
||||
required DateTime year,
|
||||
}) {
|
||||
context.read<OccupancyHeatMapBloc>().add(
|
||||
LoadOccupancyHeatMapEvent(
|
||||
GetOccupancyHeatMapParam(spaceUuid: spaceUuid, year: year),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static void loadOccupancyChartData(
|
||||
BuildContext context, {
|
||||
required String communityUuid,
|
||||
required String spaceUuid,
|
||||
required DateTime date,
|
||||
}) {
|
||||
context.read<OccupancyBloc>().add(
|
||||
LoadOccupancyEvent(
|
||||
GetOccupancyParam(
|
||||
monthDate: '${date.year}-${date.month}',
|
||||
spaceUuid: spaceUuid,
|
||||
communityUuid: communityUuid,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static void loadAnalyticsDevices(
|
||||
BuildContext context, {
|
||||
required String communityUuid,
|
||||
required String spaceUuid,
|
||||
}) {
|
||||
context.read<AnalyticsDevicesBloc>().add(
|
||||
LoadAnalyticsDevicesEvent(
|
||||
param: GetAnalyticsDevicesParam(
|
||||
communityUuid: communityUuid,
|
||||
spaceUuid: spaceUuid,
|
||||
deviceTypes: ['WPS', 'CPS'],
|
||||
requestType: AnalyticsDeviceRequestType.occupancy,
|
||||
),
|
||||
onSuccess: (device) {
|
||||
context.read<RealtimeDeviceChangesBloc>()
|
||||
..add(const RealtimeDeviceChangesClosed())
|
||||
..add(RealtimeDeviceChangesStarted(device.uuid));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -59,5 +106,9 @@ abstract final class FetchOccupancyDataHelper {
|
||||
context.read<RealtimeDeviceChangesBloc>().add(
|
||||
const RealtimeDeviceChangesClosed(),
|
||||
);
|
||||
|
||||
context.read<AnalyticsDevicesBloc>().add(
|
||||
const ClearAnalyticsDeviceEvent(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,54 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class HeatMapTooltip extends StatelessWidget {
|
||||
const HeatMapTooltip({
|
||||
required this.date,
|
||||
required this.value,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final DateTime date;
|
||||
final int value;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.topStart,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.grey700,
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
DateFormat('MMM d, yyyy').format(date),
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: ColorsManager.whiteColors,
|
||||
),
|
||||
),
|
||||
const Divider(height: 2, thickness: 1),
|
||||
Text(
|
||||
'$value Occupants',
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: ColorsManager.whiteColors,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,108 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/heat_map_tooltip.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_painter.dart';
|
||||
|
||||
class InteractiveHeatMap extends StatefulWidget {
|
||||
const InteractiveHeatMap({
|
||||
required this.items,
|
||||
required this.maxValue,
|
||||
required this.cellSize,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final List<OccupancyPaintItem> items;
|
||||
final int maxValue;
|
||||
final double cellSize;
|
||||
|
||||
@override
|
||||
State<InteractiveHeatMap> createState() => _InteractiveHeatMapState();
|
||||
}
|
||||
|
||||
class _InteractiveHeatMapState extends State<InteractiveHeatMap> {
|
||||
OccupancyPaintItem? _hoveredItem;
|
||||
OverlayEntry? _overlayEntry;
|
||||
final LayerLink _layerLink = LayerLink();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_removeOverlay();
|
||||
_overlayEntry?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _removeOverlay() {
|
||||
_overlayEntry?.remove();
|
||||
_overlayEntry = null;
|
||||
}
|
||||
|
||||
void _showTooltip(OccupancyPaintItem item, Offset localPosition) {
|
||||
_removeOverlay();
|
||||
|
||||
final column = item.index ~/ 7;
|
||||
final row = item.index % 7;
|
||||
final x = column * widget.cellSize;
|
||||
final y = row * widget.cellSize;
|
||||
|
||||
_overlayEntry = OverlayEntry(
|
||||
builder: (context) => Positioned(
|
||||
child: CompositedTransformFollower(
|
||||
link: _layerLink,
|
||||
offset: Offset(x + widget.cellSize, y),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Transform.translate(
|
||||
offset: Offset(-(widget.cellSize * 2.5), -50),
|
||||
child: HeatMapTooltip(date: item.date, value: item.value),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Overlay.of(context).insert(_overlayEntry!);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CompositedTransformTarget(
|
||||
link: _layerLink,
|
||||
child: MouseRegion(
|
||||
onHover: (event) {
|
||||
final column = event.localPosition.dx ~/ widget.cellSize;
|
||||
final row = event.localPosition.dy ~/ widget.cellSize;
|
||||
final index = column * 7 + row;
|
||||
|
||||
if (index >= 0 && index < widget.items.length) {
|
||||
final item = widget.items[index];
|
||||
if (_hoveredItem != item) {
|
||||
setState(() => _hoveredItem = item);
|
||||
_showTooltip(item, event.localPosition);
|
||||
}
|
||||
} else {
|
||||
_removeOverlay();
|
||||
setState(() => _hoveredItem = null);
|
||||
}
|
||||
},
|
||||
onExit: (_) {
|
||||
_removeOverlay();
|
||||
setState(() => _hoveredItem = null);
|
||||
},
|
||||
child: CustomPaint(
|
||||
isComplex: true,
|
||||
size: _painterSize,
|
||||
painter: OccupancyPainter(
|
||||
items: widget.items,
|
||||
maxValue: widget.maxValue,
|
||||
hoveredItem: _hoveredItem,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Size get _painterSize {
|
||||
final height = 7 * widget.cellSize;
|
||||
final width = widget.items.length ~/ 7 * widget.cellSize;
|
||||
return Size(width, height);
|
||||
}
|
||||
}
|
@ -23,7 +23,14 @@ class OccupancyChart extends StatelessWidget {
|
||||
),
|
||||
borderData: EnergyManagementChartsHelper.borderData(),
|
||||
barTouchData: _barTouchData(context),
|
||||
titlesData: _titlesData(context),
|
||||
titlesData: _titlesData(context).copyWith(
|
||||
leftTitles: _titlesData(context).leftTitles.copyWith(
|
||||
sideTitles: _titlesData(context).leftTitles.sideTitles.copyWith(
|
||||
maxIncluded: true,
|
||||
minIncluded: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
barGroups: List.generate(chartData.length, (index) {
|
||||
final actual = chartData[index];
|
||||
return BarChartGroupData(
|
||||
|
@ -47,11 +47,17 @@ class OccupancyChartBox extends StatelessWidget {
|
||||
context.read<AnalyticsDatePickerBloc>().add(
|
||||
UpdateAnalyticsDatePickerEvent(montlyDate: value),
|
||||
);
|
||||
FetchOccupancyDataHelper.loadOccupancyData(
|
||||
if (spaceTreeState.selectedSpaces.isNotEmpty) {
|
||||
FetchOccupancyDataHelper.loadOccupancyChartData(
|
||||
context,
|
||||
communityId: spaceTreeState.selectedCommunities.firstOrNull ?? '',
|
||||
spaceId: spaceTreeState.selectedSpaces.firstOrNull ?? '',
|
||||
communityUuid:
|
||||
spaceTreeState.selectedCommunities.firstOrNull ??
|
||||
'',
|
||||
spaceUuid:
|
||||
spaceTreeState.selectedSpaces.firstOrNull ?? '',
|
||||
date: value,
|
||||
);
|
||||
}
|
||||
},
|
||||
selectedDate: context
|
||||
.watch<AnalyticsDatePickerBloc>()
|
||||
|
@ -1,15 +1,16 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/power_clamp_energy_status.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/power_clamp_energy_data_device_dropdown.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/fetch_energy_management_data_helper.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/analytics_device_dropdown.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/power_clamp_energy_status_widget.dart';
|
||||
import 'package:syncrow_web/pages/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});
|
||||
@ -37,7 +38,8 @@ class OccupancyEndSideBar extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
SelectableText(
|
||||
(const Uuid().v4()),
|
||||
context.watch<AnalyticsDevicesBloc>().state.selectedDevice?.uuid ??
|
||||
'N/A',
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
@ -105,7 +107,7 @@ class OccupancyEndSideBar extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
flex: 3,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
@ -120,11 +122,18 @@ class OccupancyEndSideBar extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
const Expanded(
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
child: PowerClampEnergyDataDeviceDropdown(),
|
||||
child: AnalyticsDeviceDropdown(
|
||||
onChanged: (value) =>
|
||||
FetchEnergyManagementDataHelper.loadRealtimeDeviceChanges(
|
||||
context,
|
||||
deviceUuid: value.uuid,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'dart:math' as math show max;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/interactive_heat_map.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_days.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_gradient.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_months.dart';
|
||||
@ -15,7 +16,7 @@ class OccupancyHeatMap extends StatelessWidget {
|
||||
static const _totalWeeks = 53;
|
||||
|
||||
int get _maxValue => heatMapData.isNotEmpty
|
||||
? heatMapData.keys.map((key) => heatMapData[key]!).reduce(math.max)
|
||||
? heatMapData.keys.map((key) => heatMapData[key] ?? 0).reduce(math.max)
|
||||
: 0;
|
||||
|
||||
DateTime _getStartingDate() {
|
||||
@ -28,7 +29,7 @@ class OccupancyHeatMap extends StatelessWidget {
|
||||
return List.generate(_totalWeeks * 7, (index) {
|
||||
final date = startDate.add(Duration(days: index));
|
||||
final value = heatMapData[date] ?? 0;
|
||||
return OccupancyPaintItem(index: index, value: value);
|
||||
return OccupancyPaintItem(index: index, value: value, date: date);
|
||||
});
|
||||
}
|
||||
|
||||
@ -58,15 +59,13 @@ class OccupancyHeatMap extends StatelessWidget {
|
||||
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(
|
||||
SizedBox(
|
||||
width: _totalWeeks * _cellSize,
|
||||
height: 7 * _cellSize,
|
||||
child: InteractiveHeatMap(
|
||||
items: paintItems,
|
||||
maxValue: _maxValue,
|
||||
),
|
||||
cellSize: _cellSize,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -47,12 +47,14 @@ class OccupancyHeatMapBox extends StatelessWidget {
|
||||
context.read<AnalyticsDatePickerBloc>().add(
|
||||
UpdateAnalyticsDatePickerEvent(yearlyDate: value),
|
||||
);
|
||||
FetchOccupancyDataHelper.loadOccupancyData(
|
||||
if (spaceTreeState.selectedSpaces.isNotEmpty) {
|
||||
FetchOccupancyDataHelper.loadHeatMapData(
|
||||
context,
|
||||
communityId:
|
||||
spaceTreeState.selectedCommunities.firstOrNull ?? '',
|
||||
spaceId: spaceTreeState.selectedSpaces.firstOrNull ?? '',
|
||||
spaceUuid:
|
||||
spaceTreeState.selectedSpaces.firstOrNull ?? '',
|
||||
year: value,
|
||||
);
|
||||
}
|
||||
},
|
||||
datePickerType: DatePickerType.year,
|
||||
selectedDate: context
|
||||
@ -68,7 +70,10 @@ class OccupancyHeatMapBox extends StatelessWidget {
|
||||
Expanded(
|
||||
child: OccupancyHeatMap(
|
||||
heatMapData: state.heatMapData.asMap().map(
|
||||
(_, value) => MapEntry(value.date, value.occupancy),
|
||||
(_, value) => MapEntry(
|
||||
value.eventDate,
|
||||
value.countTotalPresenceDetected,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -4,18 +4,25 @@ import 'package:syncrow_web/utils/color_manager.dart';
|
||||
class OccupancyPaintItem {
|
||||
final int index;
|
||||
final int value;
|
||||
final DateTime date;
|
||||
|
||||
const OccupancyPaintItem({required this.index, required this.value});
|
||||
const OccupancyPaintItem({
|
||||
required this.index,
|
||||
required this.value,
|
||||
required this.date,
|
||||
});
|
||||
}
|
||||
|
||||
class OccupancyPainter extends CustomPainter {
|
||||
OccupancyPainter({
|
||||
required this.items,
|
||||
required this.maxValue,
|
||||
this.hoveredItem,
|
||||
});
|
||||
|
||||
final List<OccupancyPaintItem> items;
|
||||
final int maxValue;
|
||||
final OccupancyPaintItem? hoveredItem;
|
||||
|
||||
static const double cellSize = 16.0;
|
||||
|
||||
@ -25,6 +32,10 @@ class OccupancyPainter extends CustomPainter {
|
||||
final Paint borderPaint = Paint()
|
||||
..color = ColorsManager.grayBorder.withValues(alpha: 0.4)
|
||||
..style = PaintingStyle.stroke;
|
||||
final Paint hoveredBorderPaint = Paint()
|
||||
..color = Colors.black
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 1.5;
|
||||
|
||||
for (final item in items) {
|
||||
final column = item.index ~/ 7;
|
||||
@ -37,6 +48,10 @@ class OccupancyPainter extends CustomPainter {
|
||||
final rect = Rect.fromLTWH(x, y, cellSize, cellSize);
|
||||
canvas.drawRect(rect, fillPaint);
|
||||
|
||||
// Highlight the hovered item
|
||||
if (hoveredItem != null && hoveredItem!.index == item.index) {
|
||||
canvas.drawRect(rect, hoveredBorderPaint);
|
||||
} else {
|
||||
_drawDashedLine(
|
||||
canvas,
|
||||
Offset(x, y),
|
||||
@ -51,8 +66,9 @@ class OccupancyPainter extends CustomPainter {
|
||||
);
|
||||
|
||||
canvas.drawLine(Offset(x, y), Offset(x, y + cellSize), borderPaint);
|
||||
canvas.drawLine(
|
||||
Offset(x + cellSize, y), Offset(x + cellSize, y + cellSize), borderPaint);
|
||||
canvas.drawLine(Offset(x + cellSize, y), Offset(x + cellSize, y + cellSize),
|
||||
borderPaint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -80,5 +96,6 @@ class OccupancyPainter extends CustomPainter {
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||
bool shouldRepaint(covariant OccupancyPainter oldDelegate) =>
|
||||
oldDelegate.hoveredItem != hoveredItem;
|
||||
}
|
||||
|
22
lib/pages/analytics/params/get_analytics_devices_param.dart
Normal file
22
lib/pages/analytics/params/get_analytics_devices_param.dart
Normal file
@ -0,0 +1,22 @@
|
||||
enum AnalyticsDeviceRequestType { energyManagement, occupancy }
|
||||
|
||||
class GetAnalyticsDevicesParam {
|
||||
final String? spaceUuid;
|
||||
final List<String> deviceTypes;
|
||||
final String? communityUuid;
|
||||
final AnalyticsDeviceRequestType requestType;
|
||||
|
||||
const GetAnalyticsDevicesParam({
|
||||
required this.requestType,
|
||||
required this.spaceUuid,
|
||||
required this.deviceTypes,
|
||||
required this.communityUuid,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
if (spaceUuid != null) 'spaceUuid': spaceUuid,
|
||||
if (communityUuid != null) 'communityUuid': communityUuid,
|
||||
};
|
||||
}
|
||||
}
|
@ -1,24 +1,20 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class GetEnergyConsumptionByPhasesParam extends Equatable {
|
||||
final DateTime? startDate;
|
||||
final DateTime? endDate;
|
||||
final String? spaceId;
|
||||
final String powerClampUuid;
|
||||
final DateTime? date;
|
||||
|
||||
const GetEnergyConsumptionByPhasesParam({
|
||||
this.startDate,
|
||||
this.endDate,
|
||||
this.spaceId,
|
||||
required this.powerClampUuid,
|
||||
this.date,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'startDate': startDate?.toIso8601String(),
|
||||
'endDate': endDate?.toIso8601String(),
|
||||
'spaceId': spaceId,
|
||||
'monthDate': '${date?.year}-${date?.month.toString().padLeft(2, '0')}',
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [startDate, endDate, spaceId];
|
||||
List<Object?> get props => [powerClampUuid, date];
|
||||
}
|
||||
|
@ -1,3 +1,19 @@
|
||||
class GetEnergyConsumptionPerDeviceParam {
|
||||
const GetEnergyConsumptionPerDeviceParam();
|
||||
const GetEnergyConsumptionPerDeviceParam({
|
||||
this.monthDate,
|
||||
this.spaceId,
|
||||
this.communityId,
|
||||
});
|
||||
|
||||
final DateTime? monthDate;
|
||||
final String? spaceId;
|
||||
final String? communityId;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'monthDate':
|
||||
'${monthDate?.year}-${monthDate?.month.toString().padLeft(2, '0')}',
|
||||
if (spaceId == null || spaceId == null) 'spaceUuid': spaceId,
|
||||
'communityUuid': communityId,
|
||||
'groupByDevice': true,
|
||||
};
|
||||
}
|
||||
|
@ -1,19 +1,13 @@
|
||||
class GetOccupancyHeatMapParam {
|
||||
final DateTime year;
|
||||
final String communityId;
|
||||
final String spaceId;
|
||||
final String spaceUuid;
|
||||
|
||||
const GetOccupancyHeatMapParam({
|
||||
required this.year,
|
||||
required this.communityId,
|
||||
required this.spaceId,
|
||||
required this.spaceUuid,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'year': year.toIso8601String(),
|
||||
'communityId': communityId,
|
||||
'spaceId': spaceId,
|
||||
};
|
||||
return {'year': year.year};
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ class GetTotalEnergyConsumptionParam {
|
||||
return {
|
||||
'monthDate':
|
||||
'${monthDate?.year}-${monthDate?.month.toString().padLeft(2, '0')}',
|
||||
if (communityId == null || communityId!.isEmpty) 'spaceUuid': spaceId,
|
||||
if (spaceId == null || spaceId == null) 'spaceUuid': spaceId,
|
||||
'communityUuid': communityId,
|
||||
'groupByDevice': false,
|
||||
};
|
||||
|
@ -0,0 +1,6 @@
|
||||
import 'package:syncrow_web/pages/analytics/models/analytics_device.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart';
|
||||
|
||||
abstract interface class AnalyticsDevicesService {
|
||||
Future<List<AnalyticsDevice>> getDevices(GetAnalyticsDevicesParam param);
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
import 'package:syncrow_web/pages/analytics/models/analytics_device.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/analytics_devices/analytics_devices_service.dart';
|
||||
|
||||
class AnalyticsDevicesServiceDelegate implements AnalyticsDevicesService {
|
||||
const AnalyticsDevicesServiceDelegate(
|
||||
this._occupancyService,
|
||||
this._energyManagementService,
|
||||
);
|
||||
|
||||
final AnalyticsDevicesService _occupancyService;
|
||||
final AnalyticsDevicesService _energyManagementService;
|
||||
|
||||
@override
|
||||
Future<List<AnalyticsDevice>> getDevices(
|
||||
GetAnalyticsDevicesParam param,
|
||||
) {
|
||||
return switch (param.requestType) {
|
||||
AnalyticsDeviceRequestType.occupancy => _occupancyService.getDevices(param),
|
||||
AnalyticsDeviceRequestType.energyManagement =>
|
||||
_energyManagementService.getDevices(param),
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
import 'package:syncrow_web/pages/analytics/models/analytics_device.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/analytics_devices/analytics_devices_service.dart';
|
||||
import 'package:syncrow_web/services/api/http_service.dart';
|
||||
|
||||
final class RemoteEnergyManagementAnalyticsDevicesService
|
||||
implements AnalyticsDevicesService {
|
||||
const RemoteEnergyManagementAnalyticsDevicesService(this._httpService);
|
||||
|
||||
final HTTPService _httpService;
|
||||
|
||||
@override
|
||||
Future<List<AnalyticsDevice>> getDevices(GetAnalyticsDevicesParam param) async {
|
||||
try {
|
||||
final response = await _httpService.get(
|
||||
path: '/devices-space-community/recursive-child',
|
||||
queryParameters: param.toJson()
|
||||
..addAll({'productType': param.deviceTypes.first}),
|
||||
expectedResponseModel: (response) {
|
||||
final json = response as Map<String, dynamic>;
|
||||
final dailyData = json['data'] as List<dynamic>? ?? <dynamic>[];
|
||||
|
||||
final result = dailyData.map(
|
||||
(json) => AnalyticsDevice.fromJson(json as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
return result.toList();
|
||||
},
|
||||
);
|
||||
|
||||
return response;
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load total energy consumption: $e');
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
import 'package:syncrow_web/pages/analytics/models/analytics_device.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/analytics_devices/analytics_devices_service.dart';
|
||||
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
|
||||
import 'package:syncrow_web/services/api/http_service.dart';
|
||||
|
||||
class RemoteOccupancyAnalyticsDevicesService implements AnalyticsDevicesService {
|
||||
const RemoteOccupancyAnalyticsDevicesService(this._httpService);
|
||||
|
||||
final HTTPService _httpService;
|
||||
|
||||
@override
|
||||
Future<List<AnalyticsDevice>> getDevices(GetAnalyticsDevicesParam param) async {
|
||||
try {
|
||||
final requests = await Future.wait<List<AnalyticsDevice>>(
|
||||
param.deviceTypes.map((e) {
|
||||
final mappedParam = GetAnalyticsDevicesParam(
|
||||
requestType: AnalyticsDeviceRequestType.occupancy,
|
||||
spaceUuid: param.spaceUuid,
|
||||
deviceTypes: [e],
|
||||
communityUuid: param.communityUuid,
|
||||
);
|
||||
return _makeRequest(mappedParam);
|
||||
}).toList(),
|
||||
);
|
||||
|
||||
final result = requests.map((e) => e.first).toList();
|
||||
return result;
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load total energy consumption: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<AnalyticsDevice>> _makeRequest(GetAnalyticsDevicesParam param) async {
|
||||
try {
|
||||
final projectUuid = await ProjectManager.getProjectUUID();
|
||||
|
||||
final response = await _httpService.get(
|
||||
path:
|
||||
'/projects/$projectUuid/communities/${param.communityUuid}/spaces/${param.spaceUuid}/devices',
|
||||
queryParameters: {
|
||||
'communityUuid': param.communityUuid,
|
||||
'spaceUuid': param.spaceUuid,
|
||||
'productType': param.deviceTypes.first,
|
||||
},
|
||||
expectedResponseModel: (response) {
|
||||
final json = response as Map<String, dynamic>;
|
||||
final dailyData = json['data'] as List<dynamic>? ?? <dynamic>[];
|
||||
|
||||
final result = dailyData.map(
|
||||
(json) => AnalyticsDevice.fromJson(json as Map<String, dynamic>),
|
||||
);
|
||||
return result.toList();
|
||||
},
|
||||
);
|
||||
return response;
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
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),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -15,8 +15,9 @@ final class RemoteEnergyConsumptionByPhasesService
|
||||
) async {
|
||||
try {
|
||||
final response = await _httpService.get(
|
||||
path: 'endpoint',
|
||||
path: '/power-clamp/${param.powerClampUuid}/historical',
|
||||
showServerMessage: true,
|
||||
queryParameters: param.toJson(),
|
||||
expectedResponseModel: (data) {
|
||||
final json = data as Map<String, dynamic>? ?? {};
|
||||
final mappedData = json['data'] as List<dynamic>? ?? [];
|
||||
@ -28,7 +29,7 @@ final class RemoteEnergyConsumptionByPhasesService
|
||||
);
|
||||
return response;
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load energy consumption per device: $e');
|
||||
throw Exception('Failed to load energy consumption per phase: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
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';
|
||||
import 'package:syncrow_web/services/api/http_service.dart';
|
||||
@ -15,16 +17,10 @@ class RemoteEnergyConsumptionPerDeviceService
|
||||
) async {
|
||||
try {
|
||||
final response = await _httpService.get(
|
||||
path: 'endpoint',
|
||||
path: '/power-clamp/historical',
|
||||
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();
|
||||
},
|
||||
queryParameters: param.toJson(),
|
||||
expectedResponseModel: _EnergyConsumptionPerDeviceMapper.map,
|
||||
);
|
||||
return response;
|
||||
} catch (e) {
|
||||
@ -32,3 +28,33 @@ class RemoteEnergyConsumptionPerDeviceService
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract final class _EnergyConsumptionPerDeviceMapper {
|
||||
const _EnergyConsumptionPerDeviceMapper._();
|
||||
static List<DeviceEnergyDataModel> map(dynamic data) {
|
||||
final json = data as Map<String, dynamic>? ?? {};
|
||||
final mappedData = json['data'] as List<dynamic>? ?? [];
|
||||
return mappedData.where((e) {
|
||||
final deviceData = (e as Map<String, dynamic>)['data'] as List<dynamic>? ?? [];
|
||||
return deviceData.isNotEmpty;
|
||||
}).map((e) {
|
||||
final deviceData = e as Map<String, dynamic>;
|
||||
final energyData = deviceData['data'] as List<dynamic>;
|
||||
|
||||
return DeviceEnergyDataModel(
|
||||
deviceId: deviceData['deviceUuid'] as String,
|
||||
deviceName: deviceData['deviceName'] as String,
|
||||
color: Color((DateTime.now().microsecondsSinceEpoch +
|
||||
deviceData['deviceUuid'].hashCode) |
|
||||
0xFF000000),
|
||||
energy: energyData.map((data) {
|
||||
final energyJson = data as Map<String, dynamic>;
|
||||
return EnergyDataModel(
|
||||
date: DateTime.parse(energyJson['date'] as String),
|
||||
value: double.parse(energyJson['total_energy_consumed_kw'] as String),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
|
@ -1,25 +0,0 @@
|
||||
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,35 @@
|
||||
import 'package:syncrow_web/pages/analytics/models/occupancy_heat_map_model.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_occupancy_heat_map_param.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/occupancy_heat_map/occupancy_heat_map_service.dart';
|
||||
import 'package:syncrow_web/services/api/http_service.dart';
|
||||
|
||||
final class RemoteOccupancyHeatMapService implements OccupancyHeatMapService {
|
||||
const RemoteOccupancyHeatMapService(this._httpService);
|
||||
|
||||
final HTTPService _httpService;
|
||||
|
||||
@override
|
||||
Future<List<OccupancyHeatMapModel>> load(GetOccupancyHeatMapParam param) async {
|
||||
try {
|
||||
final response = await _httpService.get(
|
||||
path: '/occupancy/heat-map/space/${param.spaceUuid}',
|
||||
showServerMessage: true,
|
||||
queryParameters: param.toJson(),
|
||||
expectedResponseModel: (response) {
|
||||
final json = response as Map<String, dynamic>;
|
||||
final dailyData = json['data'] as List<dynamic>? ?? <dynamic>[];
|
||||
|
||||
final result = dailyData.map(
|
||||
(json) => OccupancyHeatMapModel.fromJson(json as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
return result.toList();
|
||||
},
|
||||
);
|
||||
|
||||
return response;
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load total energy consumption:');
|
||||
}
|
||||
}
|
||||
}
|
@ -12,7 +12,7 @@ class AnalyticsErrorWidget extends StatelessWidget {
|
||||
return Visibility(
|
||||
visible: errorMessage != null || (errorMessage?.isNotEmpty ?? false),
|
||||
child: Text(
|
||||
'$errorMessage ?? "Something went wrong"',
|
||||
errorMessage ?? 'Something went wrong',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
|
@ -63,7 +63,8 @@ class _DynamicTableState extends State<DynamicTable> {
|
||||
}
|
||||
}
|
||||
|
||||
bool _compareListOfLists(List<List<dynamic>> oldList, List<List<dynamic>> newList) {
|
||||
bool _compareListOfLists(
|
||||
List<List<dynamic>> oldList, List<List<dynamic>> newList) {
|
||||
// Check if the old and new lists are the same
|
||||
if (oldList.length != newList.length) return false;
|
||||
|
||||
@ -132,14 +133,18 @@ class _DynamicTableState extends State<DynamicTable> {
|
||||
children: [
|
||||
if (widget.withCheckBox) _buildSelectAllCheckbox(),
|
||||
...List.generate(widget.headers.length, (index) {
|
||||
return _buildTableHeaderCell(widget.headers[index], index);
|
||||
return _buildTableHeaderCell(
|
||||
widget.headers[index], index);
|
||||
})
|
||||
//...widget.headers.map((header) => _buildTableHeaderCell(header)),
|
||||
],
|
||||
),
|
||||
),
|
||||
widget.isEmpty
|
||||
? Column(
|
||||
? SizedBox(
|
||||
height: widget.size.height * 0.5,
|
||||
width: widget.size.width,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
@ -153,25 +158,35 @@ class _DynamicTableState extends State<DynamicTable> {
|
||||
height: 15,
|
||||
),
|
||||
Text(
|
||||
widget.tableName == 'AccessManagement' ? 'No Password ' : 'No Devices',
|
||||
widget.tableName == 'AccessManagement'
|
||||
? 'No Password '
|
||||
: 'No Devices',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall!
|
||||
.copyWith(color: ColorsManager.grayColor),
|
||||
.copyWith(
|
||||
color:
|
||||
ColorsManager.grayColor),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: Column(
|
||||
children: List.generate(widget.data.length, (index) {
|
||||
children:
|
||||
List.generate(widget.data.length, (index) {
|
||||
final row = widget.data[index];
|
||||
return Row(
|
||||
children: [
|
||||
if (widget.withCheckBox) _buildRowCheckbox(index, widget.size.height * 0.08),
|
||||
...row.map((cell) => _buildTableCell(cell.toString(), widget.size.height * 0.08)),
|
||||
if (widget.withCheckBox)
|
||||
_buildRowCheckbox(
|
||||
index, widget.size.height * 0.08),
|
||||
...row.map((cell) => _buildTableCell(
|
||||
cell.toString(),
|
||||
widget.size.height * 0.08)),
|
||||
],
|
||||
);
|
||||
}),
|
||||
@ -196,7 +211,9 @@ class _DynamicTableState extends State<DynamicTable> {
|
||||
),
|
||||
child: Checkbox(
|
||||
value: _selectAll,
|
||||
onChanged: widget.withSelectAll && widget.data.isNotEmpty ? _toggleSelectAll : null,
|
||||
onChanged: widget.withSelectAll && widget.data.isNotEmpty
|
||||
? _toggleSelectAll
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -238,7 +255,9 @@ class _DynamicTableState extends State<DynamicTable> {
|
||||
constraints: const BoxConstraints.expand(height: 40),
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: index == widget.headers.length - 1 ? 12 : 8.0, vertical: 4),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: index == widget.headers.length - 1 ? 12 : 8.0,
|
||||
vertical: 4),
|
||||
child: Text(
|
||||
title,
|
||||
style: context.textTheme.titleSmall!.copyWith(
|
||||
|
@ -97,7 +97,8 @@ class DeviceControlDialog extends StatelessWidget with RouteControlsBasedCode {
|
||||
children: [
|
||||
_buildInfoRow('Space Name:',
|
||||
device.spaces?.firstOrNull?.spaceName ?? 'N/A'),
|
||||
_buildInfoRow('Room:', device.subspace?.subspaceName ?? 'N/A'),
|
||||
_buildInfoRow(
|
||||
'Sub space:', device.subspace?.subspaceName ?? 'N/A'),
|
||||
],
|
||||
),
|
||||
TableRow(
|
||||
|
@ -41,7 +41,16 @@ class _UserDropdownMenuState extends State<UserDropdownMenu> {
|
||||
_isDropdownOpen = false;
|
||||
});
|
||||
},
|
||||
child: Transform.rotate(
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(width: 12),
|
||||
if (widget.user != null)
|
||||
Text(
|
||||
'${widget.user!.firstName} ${widget.user!.lastName}',
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Transform.rotate(
|
||||
angle: _isDropdownOpen ? -1.5708 : 1.5708,
|
||||
child: const Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
@ -49,6 +58,8 @@ class _UserDropdownMenuState extends State<UserDropdownMenu> {
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
@ -92,13 +92,6 @@ class DesktopAppBar extends StatelessWidget {
|
||||
if (rightBody != null) rightBody!,
|
||||
const SizedBox(width: 24),
|
||||
_UserAvatar(),
|
||||
const SizedBox(width: 12),
|
||||
if (user != null)
|
||||
Text(
|
||||
'${user.firstName} ${user.lastName}',
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
UserDropdownMenu(user: user),
|
||||
],
|
||||
);
|
||||
@ -146,14 +139,6 @@ class TabletAppBar extends StatelessWidget {
|
||||
if (rightBody != null) rightBody!,
|
||||
const SizedBox(width: 16),
|
||||
_UserAvatar(),
|
||||
if (user != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'${user.firstName} ${user.lastName}',
|
||||
style:
|
||||
Theme.of(context).textTheme.bodyLarge?.copyWith(fontSize: 14),
|
||||
),
|
||||
],
|
||||
UserDropdownMenu(user: user),
|
||||
],
|
||||
);
|
||||
@ -215,14 +200,6 @@ class MobileAppBar extends StatelessWidget {
|
||||
return Row(
|
||||
children: [
|
||||
_UserAvatar(),
|
||||
if (user != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'${user.firstName} ${user.lastName}',
|
||||
style:
|
||||
Theme.of(context).textTheme.bodyLarge?.copyWith(fontSize: 14),
|
||||
),
|
||||
],
|
||||
UserDropdownMenu(user: user),
|
||||
],
|
||||
);
|
||||
|
Reference in New Issue
Block a user