mirror of
https://github.com/SyncrowIOT/web.git
synced 2025-07-10 15:17:31 +00:00
Merge branch 'main' of https://github.com/SyncrowIOT/web
This commit is contained in:
@ -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
|
||||||
|
3
assets/icons/blank_calendar.svg
Normal file
3
assets/icons/blank_calendar.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M16.25 2.1875H14.6875V1.875C14.6875 1.62636 14.5887 1.3879 14.4129 1.21209C14.2371 1.03627 13.9986 0.9375 13.75 0.9375C13.5014 0.9375 13.2629 1.03627 13.0871 1.21209C12.9113 1.3879 12.8125 1.62636 12.8125 1.875V2.1875H7.1875V1.875C7.1875 1.62636 7.08873 1.3879 6.91291 1.21209C6.7371 1.03627 6.49864 0.9375 6.25 0.9375C6.00136 0.9375 5.7629 1.03627 5.58709 1.21209C5.41127 1.3879 5.3125 1.62636 5.3125 1.875V2.1875H3.75C3.3356 2.1875 2.93817 2.35212 2.64515 2.64515C2.35212 2.93817 2.1875 3.3356 2.1875 3.75V16.25C2.1875 16.6644 2.35212 17.0618 2.64515 17.3549C2.93817 17.6479 3.3356 17.8125 3.75 17.8125H16.25C16.6644 17.8125 17.0618 17.6479 17.3549 17.3549C17.6479 17.0618 17.8125 16.6644 17.8125 16.25V3.75C17.8125 3.3356 17.6479 2.93817 17.3549 2.64515C17.0618 2.35212 16.6644 2.1875 16.25 2.1875ZM5.3125 4.0625C5.3125 4.31114 5.41127 4.5496 5.58709 4.72541C5.7629 4.90123 6.00136 5 6.25 5C6.49864 5 6.7371 4.90123 6.91291 4.72541C7.08873 4.5496 7.1875 4.31114 7.1875 4.0625H12.8125C12.8125 4.31114 12.9113 4.5496 13.0871 4.72541C13.2629 4.90123 13.5014 5 13.75 5C13.9986 5 14.2371 4.90123 14.4129 4.72541C14.5887 4.5496 14.6875 4.31114 14.6875 4.0625H15.9375V5.9375H4.0625V4.0625H5.3125ZM4.0625 15.9375V7.8125H15.9375V15.9375H4.0625Z" fill="#475569"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
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];
|
||||||
|
}
|
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,17 @@
|
|||||||
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
part 'analytics_date_picker_event.dart';
|
||||||
|
|
||||||
|
class AnalyticsDatePickerBloc extends Bloc<AnalyticsDatePickerEvent, DateTime> {
|
||||||
|
AnalyticsDatePickerBloc() : super(DateTime.now()) {
|
||||||
|
on<UpdateAnalyticsDatePickerEvent>(_onUpdateAnalyticsDatePickerEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onUpdateAnalyticsDatePickerEvent(
|
||||||
|
UpdateAnalyticsDatePickerEvent event,
|
||||||
|
Emitter<DateTime> emit,
|
||||||
|
) {
|
||||||
|
emit(event.date);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
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.date);
|
||||||
|
|
||||||
|
final DateTime date;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [date];
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:syncrow_web/pages/analytics/modules/analytics/enums/analytics_page_tab.dart';
|
||||||
|
|
||||||
|
part 'analytics_tab_event.dart';
|
||||||
|
|
||||||
|
class AnalyticsTabBloc extends Bloc<AnalyticsTabEvent, AnalyticsPageTab> {
|
||||||
|
AnalyticsTabBloc() : super(AnalyticsPageTab.energyManagement) {
|
||||||
|
on<UpdateAnalyticsTabEvent>(_onUpdateAnalyticsTabEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onUpdateAnalyticsTabEvent(
|
||||||
|
UpdateAnalyticsTabEvent event,
|
||||||
|
Emitter<AnalyticsPageTab> emit,
|
||||||
|
) {
|
||||||
|
emit(event.analyticsTab);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
part of 'analytics_tab_bloc.dart';
|
||||||
|
|
||||||
|
sealed class AnalyticsTabEvent extends Equatable {
|
||||||
|
const AnalyticsTabEvent();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [];
|
||||||
|
}
|
||||||
|
|
||||||
|
class UpdateAnalyticsTabEvent extends AnalyticsTabEvent {
|
||||||
|
const UpdateAnalyticsTabEvent(this.analyticsTab);
|
||||||
|
|
||||||
|
final AnalyticsPageTab analyticsTab;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [analyticsTab];
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
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/occupancy/views/analytics_occupancy_view.dart';
|
||||||
|
|
||||||
|
enum AnalyticsPageTab {
|
||||||
|
energyManagement(
|
||||||
|
title: 'Energy Management',
|
||||||
|
child: AnalyticsEnergyManagementView(),
|
||||||
|
),
|
||||||
|
occupancy(
|
||||||
|
title: 'Occupancy',
|
||||||
|
child: AnalyticsOccupancyView(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const AnalyticsPageTab({
|
||||||
|
required this.title,
|
||||||
|
required this.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Widget child;
|
||||||
|
final String title;
|
||||||
|
}
|
@ -0,0 +1,82 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_tab/analytics_tab_bloc.dart';
|
||||||
|
import 'package:syncrow_web/pages/analytics/modules/analytics/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/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/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/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/fake_total_energy_consumption_service.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/web_layout/web_scaffold.dart';
|
||||||
|
|
||||||
|
class AnalyticsPage extends StatelessWidget {
|
||||||
|
const AnalyticsPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MultiBlocProvider(
|
||||||
|
providers: [
|
||||||
|
BlocProvider<AnalyticsTabBloc>(
|
||||||
|
create: (context) => AnalyticsTabBloc(),
|
||||||
|
),
|
||||||
|
BlocProvider(
|
||||||
|
create: (context) => TotalEnergyConsumptionBloc(
|
||||||
|
FakeTotalEnergyConsumptionService(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
BlocProvider(
|
||||||
|
create: (context) => EnergyConsumptionByPhasesBloc(
|
||||||
|
FakeEnergyConsumptionByPhasesService(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
BlocProvider(
|
||||||
|
create: (context) => EnergyConsumptionPerDeviceBloc(
|
||||||
|
FakeEnergyConsumptionPerDeviceService(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
BlocProvider(
|
||||||
|
create: (context) => PowerClampInfoBloc(
|
||||||
|
RemotePowerClampInfoService(HTTPService()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
BlocProvider<RealtimeDeviceChangesBloc>(
|
||||||
|
create: (context) => RealtimeDeviceChangesBloc(
|
||||||
|
FirebaseRealtimeDeviceService(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: const AnalyticsPageForm(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AnalyticsPageForm extends StatelessWidget {
|
||||||
|
const AnalyticsPageForm({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return WebScaffold(
|
||||||
|
rightBody: const NavigateHomeGridView(),
|
||||||
|
appBarTitle: Text(
|
||||||
|
'Syncrow Analytics',
|
||||||
|
style: ResponsiveTextTheme.of(context).deviceManagementTitle,
|
||||||
|
),
|
||||||
|
enableMenuSidebar: false,
|
||||||
|
scaffoldBody: const Row(
|
||||||
|
children: [
|
||||||
|
AnalyticsCommunitiesSidebar(),
|
||||||
|
Expanded(flex: 5, child: AnalyticsPageTabsAndChildren()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/power_clamp_info/power_clamp_info_bloc.dart';
|
||||||
|
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/fetch_energy_management_data_helper.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_tree/view/space_tree_view.dart';
|
||||||
|
|
||||||
|
class AnalyticsCommunitiesSidebar extends StatelessWidget {
|
||||||
|
const AnalyticsCommunitiesSidebar({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Builder(
|
||||||
|
builder: (context) {
|
||||||
|
return Expanded(
|
||||||
|
child: SpaceTreeView(
|
||||||
|
title: const Text('Communities'),
|
||||||
|
shouldDisableDeselectingChildrenOfSelectedParent: true,
|
||||||
|
onSelect: () {
|
||||||
|
/// Necessary to wait for the state to update before fethcing the data.
|
||||||
|
Future.delayed(
|
||||||
|
const Duration(milliseconds: 100),
|
||||||
|
() {
|
||||||
|
if (context.mounted) {
|
||||||
|
FetchEnergyManagementDataHelper.fetchEnergyManagementData(
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
FetchEnergyManagementDataHelper.loadRealtimeDeviceChanges(
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
context.read<PowerClampInfoBloc>().add(
|
||||||
|
const ClearPowerClampInfoEvent(),
|
||||||
|
);
|
||||||
|
final (selectedCommunities, selectedSpaces) =
|
||||||
|
FetchEnergyManagementDataHelper
|
||||||
|
.getSelectedCommunitiesAndSpaces(context);
|
||||||
|
if (selectedCommunities.isEmpty && selectedSpaces.isEmpty) {
|
||||||
|
context.read<PowerClampInfoBloc>().add(
|
||||||
|
const ClearPowerClampInfoEvent(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
FetchEnergyManagementDataHelper.loadPowerClampInfo(
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
isSide: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,97 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flutter_svg/svg.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart';
|
||||||
|
import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/month_picker_widget.dart';
|
||||||
|
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/fetch_energy_management_data_helper.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||||
|
|
||||||
|
class AnalyticsDateFilterButton extends StatefulWidget {
|
||||||
|
const AnalyticsDateFilterButton({super.key});
|
||||||
|
|
||||||
|
static final _color = ColorsManager.blackColor.withValues(alpha: 0.8);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AnalyticsDateFilterButton> createState() =>
|
||||||
|
_AnalyticsDateFilterButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AnalyticsDateFilterButtonState extends State<AnalyticsDateFilterButton> {
|
||||||
|
late final AnalyticsDatePickerBloc _analyticsDatePickerBloc;
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
_analyticsDatePickerBloc = AnalyticsDatePickerBloc();
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_analyticsDatePickerBloc.close();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider.value(
|
||||||
|
value: _analyticsDatePickerBloc,
|
||||||
|
child: Builder(builder: (context) {
|
||||||
|
final selectedDate = context.watch<AnalyticsDatePickerBloc>().state;
|
||||||
|
return TextButton.icon(
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
foregroundColor: AnalyticsDateFilterButton._color,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
side: const BorderSide(
|
||||||
|
color: ColorsManager.greyColor,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
backgroundColor: ColorsManager.transparentColor,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 32,
|
||||||
|
vertical: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
icon: SvgPicture.asset(
|
||||||
|
Assets.blankCalendar,
|
||||||
|
height: 20,
|
||||||
|
width: 20,
|
||||||
|
colorFilter:
|
||||||
|
ColorFilter.mode(AnalyticsDateFilterButton._color, BlendMode.srcIn),
|
||||||
|
),
|
||||||
|
label: Text(
|
||||||
|
_formatDate(selectedDate),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => MonthPickerWidget(
|
||||||
|
selectedDate: selectedDate,
|
||||||
|
onDateSelected: (value) {
|
||||||
|
_analyticsDatePickerBloc.add(
|
||||||
|
UpdateAnalyticsDatePickerEvent(value),
|
||||||
|
);
|
||||||
|
FetchEnergyManagementDataHelper.fetchEnergyManagementData(
|
||||||
|
context,
|
||||||
|
selectedDate: value,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatDate(DateTime? date) {
|
||||||
|
final formatter = DateFormat('MMMM yyyy');
|
||||||
|
final formattedDate = formatter.format(date ?? DateTime.now());
|
||||||
|
return formattedDate;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_tab/analytics_tab_bloc.dart';
|
||||||
|
import 'package:syncrow_web/pages/analytics/modules/analytics/enums/analytics_page_tab.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
|
||||||
|
class AnalyticsPageTabButton extends StatelessWidget {
|
||||||
|
const AnalyticsPageTabButton({
|
||||||
|
super.key,
|
||||||
|
required this.tab,
|
||||||
|
required this.isSelected,
|
||||||
|
});
|
||||||
|
|
||||||
|
final AnalyticsPageTab tab;
|
||||||
|
final bool isSelected;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return TextButton(
|
||||||
|
onPressed: () => context.read<AnalyticsTabBloc>().add(
|
||||||
|
UpdateAnalyticsTabEvent(tab),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
tab.title,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
maxLines: 1,
|
||||||
|
softWrap: false,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: isSelected ? FontWeight.w700 : FontWeight.w400,
|
||||||
|
fontSize: 16,
|
||||||
|
color:
|
||||||
|
isSelected ? ColorsManager.slidingBlueColor : ColorsManager.textGray,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_tab/analytics_tab_bloc.dart';
|
||||||
|
import 'package:syncrow_web/pages/analytics/modules/analytics/enums/analytics_page_tab.dart';
|
||||||
|
import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/analytics_date_filter_button.dart';
|
||||||
|
import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/analytics_page_tab_button.dart';
|
||||||
|
import 'package:syncrow_web/utils/style.dart';
|
||||||
|
|
||||||
|
class AnalyticsPageTabsAndChildren extends StatelessWidget {
|
||||||
|
const AnalyticsPageTabsAndChildren({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocBuilder<AnalyticsTabBloc, AnalyticsPageTab>(
|
||||||
|
buildWhen: (previous, current) => previous != current,
|
||||||
|
builder: (context, selectedTab) => Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
decoration: subSectionContainerDecoration,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
flex: 4,
|
||||||
|
child: FittedBox(
|
||||||
|
alignment: AlignmentDirectional.centerStart,
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
child: Row(
|
||||||
|
spacing: 32,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
...AnalyticsPageTab.values.map(
|
||||||
|
(tab) => AnimatedSwitcher(
|
||||||
|
switchInCurve: Curves.easeIn,
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
child: AnalyticsPageTabButton(
|
||||||
|
key: ValueKey(selectedTab),
|
||||||
|
tab: tab,
|
||||||
|
isSelected: tab == selectedTab,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
const Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: FittedBox(
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
alignment: AlignmentDirectional.centerEnd,
|
||||||
|
child: AnalyticsDateFilterButton(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
flex: 8,
|
||||||
|
child: AnimatedSwitcher(
|
||||||
|
switchInCurve: Curves.easeIn,
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
child: selectedTab.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,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.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, 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,111 @@
|
|||||||
|
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/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';
|
||||||
|
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
|
||||||
|
|
||||||
|
abstract final class FetchEnergyManagementDataHelper {
|
||||||
|
const FetchEnergyManagementDataHelper._();
|
||||||
|
|
||||||
|
static void fetchEnergyManagementData(
|
||||||
|
BuildContext context, {
|
||||||
|
DateTime? selectedDate,
|
||||||
|
}) {
|
||||||
|
final (selectedCommunities, selectedSpaces) =
|
||||||
|
getSelectedCommunitiesAndSpaces(context);
|
||||||
|
|
||||||
|
if (selectedCommunities.isEmpty && selectedSpaces.isEmpty) {
|
||||||
|
clearAllData(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadTotalEnergyConsumption(context);
|
||||||
|
loadEnergyConsumptionByPhases(context);
|
||||||
|
loadEnergyConsumptionPerDevice(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
static (List<String> selectedCommunities, List<String> selectedSpaces)
|
||||||
|
getSelectedCommunitiesAndSpaces(BuildContext context) {
|
||||||
|
final spaceTreeState = context.read<SpaceTreeBloc>().state;
|
||||||
|
final selectedCommunities = spaceTreeState.selectedCommunities;
|
||||||
|
final selectedSpaces = spaceTreeState.selectedSpaces;
|
||||||
|
|
||||||
|
return (selectedCommunities, selectedSpaces);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
}) {
|
||||||
|
final (selectedCommunities, selectedSpaces) =
|
||||||
|
getSelectedCommunitiesAndSpaces(context);
|
||||||
|
|
||||||
|
final param = GetTotalEnergyConsumptionParam(
|
||||||
|
spaceId: selectedCommunities.firstOrNull,
|
||||||
|
startDate: 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('cb71d6ad-6e29-4eaa-ae3e-1a0d1c5f60fa'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void loadRealtimeDeviceChanges(BuildContext context) {
|
||||||
|
context.read<RealtimeDeviceChangesBloc>().add(
|
||||||
|
const RealtimeDeviceChangesStarted('cb71d6ad-6e29-4eaa-ae3e-1a0d1c5f60fa'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,70 @@
|
|||||||
|
import 'package:flutter/material.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';
|
||||||
|
|
||||||
|
class AnalyticsEnergyManagementView extends StatelessWidget {
|
||||||
|
const AnalyticsEnergyManagementView({super.key});
|
||||||
|
|
||||||
|
static const _padding = EdgeInsetsDirectional.all(32);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return LayoutBuilder(
|
||||||
|
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,36 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
|
||||||
|
class PowerClampEnergyDataDeviceDropdown extends StatelessWidget {
|
||||||
|
const PowerClampEnergyDataDeviceDropdown({super.key});
|
||||||
|
|
||||||
|
static final _color = ColorsManager.blackColor.withValues(alpha: 0.8);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return TextButton(
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
foregroundColor: _color,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
side: const BorderSide(
|
||||||
|
color: ColorsManager.greyColor,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
backgroundColor: ColorsManager.transparentColor,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 32,
|
||||||
|
vertical: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'Device 1',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: () {},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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,80 @@
|
|||||||
|
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,
|
||||||
|
leftTitlesInterval: 5000,
|
||||||
|
),
|
||||||
|
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,12 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class AnalyticsOccupancyView extends StatelessWidget {
|
||||||
|
const AnalyticsOccupancyView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const Center(
|
||||||
|
child: Text('AnalyticsOccupancyView is Working!'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
class GetTotalEnergyConsumptionParam {
|
||||||
|
final DateTime? startDate;
|
||||||
|
final DateTime? endDate;
|
||||||
|
final String? spaceId;
|
||||||
|
|
||||||
|
const GetTotalEnergyConsumptionParam({
|
||||||
|
this.startDate,
|
||||||
|
this.endDate,
|
||||||
|
this.spaceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'startDate': startDate?.toIso8601String(),
|
||||||
|
'endDate': endDate?.toIso8601String(),
|
||||||
|
'spaceId': spaceId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -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,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: num.tryParse(status['value']?.toString() ?? '0'),
|
||||||
|
);
|
||||||
|
}).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,19 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
class FakeTotalEnergyConsumptionService implements TotalEnergyConsumptionService {
|
||||||
|
@override
|
||||||
|
Future<List<EnergyDataModel>> load(
|
||||||
|
GetTotalEnergyConsumptionParam param,
|
||||||
|
) {
|
||||||
|
return Future.value(
|
||||||
|
List.generate(30, (index) {
|
||||||
|
return EnergyDataModel(
|
||||||
|
date: DateTime(2025, 1, index + 1),
|
||||||
|
value: 20000 + (index * 1000) % 5000,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
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: '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 EnergyDataModel.fromJson(jsonData);
|
||||||
|
}).toList();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return response;
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception('Failed to load total energy consumption: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
);
|
||||||
|
}
|
26
lib/pages/analytics/widgets/analytics_error_widget.dart
Normal file
26
lib/pages/analytics/widgets/analytics_error_widget.dart
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
|
|
||||||
|
class AnalyticsErrorWidget extends StatelessWidget {
|
||||||
|
const AnalyticsErrorWidget(this.errorMessage, {super.key});
|
||||||
|
|
||||||
|
final String? errorMessage;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Visibility(
|
||||||
|
visible: errorMessage != null || (errorMessage?.isNotEmpty ?? false),
|
||||||
|
child: Text(
|
||||||
|
'$errorMessage ?? "Something went wrong"',
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: context.textTheme.bodySmall?.copyWith(
|
||||||
|
color: ColorsManager.red,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
fontSize: 8,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
25
lib/pages/analytics/widgets/charts_loading_widget.dart
Normal file
25
lib/pages/analytics/widgets/charts_loading_widget.dart
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class ChartsLoadingWidget extends StatelessWidget {
|
||||||
|
const ChartsLoadingWidget({
|
||||||
|
required this.isLoading,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
final bool isLoading;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Visibility(
|
||||||
|
visible: isLoading,
|
||||||
|
child: const SizedBox.square(
|
||||||
|
dimension: 16,
|
||||||
|
child: FittedBox(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsetsDirectional.only(end: 8),
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,6 @@
|
|||||||
// PowerClampModel class to represent the response
|
// PowerClampModel class to represent the response
|
||||||
|
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
|
||||||
|
|
||||||
class PowerClampModel {
|
class PowerClampModel {
|
||||||
String productUuid;
|
String productUuid;
|
||||||
String productType;
|
String productType;
|
||||||
@ -49,8 +51,24 @@ class PowerStatus {
|
|||||||
phaseA: Phase.fromJson(json['phaseA'] as List<dynamic>? ?? []),
|
phaseA: Phase.fromJson(json['phaseA'] as List<dynamic>? ?? []),
|
||||||
phaseB: Phase.fromJson(json['phaseB'] as List<dynamic>? ?? []),
|
phaseB: Phase.fromJson(json['phaseB'] as List<dynamic>? ?? []),
|
||||||
phaseC: Phase.fromJson(json['phaseC'] as List<dynamic>? ?? []),
|
phaseC: Phase.fromJson(json['phaseC'] as List<dynamic>? ?? []),
|
||||||
general: Phase.fromJson(json['general']as List<dynamic>? ?? []
|
general: Phase.fromJson(json['general'] as List<dynamic>? ?? []),
|
||||||
));
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory PowerStatus.fromStatusList(List<Status> statuses) {
|
||||||
|
List<DataPoint> extractPhase(String prefix) {
|
||||||
|
return statuses
|
||||||
|
.where((s) => s.code.endsWith(prefix))
|
||||||
|
.map((s) => DataPoint(code: s.code, value: s.value))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return PowerStatus(
|
||||||
|
phaseA: Phase(dataPoints: extractPhase('A')),
|
||||||
|
phaseB: Phase(dataPoints: extractPhase('B')),
|
||||||
|
phaseC: Phase(dataPoints: extractPhase('C')),
|
||||||
|
general: Phase(dataPoints: extractPhase('')),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,8 +50,9 @@ class HomeMobilePage extends StatelessWidget {
|
|||||||
height: size.height * 0.6,
|
height: size.height * 0.6,
|
||||||
width: size.width * 0.68,
|
width: size.width * 0.68,
|
||||||
child: GridView.builder(
|
child: GridView.builder(
|
||||||
itemCount: 3,
|
itemCount: homeItems.length,
|
||||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
gridDelegate:
|
||||||
|
const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
crossAxisCount: 2,
|
crossAxisCount: 2,
|
||||||
crossAxisSpacing: 20.0,
|
crossAxisSpacing: 20.0,
|
||||||
mainAxisSpacing: 20.0,
|
mainAxisSpacing: 20.0,
|
||||||
@ -60,10 +61,11 @@ class HomeMobilePage extends StatelessWidget {
|
|||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
return HomeCard(
|
return HomeCard(
|
||||||
index: index,
|
index: index,
|
||||||
active: homeItems[index]['active'],
|
active: homeBloc.homeItems[index].active!,
|
||||||
name: homeItems[index]['title'],
|
name: homeBloc.homeItems[index].title!,
|
||||||
img: homeItems[index]['icon'],
|
img: homeBloc.homeItems[index].icon!,
|
||||||
onTap: () => homeBloc.homeItems[index].onPress(context),
|
onTap: () =>
|
||||||
|
homeBloc.homeItems[index].onPress(context),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -94,6 +96,11 @@ class HomeMobilePage extends StatelessWidget {
|
|||||||
'icon': Assets.devicesIcon,
|
'icon': Assets.devicesIcon,
|
||||||
'active': true,
|
'active': true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'title': 'Syncrow Analytics',
|
||||||
|
'icon': Assets.iconEdit,
|
||||||
|
'active': true,
|
||||||
|
},
|
||||||
// {
|
// {
|
||||||
// 'title': 'Move in',
|
// 'title': 'Move in',
|
||||||
// 'icon': Assets.moveinIcon,
|
// 'icon': Assets.moveinIcon,
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
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:flutter_svg/svg.dart';
|
import 'package:flutter_svg/svg.dart';
|
||||||
import 'package:syncrow_web/pages/home/bloc/home_event.dart';
|
|
||||||
import 'package:syncrow_web/pages/home/view/agreement_and_privacy_dialog.dart';
|
|
||||||
import 'package:syncrow_web/pages/home/bloc/home_bloc.dart';
|
import 'package:syncrow_web/pages/home/bloc/home_bloc.dart';
|
||||||
|
import 'package:syncrow_web/pages/home/bloc/home_event.dart';
|
||||||
import 'package:syncrow_web/pages/home/bloc/home_state.dart';
|
import 'package:syncrow_web/pages/home/bloc/home_state.dart';
|
||||||
|
import 'package:syncrow_web/pages/home/view/agreement_and_privacy_dialog.dart';
|
||||||
import 'package:syncrow_web/pages/home/view/home_card.dart';
|
import 'package:syncrow_web/pages/home/view/home_card.dart';
|
||||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||||
import 'package:syncrow_web/web_layout/web_scaffold.dart';
|
import 'package:syncrow_web/web_layout/web_scaffold.dart';
|
||||||
@ -24,7 +24,7 @@ class _HomeWebPageState extends State<HomeWebPage> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
final homeBloc = BlocProvider.of<HomeBloc>(context);
|
final homeBloc = BlocProvider.of<HomeBloc>(context);
|
||||||
homeBloc.add(FetchUserInfo());
|
homeBloc.add(const FetchUserInfo());
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -97,7 +97,7 @@ class _HomeWebPageState extends State<HomeWebPage> {
|
|||||||
height: size.height * 0.6,
|
height: size.height * 0.6,
|
||||||
width: size.width * 0.68,
|
width: size.width * 0.68,
|
||||||
child: GridView.builder(
|
child: GridView.builder(
|
||||||
itemCount: 3, // Change this count if needed.
|
itemCount: homeBloc.homeItems.length,
|
||||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
crossAxisCount: 3, // Adjust as needed.
|
crossAxisCount: 3, // Adjust as needed.
|
||||||
crossAxisSpacing: 20.0,
|
crossAxisSpacing: 20.0,
|
||||||
|
@ -23,6 +23,7 @@ class SpaceTreeBloc extends Bloc<SpaceTreeEvent, SpaceTreeState> {
|
|||||||
on<OnCommunityUpdated>(_onCommunityUpdate);
|
on<OnCommunityUpdated>(_onCommunityUpdate);
|
||||||
on<PaginationEvent>(_fetchPaginationSpaces);
|
on<PaginationEvent>(_fetchPaginationSpaces);
|
||||||
on<DebouncedSearchEvent>(_onDebouncedSearch);
|
on<DebouncedSearchEvent>(_onDebouncedSearch);
|
||||||
|
on<SpaceTreeClearSelectionEvent>(_onSpaceTreeClearSelectionEvent);
|
||||||
}
|
}
|
||||||
Timer _timer = Timer(const Duration(microseconds: 0), () {});
|
Timer _timer = Timer(const Duration(microseconds: 0), () {});
|
||||||
|
|
||||||
@ -36,8 +37,8 @@ class SpaceTreeBloc extends Bloc<SpaceTreeEvent, SpaceTreeState> {
|
|||||||
final updatedCommunity = event.updatedCommunity;
|
final updatedCommunity = event.updatedCommunity;
|
||||||
final updatedCommunities = List<CommunityModel>.from(state.communityList);
|
final updatedCommunities = List<CommunityModel>.from(state.communityList);
|
||||||
|
|
||||||
final index =
|
final index = updatedCommunities
|
||||||
updatedCommunities.indexWhere((community) => community.uuid == updatedCommunity.uuid);
|
.indexWhere((community) => community.uuid == updatedCommunity.uuid);
|
||||||
|
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
updatedCommunities[index] = updatedCommunity;
|
updatedCommunities[index] = updatedCommunity;
|
||||||
@ -93,8 +94,11 @@ class SpaceTreeBloc extends Bloc<SpaceTreeEvent, SpaceTreeState> {
|
|||||||
if (paginationModel.hasNext && state.searchQuery.isEmpty) {
|
if (paginationModel.hasNext && state.searchQuery.isEmpty) {
|
||||||
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
|
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
|
||||||
|
|
||||||
paginationModel = await CommunitySpaceManagementApi().fetchCommunitiesAndSpaces(
|
paginationModel = await CommunitySpaceManagementApi()
|
||||||
projectId: projectUuid, page: paginationModel.pageNum, search: state.searchQuery);
|
.fetchCommunitiesAndSpaces(
|
||||||
|
projectId: projectUuid,
|
||||||
|
page: paginationModel.pageNum,
|
||||||
|
search: state.searchQuery);
|
||||||
|
|
||||||
communities.addAll(paginationModel.communities);
|
communities.addAll(paginationModel.communities);
|
||||||
}
|
}
|
||||||
@ -107,16 +111,19 @@ class SpaceTreeBloc extends Bloc<SpaceTreeEvent, SpaceTreeState> {
|
|||||||
paginationIsLoading: false));
|
paginationIsLoading: false));
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onCommunityAdded(OnCommunityAdded event, Emitter<SpaceTreeState> emit) async {
|
void _onCommunityAdded(
|
||||||
|
OnCommunityAdded event, Emitter<SpaceTreeState> emit) async {
|
||||||
final updatedCommunities = List<CommunityModel>.from(state.communityList);
|
final updatedCommunities = List<CommunityModel>.from(state.communityList);
|
||||||
updatedCommunities.add(event.newCommunity);
|
updatedCommunities.add(event.newCommunity);
|
||||||
|
|
||||||
emit(state.copyWith(communitiesList: updatedCommunities));
|
emit(state.copyWith(communitiesList: updatedCommunities));
|
||||||
}
|
}
|
||||||
|
|
||||||
_onCommunityExpanded(OnCommunityExpanded event, Emitter<SpaceTreeState> emit) async {
|
_onCommunityExpanded(
|
||||||
|
OnCommunityExpanded event, Emitter<SpaceTreeState> emit) async {
|
||||||
try {
|
try {
|
||||||
List<String> updatedExpandedCommunityList = List.from(state.expandedCommunities);
|
List<String> updatedExpandedCommunityList =
|
||||||
|
List.from(state.expandedCommunities);
|
||||||
|
|
||||||
if (updatedExpandedCommunityList.contains(event.communityId)) {
|
if (updatedExpandedCommunityList.contains(event.communityId)) {
|
||||||
updatedExpandedCommunityList.remove(event.communityId);
|
updatedExpandedCommunityList.remove(event.communityId);
|
||||||
@ -148,14 +155,18 @@ class SpaceTreeBloc extends Bloc<SpaceTreeEvent, SpaceTreeState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onCommunitySelected(OnCommunitySelected event, Emitter<SpaceTreeState> emit) async {
|
_onCommunitySelected(
|
||||||
|
OnCommunitySelected event, Emitter<SpaceTreeState> emit) async {
|
||||||
try {
|
try {
|
||||||
List<String> updatedSelectedCommunities =
|
List<String> updatedSelectedCommunities =
|
||||||
List.from(state.selectedCommunities.toSet().toList());
|
List.from(state.selectedCommunities.toSet().toList());
|
||||||
List<String> updatedSelectedSpaces = List.from(state.selectedSpaces.toSet().toList());
|
List<String> updatedSelectedSpaces =
|
||||||
|
List.from(state.selectedSpaces.toSet().toList());
|
||||||
List<String> updatedSoldChecks = List.from(state.soldCheck.toSet().toList());
|
List<String> updatedSoldChecks = List.from(state.soldCheck.toSet().toList());
|
||||||
Map<String, List<String>> communityAndSpaces = Map.from(state.selectedCommunityAndSpaces);
|
Map<String, List<String>> communityAndSpaces =
|
||||||
List<String> selectedSpacesInCommunity = communityAndSpaces[event.communityId] ?? [];
|
Map.from(state.selectedCommunityAndSpaces);
|
||||||
|
List<String> selectedSpacesInCommunity =
|
||||||
|
communityAndSpaces[event.communityId] ?? [];
|
||||||
|
|
||||||
List<String> childrenIds = _getAllChildIds(event.children);
|
List<String> childrenIds = _getAllChildIds(event.children);
|
||||||
|
|
||||||
@ -188,11 +199,14 @@ class SpaceTreeBloc extends Bloc<SpaceTreeEvent, SpaceTreeState> {
|
|||||||
try {
|
try {
|
||||||
List<String> updatedSelectedCommunities =
|
List<String> updatedSelectedCommunities =
|
||||||
List.from(state.selectedCommunities.toSet().toList());
|
List.from(state.selectedCommunities.toSet().toList());
|
||||||
List<String> updatedSelectedSpaces = List.from(state.selectedSpaces.toSet().toList());
|
List<String> updatedSelectedSpaces =
|
||||||
|
List.from(state.selectedSpaces.toSet().toList());
|
||||||
List<String> updatedSoldChecks = List.from(state.soldCheck.toSet().toList());
|
List<String> updatedSoldChecks = List.from(state.soldCheck.toSet().toList());
|
||||||
Map<String, List<String>> communityAndSpaces = Map.from(state.selectedCommunityAndSpaces);
|
Map<String, List<String>> communityAndSpaces =
|
||||||
|
Map.from(state.selectedCommunityAndSpaces);
|
||||||
|
|
||||||
List<String> selectedSpacesInCommunity = communityAndSpaces[event.communityModel.uuid] ?? [];
|
List<String> selectedSpacesInCommunity =
|
||||||
|
communityAndSpaces[event.communityModel.uuid] ?? [];
|
||||||
|
|
||||||
List<String> childrenIds = _getAllChildIds(event.children);
|
List<String> childrenIds = _getAllChildIds(event.children);
|
||||||
bool isChildSelected = false;
|
bool isChildSelected = false;
|
||||||
@ -215,9 +229,11 @@ class SpaceTreeBloc extends Bloc<SpaceTreeEvent, SpaceTreeState> {
|
|||||||
selectedSpacesInCommunity.addAll(childrenIds);
|
selectedSpacesInCommunity.addAll(childrenIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> spaces = _getThePathToChild(event.communityModel.uuid, event.spaceId);
|
List<String> spaces =
|
||||||
|
_getThePathToChild(event.communityModel.uuid, event.spaceId);
|
||||||
for (String space in spaces) {
|
for (String space in spaces) {
|
||||||
if (!updatedSelectedSpaces.contains(space) && !updatedSoldChecks.contains(space)) {
|
if (!updatedSelectedSpaces.contains(space) &&
|
||||||
|
!updatedSoldChecks.contains(space)) {
|
||||||
updatedSoldChecks.add(space);
|
updatedSoldChecks.add(space);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -240,7 +256,9 @@ class SpaceTreeBloc extends Bloc<SpaceTreeEvent, SpaceTreeState> {
|
|||||||
updatedSoldChecks.remove(event.spaceId);
|
updatedSoldChecks.remove(event.spaceId);
|
||||||
|
|
||||||
List<String> parents =
|
List<String> parents =
|
||||||
_getThePathToChild(event.communityModel.uuid, event.spaceId).toSet().toList();
|
_getThePathToChild(event.communityModel.uuid, event.spaceId)
|
||||||
|
.toSet()
|
||||||
|
.toList();
|
||||||
|
|
||||||
if (updatedSelectedSpaces.isEmpty) {
|
if (updatedSelectedSpaces.isEmpty) {
|
||||||
updatedSoldChecks.removeWhere(parents.contains);
|
updatedSoldChecks.removeWhere(parents.contains);
|
||||||
@ -248,7 +266,8 @@ class SpaceTreeBloc extends Bloc<SpaceTreeEvent, SpaceTreeState> {
|
|||||||
} else {
|
} else {
|
||||||
// Check if any parent has selected children
|
// Check if any parent has selected children
|
||||||
for (String space in parents) {
|
for (String space in parents) {
|
||||||
if (!_noChildrenSelected(event.communityModel, space, updatedSelectedSpaces, parents)) {
|
if (!_noChildrenSelected(
|
||||||
|
event.communityModel, space, updatedSelectedSpaces, parents)) {
|
||||||
updatedSoldChecks.remove(space);
|
updatedSoldChecks.remove(space);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -273,8 +292,8 @@ class SpaceTreeBloc extends Bloc<SpaceTreeEvent, SpaceTreeState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_noChildrenSelected(
|
_noChildrenSelected(CommunityModel community, String spaceId,
|
||||||
CommunityModel community, String spaceId, List<String> selectedSpaces, List<String> parents) {
|
List<String> selectedSpaces, List<String> parents) {
|
||||||
if (selectedSpaces.contains(spaceId)) {
|
if (selectedSpaces.contains(spaceId)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -300,7 +319,8 @@ class SpaceTreeBloc extends Bloc<SpaceTreeEvent, SpaceTreeState> {
|
|||||||
if (_timer.isActive) {
|
if (_timer.isActive) {
|
||||||
_timer.cancel(); // clear timer
|
_timer.cancel(); // clear timer
|
||||||
}
|
}
|
||||||
_timer = Timer(duration, () async => add(DebouncedSearchEvent(event.searchQuery)));
|
_timer =
|
||||||
|
Timer(duration, () async => add(DebouncedSearchEvent(event.searchQuery)));
|
||||||
|
|
||||||
// List<CommunityModel> communities = List.from(state.communityList);
|
// List<CommunityModel> communities = List.from(state.communityList);
|
||||||
// List<CommunityModel> filteredCommunity = [];
|
// List<CommunityModel> filteredCommunity = [];
|
||||||
@ -324,7 +344,8 @@ class SpaceTreeBloc extends Bloc<SpaceTreeEvent, SpaceTreeState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onDebouncedSearch(DebouncedSearchEvent event, Emitter<SpaceTreeState> emit) async {
|
_onDebouncedSearch(
|
||||||
|
DebouncedSearchEvent event, Emitter<SpaceTreeState> emit) async {
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
isSearching: true,
|
isSearching: true,
|
||||||
));
|
));
|
||||||
@ -333,7 +354,8 @@ class SpaceTreeBloc extends Bloc<SpaceTreeEvent, SpaceTreeState> {
|
|||||||
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
|
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
|
||||||
|
|
||||||
paginationModel = await CommunitySpaceManagementApi()
|
paginationModel = await CommunitySpaceManagementApi()
|
||||||
.fetchCommunitiesAndSpaces(projectId: projectUuid, page: 1, search: event.searchQuery);
|
.fetchCommunitiesAndSpaces(
|
||||||
|
projectId: projectUuid, page: 1, search: event.searchQuery);
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
@ -405,8 +427,8 @@ class SpaceTreeBloc extends Bloc<SpaceTreeEvent, SpaceTreeState> {
|
|||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _anySpacesSelectedInCommunity(
|
bool _anySpacesSelectedInCommunity(CommunityModel community,
|
||||||
CommunityModel community, List<String> selectedSpaces, List<String> partialCheckedList) {
|
List<String> selectedSpaces, List<String> partialCheckedList) {
|
||||||
bool result = false;
|
bool result = false;
|
||||||
List<String> ids = _getAllChildIds(community.spaces);
|
List<String> ids = _getAllChildIds(community.spaces);
|
||||||
for (var id in ids) {
|
for (var id in ids) {
|
||||||
@ -435,7 +457,8 @@ class SpaceTreeBloc extends Bloc<SpaceTreeEvent, SpaceTreeState> {
|
|||||||
return ids;
|
return ids;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> _getAllParentsIds(SpaceModel child, String spaceId, List<String> listIds) {
|
List<String> _getAllParentsIds(
|
||||||
|
SpaceModel child, String spaceId, List<String> listIds) {
|
||||||
List<String> ids = listIds;
|
List<String> ids = listIds;
|
||||||
|
|
||||||
ids.add(child.uuid ?? '');
|
ids.add(child.uuid ?? '');
|
||||||
@ -457,6 +480,19 @@ class SpaceTreeBloc extends Bloc<SpaceTreeEvent, SpaceTreeState> {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onSpaceTreeClearSelectionEvent(
|
||||||
|
SpaceTreeClearSelectionEvent event,
|
||||||
|
Emitter<SpaceTreeState> emit,
|
||||||
|
) async {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
selectedCommunities: [],
|
||||||
|
selectedCommunityAndSpaces: {},
|
||||||
|
selectedSpaces: [],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> close() async {
|
Future<void> close() async {
|
||||||
_timer.cancel();
|
_timer.cancel();
|
||||||
|
@ -108,3 +108,7 @@ class OnCommunityUpdated extends SpaceTreeEvent {
|
|||||||
class ClearAllData extends SpaceTreeEvent {}
|
class ClearAllData extends SpaceTreeEvent {}
|
||||||
|
|
||||||
class ClearCachedData extends SpaceTreeEvent {}
|
class ClearCachedData extends SpaceTreeEvent {}
|
||||||
|
|
||||||
|
class SpaceTreeClearSelectionEvent extends SpaceTreeEvent {
|
||||||
|
const SpaceTreeClearSelectionEvent();
|
||||||
|
}
|
||||||
|
@ -17,7 +17,15 @@ import 'package:syncrow_web/utils/style.dart';
|
|||||||
class SpaceTreeView extends StatefulWidget {
|
class SpaceTreeView extends StatefulWidget {
|
||||||
final bool? isSide;
|
final bool? isSide;
|
||||||
final Function onSelect;
|
final Function onSelect;
|
||||||
const SpaceTreeView({required this.onSelect, this.isSide, super.key});
|
final bool shouldDisableDeselectingChildrenOfSelectedParent;
|
||||||
|
final Widget? title;
|
||||||
|
const SpaceTreeView({
|
||||||
|
required this.onSelect,
|
||||||
|
this.isSide,
|
||||||
|
super.key,
|
||||||
|
this.shouldDisableDeselectingChildrenOfSelectedParent = false,
|
||||||
|
this.title,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<SpaceTreeView> createState() => _SpaceTreeViewState();
|
State<SpaceTreeView> createState() => _SpaceTreeViewState();
|
||||||
@ -41,17 +49,31 @@ class _SpaceTreeViewState extends State<SpaceTreeView> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocBuilder<SpaceTreeBloc, SpaceTreeState>(builder: (context, state) {
|
return BlocBuilder<SpaceTreeBloc, SpaceTreeState>(builder: (context, state) {
|
||||||
final communities =
|
final communities = state.searchQuery.isNotEmpty
|
||||||
state.searchQuery.isNotEmpty ? state.filteredCommunity : state.communityList;
|
? state.filteredCommunity
|
||||||
|
: state.communityList;
|
||||||
return Container(
|
return Container(
|
||||||
height: MediaQuery.sizeOf(context).height,
|
height: MediaQuery.sizeOf(context).height,
|
||||||
decoration: widget.isSide == true
|
decoration: widget.isSide == true
|
||||||
? subSectionContainerDecoration.copyWith(color: ColorsManager.whiteColors)
|
? subSectionContainerDecoration.copyWith(
|
||||||
|
color: ColorsManager.whiteColors)
|
||||||
: const BoxDecoration(color: ColorsManager.whiteColors),
|
: const BoxDecoration(color: ColorsManager.whiteColors),
|
||||||
child: state is SpaceTreeLoadingState
|
child: state is SpaceTreeLoadingState
|
||||||
? const Center(child: CircularProgressIndicator())
|
? const Center(child: CircularProgressIndicator())
|
||||||
: Column(
|
: Column(
|
||||||
children: [
|
children: [
|
||||||
|
if (widget.title != null)
|
||||||
|
Container(
|
||||||
|
alignment: AlignmentDirectional.centerStart,
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: DefaultTextStyle(
|
||||||
|
style: context.textTheme.titleMedium!.copyWith(
|
||||||
|
color: ColorsManager.blackColor,
|
||||||
|
fontSize: 20,
|
||||||
|
),
|
||||||
|
child: widget.title!,
|
||||||
|
),
|
||||||
|
),
|
||||||
if (widget.isSide == true)
|
if (widget.isSide == true)
|
||||||
Container(
|
Container(
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
@ -79,10 +101,12 @@ class _SpaceTreeViewState extends State<SpaceTreeView> {
|
|||||||
style: context.textTheme.bodyMedium?.copyWith(
|
style: context.textTheme.bodyMedium?.copyWith(
|
||||||
color: ColorsManager.blackColor,
|
color: ColorsManager.blackColor,
|
||||||
),
|
),
|
||||||
onChanged: (value) => context.read<SpaceTreeBloc>().add(
|
onChanged: (value) =>
|
||||||
|
context.read<SpaceTreeBloc>().add(
|
||||||
SearchQueryEvent(value),
|
SearchQueryEvent(value),
|
||||||
),
|
),
|
||||||
decoration: textBoxDecoration(radios: 20)?.copyWith(
|
decoration:
|
||||||
|
textBoxDecoration(radios: 20)?.copyWith(
|
||||||
fillColor: Colors.white,
|
fillColor: Colors.white,
|
||||||
suffixIcon: Padding(
|
suffixIcon: Padding(
|
||||||
padding: const EdgeInsets.only(right: 16),
|
padding: const EdgeInsets.only(right: 16),
|
||||||
@ -92,7 +116,8 @@ class _SpaceTreeViewState extends State<SpaceTreeView> {
|
|||||||
height: 24,
|
height: 24,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
hintStyle: context.textTheme.bodyMedium?.copyWith(
|
hintStyle:
|
||||||
|
context.textTheme.bodyMedium?.copyWith(
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: ColorsManager.textGray,
|
color: ColorsManager.textGray,
|
||||||
@ -131,11 +156,12 @@ class _SpaceTreeViewState extends State<SpaceTreeView> {
|
|||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
return CustomExpansionTileSpaceTree(
|
return CustomExpansionTileSpaceTree(
|
||||||
title: communities[index].name,
|
title: communities[index].name,
|
||||||
isSelected:
|
isSelected: state.selectedCommunities
|
||||||
state.selectedCommunities.contains(communities[index].uuid),
|
.contains(communities[index].uuid),
|
||||||
isSoldCheck:
|
isSoldCheck: state.selectedCommunities
|
||||||
state.selectedCommunities.contains(communities[index].uuid),
|
.contains(communities[index].uuid),
|
||||||
onExpansionChanged: () => context.read<SpaceTreeBloc>().add(
|
onExpansionChanged: () =>
|
||||||
|
context.read<SpaceTreeBloc>().add(
|
||||||
OnCommunityExpanded(
|
OnCommunityExpanded(
|
||||||
communities[index].uuid,
|
communities[index].uuid,
|
||||||
),
|
),
|
||||||
@ -144,20 +170,32 @@ class _SpaceTreeViewState extends State<SpaceTreeView> {
|
|||||||
communities[index].uuid,
|
communities[index].uuid,
|
||||||
),
|
),
|
||||||
onItemSelected: () {
|
onItemSelected: () {
|
||||||
|
widget.onSelect();
|
||||||
context.read<SpaceTreeBloc>().add(
|
context.read<SpaceTreeBloc>().add(
|
||||||
OnCommunitySelected(
|
OnCommunitySelected(
|
||||||
communities[index].uuid,
|
communities[index].uuid,
|
||||||
communities[index].spaces,
|
communities[index].spaces,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
widget.onSelect();
|
|
||||||
},
|
},
|
||||||
children: communities[index].spaces.map(
|
children: communities[index].spaces.map(
|
||||||
(space) {
|
(space) {
|
||||||
return CustomExpansionTileSpaceTree(
|
return CustomExpansionTileSpaceTree(
|
||||||
title: space.name,
|
title: space.name,
|
||||||
isExpanded: state.expandedSpaces.contains(space.uuid),
|
isExpanded:
|
||||||
|
state.expandedSpaces.contains(space.uuid),
|
||||||
onItemSelected: () {
|
onItemSelected: () {
|
||||||
|
final isParentSelected = _isParentSelected(
|
||||||
|
state,
|
||||||
|
communities[index],
|
||||||
|
space,
|
||||||
|
);
|
||||||
|
if (widget
|
||||||
|
.shouldDisableDeselectingChildrenOfSelectedParent &&
|
||||||
|
isParentSelected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
widget.onSelect();
|
||||||
context.read<SpaceTreeBloc>().add(
|
context.read<SpaceTreeBloc>().add(
|
||||||
OnSpaceSelected(
|
OnSpaceSelected(
|
||||||
communities[index],
|
communities[index],
|
||||||
@ -165,17 +203,19 @@ class _SpaceTreeViewState extends State<SpaceTreeView> {
|
|||||||
space.children,
|
space.children,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
widget.onSelect();
|
|
||||||
},
|
},
|
||||||
onExpansionChanged: () => context.read<SpaceTreeBloc>().add(
|
onExpansionChanged: () =>
|
||||||
|
context.read<SpaceTreeBloc>().add(
|
||||||
OnSpaceExpanded(
|
OnSpaceExpanded(
|
||||||
communities[index].uuid,
|
communities[index].uuid,
|
||||||
space.uuid ?? '',
|
space.uuid ?? '',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
isSelected: state.selectedSpaces.contains(space.uuid) ||
|
isSelected: state.selectedSpaces
|
||||||
|
.contains(space.uuid) ||
|
||||||
|
state.soldCheck.contains(space.uuid),
|
||||||
|
isSoldCheck:
|
||||||
state.soldCheck.contains(space.uuid),
|
state.soldCheck.contains(space.uuid),
|
||||||
isSoldCheck: state.soldCheck.contains(space.uuid),
|
|
||||||
children: _buildNestedSpaces(
|
children: _buildNestedSpaces(
|
||||||
context,
|
context,
|
||||||
state,
|
state,
|
||||||
@ -196,6 +236,13 @@ class _SpaceTreeViewState extends State<SpaceTreeView> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _isParentSelected(
|
||||||
|
SpaceTreeState state, CommunityModel community, SpaceModel space) {
|
||||||
|
return state.selectedCommunities.contains(community.uuid) ||
|
||||||
|
(space.spaceModel?.uuid != null &&
|
||||||
|
state.selectedSpaces.contains(space.spaceModel?.uuid));
|
||||||
|
}
|
||||||
|
|
||||||
List<Widget> _buildNestedSpaces(
|
List<Widget> _buildNestedSpaces(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
SpaceTreeState state,
|
SpaceTreeState state,
|
||||||
@ -204,8 +251,8 @@ class _SpaceTreeViewState extends State<SpaceTreeView> {
|
|||||||
) {
|
) {
|
||||||
return space.children.map((child) {
|
return space.children.map((child) {
|
||||||
return CustomExpansionTileSpaceTree(
|
return CustomExpansionTileSpaceTree(
|
||||||
isSelected:
|
isSelected: state.selectedSpaces.contains(child.uuid) ||
|
||||||
state.selectedSpaces.contains(child.uuid) || state.soldCheck.contains(child.uuid),
|
state.soldCheck.contains(child.uuid),
|
||||||
isSoldCheck: state.soldCheck.contains(child.uuid),
|
isSoldCheck: state.soldCheck.contains(child.uuid),
|
||||||
title: child.name,
|
title: child.name,
|
||||||
isExpanded: state.expandedSpaces.contains(child.uuid),
|
isExpanded: state.expandedSpaces.contains(child.uuid),
|
||||||
|
@ -457,16 +457,17 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
|
|||||||
emit(SpaceManagementLoading());
|
emit(SpaceManagementLoading());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
final spaceTreeState = event.context.read<SpaceTreeBloc>().state;
|
||||||
|
|
||||||
final updatedSpaces =
|
final updatedSpaces =
|
||||||
await saveSpacesHierarchically(event.context, event.spaces, event.communityUuid);
|
await saveSpacesHierarchically(event.context, event.spaces, event.communityUuid);
|
||||||
|
|
||||||
final allSpaces = await _fetchSpacesForCommunity(event.communityUuid);
|
final allSpaces = await _fetchSpacesForCommunity(event.communityUuid);
|
||||||
|
|
||||||
emit(SpaceCreationSuccess(spaces: updatedSpaces));
|
emit(SpaceCreationSuccess(spaces: updatedSpaces));
|
||||||
|
|
||||||
if (previousState is SpaceManagementLoaded) {
|
if (previousState is SpaceManagementLoaded) {
|
||||||
await _updateLoadedState(
|
await _updateLoadedState(
|
||||||
event.context,
|
spaceTreeState,
|
||||||
previousState,
|
previousState,
|
||||||
allSpaces,
|
allSpaces,
|
||||||
event.communityUuid,
|
event.communityUuid,
|
||||||
@ -483,15 +484,17 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _updateLoadedState(
|
Future<void> _updateLoadedState(
|
||||||
BuildContext context,
|
SpaceTreeState spaceTreeState,
|
||||||
SpaceManagementLoaded previousState,
|
SpaceManagementLoaded previousState,
|
||||||
List<SpaceModel> allSpaces,
|
List<SpaceModel> allSpaces,
|
||||||
String communityUuid,
|
String communityUuid,
|
||||||
Emitter<SpaceManagementState> emit,
|
Emitter<SpaceManagementState> emit,
|
||||||
) async {
|
) async {
|
||||||
|
try {
|
||||||
var prevSpaceModels = await fetchSpaceModels();
|
var prevSpaceModels = await fetchSpaceModels();
|
||||||
|
|
||||||
await fetchTags();
|
await fetchTags();
|
||||||
final spaceTreeState = context.read<SpaceTreeBloc>().state;
|
|
||||||
final communities = spaceTreeState.searchQuery.isNotEmpty
|
final communities = spaceTreeState.searchQuery.isNotEmpty
|
||||||
? spaceTreeState.filteredCommunity
|
? spaceTreeState.filteredCommunity
|
||||||
: spaceTreeState.communityList;
|
: spaceTreeState.communityList;
|
||||||
@ -507,12 +510,14 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
|
|||||||
selectedCommunity: community,
|
selectedCommunity: community,
|
||||||
selectedSpace: null,
|
selectedSpace: null,
|
||||||
spaceModels: prevSpaceModels,
|
spaceModels: prevSpaceModels,
|
||||||
allTags: _cachedTags ?? []));
|
allTags: _cachedTags ?? [],
|
||||||
|
));
|
||||||
return;
|
return;
|
||||||
} else {
|
|
||||||
print("Community not found");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<SpaceModel>> saveSpacesHierarchically(
|
Future<List<SpaceModel>> saveSpacesHierarchically(
|
||||||
|
@ -53,6 +53,9 @@ class SpaceManagementPageState extends State<SpaceManagementPage> {
|
|||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
if (state is SpaceManagementLoading) {
|
if (state is SpaceManagementLoading) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
if (state is SpaceManagementInitial) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
} else if (state is BlankState) {
|
} else if (state is BlankState) {
|
||||||
return LoadedSpaceView(
|
return LoadedSpaceView(
|
||||||
communities: state.communities,
|
communities: state.communities,
|
||||||
|
@ -526,6 +526,8 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
|
|||||||
isNameFieldInvalid = true;
|
isNameFieldInvalid = true;
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
} else if (isNameFieldExist) {
|
||||||
|
return;
|
||||||
} else {
|
} else {
|
||||||
String newName = enteredName.isNotEmpty ? enteredName : (widget.name ?? '');
|
String newName = enteredName.isNotEmpty ? enteredName : (widget.name ?? '');
|
||||||
if (newName.isNotEmpty) {
|
if (newName.isNotEmpty) {
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
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/space_tree/bloc/space_tree_bloc.dart';
|
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
|
||||||
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart';
|
|
||||||
import 'package:syncrow_web/pages/space_tree/view/space_tree_view.dart';
|
import 'package:syncrow_web/pages/space_tree/view/space_tree_view.dart';
|
||||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_bloc.dart';
|
import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_bloc.dart';
|
||||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_event.dart';
|
import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_event.dart';
|
||||||
|
@ -2,10 +2,10 @@ import 'dart:async';
|
|||||||
|
|
||||||
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/common/widgets/empty_search_result_widget.dart';
|
|
||||||
import 'package:syncrow_web/common/widgets/search_bar.dart';
|
import 'package:syncrow_web/common/widgets/search_bar.dart';
|
||||||
import 'package:syncrow_web/common/widgets/sidebar_communities_list.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_bloc.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_state.dart';
|
||||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_bloc.dart';
|
import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_bloc.dart';
|
||||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_event.dart';
|
import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_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/community_model.dart';
|
||||||
@ -114,7 +114,9 @@ class _SidebarWidgetState extends State<SidebarWidget> {
|
|||||||
return Container(
|
return Container(
|
||||||
width: _width,
|
width: _width,
|
||||||
decoration: subSectionContainerDecoration,
|
decoration: subSectionContainerDecoration,
|
||||||
child: Column(
|
child: spaceTreeState is SpaceTreeLoadingState
|
||||||
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@ -124,10 +126,9 @@ class _SidebarWidgetState extends State<SidebarWidget> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Visibility(
|
child: Builder(
|
||||||
visible: filteredCommunities.isNotEmpty,
|
builder: (_) {
|
||||||
replacement: const EmptySearchResultWidget(),
|
return SidebarCommunitiesList(
|
||||||
child: SidebarCommunitiesList(
|
|
||||||
scrollController: _scrollController,
|
scrollController: _scrollController,
|
||||||
onScrollToEnd: () {},
|
onScrollToEnd: () {},
|
||||||
communities: filteredCommunities,
|
communities: filteredCommunities,
|
||||||
@ -144,9 +145,15 @@ class _SidebarWidgetState extends State<SidebarWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return _buildCommunityTile(context, filteredCommunities[index]);
|
return _buildCommunityTile(context, filteredCommunities[index]);
|
||||||
}),
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (spaceTreeState.paginationIsLoading || spaceTreeState.isSearching)
|
||||||
|
Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:syncrow_web/pages/access_management/view/access_management.dart';
|
import 'package:syncrow_web/pages/access_management/view/access_management.dart';
|
||||||
|
import 'package:syncrow_web/pages/analytics/modules/analytics/views/analytics_page.dart';
|
||||||
import 'package:syncrow_web/pages/auth/view/login_page.dart';
|
import 'package:syncrow_web/pages/auth/view/login_page.dart';
|
||||||
import 'package:syncrow_web/pages/device_managment/all_devices/view/device_managment_page.dart';
|
import 'package:syncrow_web/pages/device_managment/all_devices/view/device_managment_page.dart';
|
||||||
import 'package:syncrow_web/pages/home/view/home_page.dart';
|
import 'package:syncrow_web/pages/home/view/home_page.dart';
|
||||||
@ -37,6 +38,11 @@ class AppRoutes {
|
|||||||
GoRoute(
|
GoRoute(
|
||||||
path: RoutesConst.rolesAndPermissions,
|
path: RoutesConst.rolesAndPermissions,
|
||||||
builder: (context, state) => const RolesAndPermissionPage()),
|
builder: (context, state) => const RolesAndPermissionPage()),
|
||||||
|
GoRoute(
|
||||||
|
path: RoutesConst.analytics,
|
||||||
|
name: 'analytics',
|
||||||
|
builder: (context, state) => const AnalyticsPage(),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -72,4 +72,5 @@ abstract class ColorsManager {
|
|||||||
//background: #F8F8F8;
|
//background: #F8F8F8;
|
||||||
static const Color vividBlue = Color(0xFF023DFE);
|
static const Color vividBlue = Color(0xFF023DFE);
|
||||||
static const Color semiTransparentRed = Color(0x99FF0000);
|
static const Color semiTransparentRed = Color(0x99FF0000);
|
||||||
|
static const Color grey700 = Color(0xFF2D3748);
|
||||||
}
|
}
|
||||||
|
@ -480,4 +480,5 @@ class Assets {
|
|||||||
static const String DisappeDelayIcon = 'assets/icons/disappe_delay_icon.svg';
|
static const String DisappeDelayIcon = 'assets/icons/disappe_delay_icon.svg';
|
||||||
static const String indentLevelIcon = 'assets/icons/indent_level_icon.svg';
|
static const String indentLevelIcon = 'assets/icons/indent_level_icon.svg';
|
||||||
static const String triggerLevelIcon = 'assets/icons/trigger_level_icon.svg';
|
static const String triggerLevelIcon = 'assets/icons/trigger_level_icon.svg';
|
||||||
|
static const String blankCalendar = 'assets/icons/blank_calendar.svg';
|
||||||
}
|
}
|
||||||
|
@ -6,4 +6,5 @@ class RoutesConst {
|
|||||||
static const String deviceManagementPage = '/device-management-page';
|
static const String deviceManagementPage = '/device-management-page';
|
||||||
static const String spacesManagementPage = '/spaces_management-page';
|
static const String spacesManagementPage = '/spaces_management-page';
|
||||||
static const String rolesAndPermissions = '/roles_and_Permissions-page';
|
static const String rolesAndPermissions = '/roles_and_Permissions-page';
|
||||||
|
static const String analytics = '/syncrow_analytics';
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
import 'color_manager.dart';
|
import 'color_manager.dart';
|
||||||
|
|
||||||
InputDecoration? textBoxDecoration(
|
InputDecoration? textBoxDecoration({bool suffixIcon = false, double radios = 8}) =>
|
||||||
{bool suffixIcon = false, double radios = 8}) =>
|
|
||||||
InputDecoration(
|
InputDecoration(
|
||||||
focusColor: ColorsManager.grayColor,
|
focusColor: ColorsManager.grayColor,
|
||||||
suffixIcon: suffixIcon ? const Icon(Icons.search) : null,
|
suffixIcon: suffixIcon ? const Icon(Icons.search) : null,
|
||||||
@ -68,10 +67,24 @@ BoxDecoration subSectionContainerDecoration = BoxDecoration(
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final secondarySection = BoxDecoration(
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.1),
|
||||||
|
spreadRadius: 1,
|
||||||
|
blurRadius: 7,
|
||||||
|
offset: const Offset(0, 10),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
color: ColorsManager.circleRolesBackground,
|
||||||
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(15),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
InputDecoration inputTextFormDeco({hintText}) => InputDecoration(
|
InputDecoration inputTextFormDeco({hintText}) => InputDecoration(
|
||||||
hintText: hintText,
|
hintText: hintText,
|
||||||
border: const OutlineInputBorder(
|
border: const OutlineInputBorder(
|
||||||
|
|
||||||
borderSide: BorderSide(
|
borderSide: BorderSide(
|
||||||
width: 1,
|
width: 1,
|
||||||
color: ColorsManager.textGray, // Border color for unfocused state
|
color: ColorsManager.textGray, // Border color for unfocused state
|
||||||
|
14
pubspec.yaml
14
pubspec.yaml
@ -35,21 +35,21 @@ dependencies:
|
|||||||
# The following adds the Cupertino Icons font to your application.
|
# The following adds the Cupertino Icons font to your application.
|
||||||
# Use with the CupertinoIcons class for iOS style icons.
|
# Use with the CupertinoIcons class for iOS style icons.
|
||||||
cupertino_icons: ^1.0.6
|
cupertino_icons: ^1.0.6
|
||||||
flutter_bloc: ^8.1.5
|
flutter_bloc: ^9.1.0
|
||||||
equatable: ^2.0.5
|
equatable: ^2.0.5
|
||||||
graphview: ^1.2.0
|
graphview: ^1.2.0
|
||||||
flutter_svg: ^2.0.10+1
|
flutter_svg: ^2.0.10+1
|
||||||
dio: ^5.5.0+1
|
dio: ^5.5.0+1
|
||||||
get_it: ^7.6.7
|
get_it: ^8.0.3
|
||||||
flutter_secure_storage: ^9.2.2
|
flutter_secure_storage: ^9.2.2
|
||||||
shared_preferences: ^2.3.0
|
shared_preferences: ^2.3.0
|
||||||
dropdown_button2: ^2.3.9
|
dropdown_button2: ^2.3.9
|
||||||
data_table_2: ^2.5.15
|
data_table_2: ^2.5.15
|
||||||
go_router:
|
go_router:
|
||||||
intl: ^0.19.0
|
intl: ^0.20.2
|
||||||
dropdown_search: ^5.0.6
|
dropdown_search: ^6.0.2
|
||||||
flutter_dotenv: ^5.1.0
|
flutter_dotenv: ^5.1.0
|
||||||
fl_chart: ^0.69.0
|
fl_chart: ^0.71.0
|
||||||
uuid: ^4.4.2
|
uuid: ^4.4.2
|
||||||
time_picker_spinner: ^1.0.0
|
time_picker_spinner: ^1.0.0
|
||||||
intl_phone_field: ^3.2.0
|
intl_phone_field: ^3.2.0
|
||||||
@ -60,7 +60,7 @@ dependencies:
|
|||||||
firebase_core: ^3.11.0
|
firebase_core: ^3.11.0
|
||||||
firebase_crashlytics: ^4.3.2
|
firebase_crashlytics: ^4.3.2
|
||||||
firebase_database: ^11.3.2
|
firebase_database: ^11.3.2
|
||||||
bloc: ^8.1.4
|
bloc: ^9.0.0
|
||||||
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
@ -72,7 +72,7 @@ dev_dependencies:
|
|||||||
# activated in the `analysis_options.yaml` file located at the root of your
|
# activated in the `analysis_options.yaml` file located at the root of your
|
||||||
# package. See that file for information about deactivating specific lint
|
# package. See that file for information about deactivating specific lint
|
||||||
# rules and activating additional ones.
|
# rules and activating additional ones.
|
||||||
flutter_lints: ^3.0.0
|
flutter_lints: ^5.0.0
|
||||||
|
|
||||||
# For information on the generic Dart part of this file, see the
|
# For information on the generic Dart part of this file, see the
|
||||||
# following page: https://dart.dev/tools/pub/pubspec
|
# following page: https://dart.dev/tools/pub/pubspec
|
||||||
|
Reference in New Issue
Block a user