diff --git a/analysis_options.yaml b/analysis_options.yaml index 2e349a87..81bdd00d 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -26,6 +26,7 @@ linter: rules: # avoid_print: false # Uncomment to disable the `avoid_print` 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 # https://dart.dev/guides/language/analysis-options diff --git a/assets/icons/blank_calendar.svg b/assets/icons/blank_calendar.svg new file mode 100644 index 00000000..ae5056a0 --- /dev/null +++ b/assets/icons/blank_calendar.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/pages/analytics/helpers/format_number_to_kwh.dart b/lib/pages/analytics/helpers/format_number_to_kwh.dart new file mode 100644 index 00000000..65789ada --- /dev/null +++ b/lib/pages/analytics/helpers/format_number_to_kwh.dart @@ -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'; + } +} diff --git a/lib/pages/analytics/helpers/get_month_name_from_int.dart b/lib/pages/analytics/helpers/get_month_name_from_int.dart new file mode 100644 index 00000000..54b0fa87 --- /dev/null +++ b/lib/pages/analytics/helpers/get_month_name_from_int.dart @@ -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' + }; + } +} \ No newline at end of file diff --git a/lib/pages/analytics/models/device_energy_data_model.dart b/lib/pages/analytics/models/device_energy_data_model.dart new file mode 100644 index 00000000..2bbb2a85 --- /dev/null +++ b/lib/pages/analytics/models/device_energy_data_model.dart @@ -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 energy; + final String deviceName; + final String deviceId; + final Color color; + + @override + List get props => [energy, deviceName, deviceId]; + + factory DeviceEnergyDataModel.fromJson(Map json) { + final energy = (json['energy'] as List? ?? []) + .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')), + ); + } +} diff --git a/lib/pages/analytics/models/energy_data_model.dart b/lib/pages/analytics/models/energy_data_model.dart new file mode 100644 index 00000000..f1a7ff66 --- /dev/null +++ b/lib/pages/analytics/models/energy_data_model.dart @@ -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 json) { + return EnergyDataModel( + date: DateTime.parse(json['date'] as String), + value: (json['value'] as num).toDouble(), + ); + } + + @override + List get props => [date, value]; +} diff --git a/lib/pages/analytics/models/phases_energy_consumption.dart b/lib/pages/analytics/models/phases_energy_consumption.dart new file mode 100644 index 00000000..f986c3ad --- /dev/null +++ b/lib/pages/analytics/models/phases_energy_consumption.dart @@ -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 get props => [month, phaseA, phaseB, phaseC]; + + factory PhasesEnergyConsumption.fromJson(Map 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(), + ); + } +} diff --git a/lib/pages/analytics/models/power_clamp_energy_status.dart b/lib/pages/analytics/models/power_clamp_energy_status.dart new file mode 100644 index 00000000..cb99254f --- /dev/null +++ b/lib/pages/analytics/models/power_clamp_energy_status.dart @@ -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, + }); +} diff --git a/lib/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart b/lib/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart new file mode 100644 index 00000000..b848f79f --- /dev/null +++ b/lib/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart @@ -0,0 +1,17 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; + +part 'analytics_date_picker_event.dart'; + +class AnalyticsDatePickerBloc extends Bloc { + AnalyticsDatePickerBloc() : super(DateTime.now()) { + on(_onUpdateAnalyticsDatePickerEvent); + } + + void _onUpdateAnalyticsDatePickerEvent( + UpdateAnalyticsDatePickerEvent event, + Emitter emit, + ) { + emit(event.date); + } +} diff --git a/lib/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_event.dart b/lib/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_event.dart new file mode 100644 index 00000000..4fdb265e --- /dev/null +++ b/lib/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_event.dart @@ -0,0 +1,17 @@ +part of 'analytics_date_picker_bloc.dart'; + +sealed class AnalyticsDatePickerEvent extends Equatable { + const AnalyticsDatePickerEvent(); + + @override + List get props => []; +} + +final class UpdateAnalyticsDatePickerEvent extends AnalyticsDatePickerEvent { + const UpdateAnalyticsDatePickerEvent(this.date); + + final DateTime date; + + @override + List get props => [date]; +} diff --git a/lib/pages/analytics/modules/analytics/blocs/analytics_tab/analytics_tab_bloc.dart b/lib/pages/analytics/modules/analytics/blocs/analytics_tab/analytics_tab_bloc.dart new file mode 100644 index 00000000..94a359f2 --- /dev/null +++ b/lib/pages/analytics/modules/analytics/blocs/analytics_tab/analytics_tab_bloc.dart @@ -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 { + AnalyticsTabBloc() : super(AnalyticsPageTab.energyManagement) { + on(_onUpdateAnalyticsTabEvent); + } + + void _onUpdateAnalyticsTabEvent( + UpdateAnalyticsTabEvent event, + Emitter emit, + ) { + emit(event.analyticsTab); + } +} diff --git a/lib/pages/analytics/modules/analytics/blocs/analytics_tab/analytics_tab_event.dart b/lib/pages/analytics/modules/analytics/blocs/analytics_tab/analytics_tab_event.dart new file mode 100644 index 00000000..0ae7d8c5 --- /dev/null +++ b/lib/pages/analytics/modules/analytics/blocs/analytics_tab/analytics_tab_event.dart @@ -0,0 +1,17 @@ +part of 'analytics_tab_bloc.dart'; + +sealed class AnalyticsTabEvent extends Equatable { + const AnalyticsTabEvent(); + + @override + List get props => []; +} + +class UpdateAnalyticsTabEvent extends AnalyticsTabEvent { + const UpdateAnalyticsTabEvent(this.analyticsTab); + + final AnalyticsPageTab analyticsTab; + + @override + List get props => [analyticsTab]; +} diff --git a/lib/pages/analytics/modules/analytics/enums/analytics_page_tab.dart b/lib/pages/analytics/modules/analytics/enums/analytics_page_tab.dart new file mode 100644 index 00000000..b26cfc95 --- /dev/null +++ b/lib/pages/analytics/modules/analytics/enums/analytics_page_tab.dart @@ -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; +} diff --git a/lib/pages/analytics/modules/analytics/views/analytics_page.dart b/lib/pages/analytics/modules/analytics/views/analytics_page.dart new file mode 100644 index 00000000..dbbf855f --- /dev/null +++ b/lib/pages/analytics/modules/analytics/views/analytics_page.dart @@ -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( + 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( + 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()), + ], + ), + ); + } +} diff --git a/lib/pages/analytics/modules/analytics/widgets/analytics_communities_sidebar.dart b/lib/pages/analytics/modules/analytics/widgets/analytics_communities_sidebar.dart new file mode 100644 index 00000000..801af744 --- /dev/null +++ b/lib/pages/analytics/modules/analytics/widgets/analytics_communities_sidebar.dart @@ -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().add( + const ClearPowerClampInfoEvent(), + ); + final (selectedCommunities, selectedSpaces) = + FetchEnergyManagementDataHelper + .getSelectedCommunitiesAndSpaces(context); + if (selectedCommunities.isEmpty && selectedSpaces.isEmpty) { + context.read().add( + const ClearPowerClampInfoEvent(), + ); + } else { + FetchEnergyManagementDataHelper.loadPowerClampInfo( + context, + ); + } + } + }, + ); + }, + isSide: false, + ), + ); + }, + ); + } +} diff --git a/lib/pages/analytics/modules/analytics/widgets/analytics_date_filter_button.dart b/lib/pages/analytics/modules/analytics/widgets/analytics_date_filter_button.dart new file mode 100644 index 00000000..19a72566 --- /dev/null +++ b/lib/pages/analytics/modules/analytics/widgets/analytics_date_filter_button.dart @@ -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 createState() => + _AnalyticsDateFilterButtonState(); +} + +class _AnalyticsDateFilterButtonState extends State { + 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().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; + } +} diff --git a/lib/pages/analytics/modules/analytics/widgets/analytics_page_tab_button.dart b/lib/pages/analytics/modules/analytics/widgets/analytics_page_tab_button.dart new file mode 100644 index 00000000..fb0983fa --- /dev/null +++ b/lib/pages/analytics/modules/analytics/widgets/analytics_page_tab_button.dart @@ -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().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, + ), + ), + ); + } +} diff --git a/lib/pages/analytics/modules/analytics/widgets/analytics_page_tabs_and_children.dart b/lib/pages/analytics/modules/analytics/widgets/analytics_page_tabs_and_children.dart new file mode 100644 index 00000000..d7e2cfef --- /dev/null +++ b/lib/pages/analytics/modules/analytics/widgets/analytics_page_tabs_and_children.dart @@ -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( + 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, + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/analytics/modules/analytics/widgets/month_picker_widget.dart b/lib/pages/analytics/modules/analytics/widgets/month_picker_widget.dart new file mode 100644 index 00000000..53ba31af --- /dev/null +++ b/lib/pages/analytics/modules/analytics/widgets/month_picker_widget.dart @@ -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? onDateSelected; + + @override + State createState() => _MonthPickerWidgetState(); +} + +class _MonthPickerWidgetState extends State { + 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, + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/pages/analytics/modules/energy_management/blocs/energy_consumption_by_phases/energy_consumption_by_phases_bloc.dart b/lib/pages/analytics/modules/energy_management/blocs/energy_consumption_by_phases/energy_consumption_by_phases_bloc.dart new file mode 100644 index 00000000..012f435a --- /dev/null +++ b/lib/pages/analytics/modules/energy_management/blocs/energy_consumption_by_phases/energy_consumption_by_phases_bloc.dart @@ -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 { + EnergyConsumptionByPhasesBloc( + this._energyConsumptionByPhasesService, + ) : super(const EnergyConsumptionByPhasesState()) { + on(_onLoadEnergyConsumptionByPhasesEvent); + on(_onClearEnergyConsumptionByPhasesEvent); + } + + final EnergyConsumptionByPhasesService _energyConsumptionByPhasesService; + + Future _onLoadEnergyConsumptionByPhasesEvent( + LoadEnergyConsumptionByPhasesEvent event, + Emitter 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 emit, + ) async { + emit(const EnergyConsumptionByPhasesState()); + } +} diff --git a/lib/pages/analytics/modules/energy_management/blocs/energy_consumption_by_phases/energy_consumption_by_phases_event.dart b/lib/pages/analytics/modules/energy_management/blocs/energy_consumption_by_phases/energy_consumption_by_phases_event.dart new file mode 100644 index 00000000..87bcf447 --- /dev/null +++ b/lib/pages/analytics/modules/energy_management/blocs/energy_consumption_by_phases/energy_consumption_by_phases_event.dart @@ -0,0 +1,23 @@ +part of 'energy_consumption_by_phases_bloc.dart'; + +sealed class EnergyConsumptionByPhasesEvent extends Equatable { + const EnergyConsumptionByPhasesEvent(); + + @override + List get props => []; +} + +class LoadEnergyConsumptionByPhasesEvent extends EnergyConsumptionByPhasesEvent { + const LoadEnergyConsumptionByPhasesEvent({ + required this.param, + }); + + final GetEnergyConsumptionByPhasesParam param; + + @override + List get props => [param]; +} + +final class ClearEnergyConsumptionByPhasesEvent extends EnergyConsumptionByPhasesEvent { + const ClearEnergyConsumptionByPhasesEvent(); +} diff --git a/lib/pages/analytics/modules/energy_management/blocs/energy_consumption_by_phases/energy_consumption_by_phases_state.dart b/lib/pages/analytics/modules/energy_management/blocs/energy_consumption_by_phases/energy_consumption_by_phases_state.dart new file mode 100644 index 00000000..b6dd5890 --- /dev/null +++ b/lib/pages/analytics/modules/energy_management/blocs/energy_consumption_by_phases/energy_consumption_by_phases_state.dart @@ -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 [], + this.errorMessage, + }); + + final List chartData; + final EnergyConsumptionByPhasesStatus status; + final String? errorMessage; + + EnergyConsumptionByPhasesState copyWith({ + List? chartData, + EnergyConsumptionByPhasesStatus? status, + String? errorMessage, + }) { + return EnergyConsumptionByPhasesState( + chartData: chartData ?? this.chartData, + status: status ?? this.status, + errorMessage: errorMessage ?? this.errorMessage, + ); + } + + @override + List get props => [chartData, status, errorMessage]; +} diff --git a/lib/pages/analytics/modules/energy_management/blocs/energy_consumption_per_device/energy_consumption_per_device_bloc.dart b/lib/pages/analytics/modules/energy_management/blocs/energy_consumption_per_device/energy_consumption_per_device_bloc.dart new file mode 100644 index 00000000..c1c51a16 --- /dev/null +++ b/lib/pages/analytics/modules/energy_management/blocs/energy_consumption_per_device/energy_consumption_per_device_bloc.dart @@ -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 { + EnergyConsumptionPerDeviceBloc( + this._energyConsumptionPerDeviceService, + ) : super(const EnergyConsumptionPerDeviceState()) { + on(_onLoadEnergyConsumptionPerDeviceEvent); + on(_onClearEnergyConsumptionPerDeviceEvent); + } + + final EnergyConsumptionPerDeviceService _energyConsumptionPerDeviceService; + + Future _onLoadEnergyConsumptionPerDeviceEvent( + LoadEnergyConsumptionPerDeviceEvent event, + Emitter 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 emit, + ) async { + emit(const EnergyConsumptionPerDeviceState()); + } +} diff --git a/lib/pages/analytics/modules/energy_management/blocs/energy_consumption_per_device/energy_consumption_per_device_event.dart b/lib/pages/analytics/modules/energy_management/blocs/energy_consumption_per_device/energy_consumption_per_device_event.dart new file mode 100644 index 00000000..2e59da8e --- /dev/null +++ b/lib/pages/analytics/modules/energy_management/blocs/energy_consumption_per_device/energy_consumption_per_device_event.dart @@ -0,0 +1,23 @@ +part of 'energy_consumption_per_device_bloc.dart'; + +sealed class EnergyConsumptionPerDeviceEvent extends Equatable { + const EnergyConsumptionPerDeviceEvent(); + + @override + List get props => []; +} + +final class LoadEnergyConsumptionPerDeviceEvent + extends EnergyConsumptionPerDeviceEvent { + const LoadEnergyConsumptionPerDeviceEvent(this.param); + + final GetEnergyConsumptionPerDeviceParam param; + + @override + List get props => [param]; +} + +final class ClearEnergyConsumptionPerDeviceEvent + extends EnergyConsumptionPerDeviceEvent { + const ClearEnergyConsumptionPerDeviceEvent(); +} diff --git a/lib/pages/analytics/modules/energy_management/blocs/energy_consumption_per_device/energy_consumption_per_device_state.dart b/lib/pages/analytics/modules/energy_management/blocs/energy_consumption_per_device/energy_consumption_per_device_state.dart new file mode 100644 index 00000000..9ea7bd82 --- /dev/null +++ b/lib/pages/analytics/modules/energy_management/blocs/energy_consumption_per_device/energy_consumption_per_device_state.dart @@ -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 [], + this.errorMessage, + }); + + final List chartData; + final EnergyConsumptionPerDeviceStatus status; + final String? errorMessage; + + EnergyConsumptionPerDeviceState copyWith({ + List? chartData, + EnergyConsumptionPerDeviceStatus? status, + String? errorMessage, + }) { + return EnergyConsumptionPerDeviceState( + chartData: chartData ?? this.chartData, + status: status ?? this.status, + errorMessage: errorMessage ?? this.errorMessage, + ); + } + + @override + List get props => [chartData, status, errorMessage]; +} diff --git a/lib/pages/analytics/modules/energy_management/blocs/power_clamp_info/power_clamp_info_bloc.dart b/lib/pages/analytics/modules/energy_management/blocs/power_clamp_info/power_clamp_info_bloc.dart new file mode 100644 index 00000000..d0e7aab6 --- /dev/null +++ b/lib/pages/analytics/modules/energy_management/blocs/power_clamp_info/power_clamp_info_bloc.dart @@ -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 { + PowerClampInfoBloc( + this._powerClampInfoService, + ) : super(const PowerClampInfoState()) { + on(_onLoadPowerClampInfoEvent); + on(_onUpdatePowerClampStatusEvent); + on(_onClearPowerClampInfoEvent); + } + + final PowerClampInfoService _powerClampInfoService; + + Future _onLoadPowerClampInfoEvent( + LoadPowerClampInfoEvent event, + Emitter 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 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 emit, + ) { + emit(const PowerClampInfoState()); + } +} diff --git a/lib/pages/analytics/modules/energy_management/blocs/power_clamp_info/power_clamp_info_event.dart b/lib/pages/analytics/modules/energy_management/blocs/power_clamp_info/power_clamp_info_event.dart new file mode 100644 index 00000000..b69a2556 --- /dev/null +++ b/lib/pages/analytics/modules/energy_management/blocs/power_clamp_info/power_clamp_info_event.dart @@ -0,0 +1,31 @@ +part of 'power_clamp_info_bloc.dart'; + +sealed class PowerClampInfoEvent extends Equatable { + const PowerClampInfoEvent(); + + @override + List get props => []; +} + +final class LoadPowerClampInfoEvent extends PowerClampInfoEvent { + const LoadPowerClampInfoEvent(this.deviceId); + + final String deviceId; + + @override + List get props => [deviceId]; +} + + +final class UpdatePowerClampStatusEvent extends PowerClampInfoEvent { + const UpdatePowerClampStatusEvent(this.statusList); + + final List statusList; + + @override + List get props => [statusList]; +} + +final class ClearPowerClampInfoEvent extends PowerClampInfoEvent { + const ClearPowerClampInfoEvent(); +} \ No newline at end of file diff --git a/lib/pages/analytics/modules/energy_management/blocs/power_clamp_info/power_clamp_info_state.dart b/lib/pages/analytics/modules/energy_management/blocs/power_clamp_info/power_clamp_info_state.dart new file mode 100644 index 00000000..c541e9ac --- /dev/null +++ b/lib/pages/analytics/modules/energy_management/blocs/power_clamp_info/power_clamp_info_state.dart @@ -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 get props => [status, powerClampModel, errorMessage]; +} diff --git a/lib/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart b/lib/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart new file mode 100644 index 00000000..9099293f --- /dev/null +++ b/lib/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart @@ -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 { + RealtimeDeviceChangesBloc( + this._realtimeDeviceService, + ) : super(const RealtimeDeviceChangesState()) { + on(_onRealtimeDeviceChangesStarted); + on(_onRealtimeDeviceChangesClosed); + on<_RealtimeDeviceChangesUpdated>(_onRealtimeDeviceChangesUpdated); + } + + final RealtimeDeviceService _realtimeDeviceService; + StreamSubscription>? _subscription; + + Future _onRealtimeDeviceChangesStarted( + RealtimeDeviceChangesStarted event, + Emitter 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 _onRealtimeDeviceChangesClosed( + RealtimeDeviceChangesClosed event, + Emitter emit, + ) async { + add(const _RealtimeDeviceChangesUpdated([])); + await _subscription?.cancel(); + _subscription = null; + emit(const RealtimeDeviceChangesState()); + } + + void _onRealtimeDeviceChangesUpdated( + _RealtimeDeviceChangesUpdated event, + Emitter 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 close() async { + await _subscription?.cancel(); + return super.close(); + } +} diff --git a/lib/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_event.dart b/lib/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_event.dart new file mode 100644 index 00000000..1eba8f7e --- /dev/null +++ b/lib/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_event.dart @@ -0,0 +1,27 @@ +part of 'realtime_device_changes_bloc.dart'; + +sealed class RealtimeDeviceChangesEvent extends Equatable { + const RealtimeDeviceChangesEvent(); + + @override + List get props => []; +} + +final class RealtimeDeviceChangesStarted extends RealtimeDeviceChangesEvent { + const RealtimeDeviceChangesStarted(this.deviceId); + + final String deviceId; + + @override + List get props => [deviceId]; +} + +final class RealtimeDeviceChangesClosed extends RealtimeDeviceChangesEvent { + const RealtimeDeviceChangesClosed(); +} + +class _RealtimeDeviceChangesUpdated extends RealtimeDeviceChangesEvent { + final List deviceStatusList; + + const _RealtimeDeviceChangesUpdated(this.deviceStatusList); +} \ No newline at end of file diff --git a/lib/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_state.dart b/lib/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_state.dart new file mode 100644 index 00000000..5ba47feb --- /dev/null +++ b/lib/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_state.dart @@ -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 [], + this.errorMessage, + }); + + final RealtimeDeviceChangesStatus status; + final List deviceStatusList; + final String? errorMessage; + + RealtimeDeviceChangesState copyWith({ + RealtimeDeviceChangesStatus? status, + List? deviceStatusList, + String? errorMessage, + }) { + return RealtimeDeviceChangesState( + status: status ?? this.status, + deviceStatusList: deviceStatusList ?? this.deviceStatusList, + errorMessage: errorMessage ?? this.errorMessage, + ); + } + + @override + List get props => [status, deviceStatusList, errorMessage]; +} diff --git a/lib/pages/analytics/modules/energy_management/blocs/total_energy_consumption/total_energy_consumption_bloc.dart b/lib/pages/analytics/modules/energy_management/blocs/total_energy_consumption/total_energy_consumption_bloc.dart new file mode 100644 index 00000000..42ad57e8 --- /dev/null +++ b/lib/pages/analytics/modules/energy_management/blocs/total_energy_consumption/total_energy_consumption_bloc.dart @@ -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 { + TotalEnergyConsumptionBloc( + this._totalEnergyConsumptionService, + ) : super(const TotalEnergyConsumptionState()) { + on(_onTotalEnergyConsumptionLoadEvent); + on(_onClearTotalEnergyConsumptionEvent); + } + + final TotalEnergyConsumptionService _totalEnergyConsumptionService; + + Future _onTotalEnergyConsumptionLoadEvent( + TotalEnergyConsumptionLoadEvent event, + Emitter 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 emit, + ) async { + emit(const TotalEnergyConsumptionState()); + } +} diff --git a/lib/pages/analytics/modules/energy_management/blocs/total_energy_consumption/total_energy_consumption_event.dart b/lib/pages/analytics/modules/energy_management/blocs/total_energy_consumption/total_energy_consumption_event.dart new file mode 100644 index 00000000..f9510737 --- /dev/null +++ b/lib/pages/analytics/modules/energy_management/blocs/total_energy_consumption/total_energy_consumption_event.dart @@ -0,0 +1,21 @@ +part of 'total_energy_consumption_bloc.dart'; + +sealed class TotalEnergyConsumptionEvent extends Equatable { + const TotalEnergyConsumptionEvent(); + + @override + List get props => []; +} + +final class TotalEnergyConsumptionLoadEvent extends TotalEnergyConsumptionEvent { + const TotalEnergyConsumptionLoadEvent({required this.param}); + + final GetTotalEnergyConsumptionParam param; + + @override + List get props => [param]; +} + +final class ClearTotalEnergyConsumptionEvent extends TotalEnergyConsumptionEvent { + const ClearTotalEnergyConsumptionEvent(); +} diff --git a/lib/pages/analytics/modules/energy_management/blocs/total_energy_consumption/total_energy_consumption_state.dart b/lib/pages/analytics/modules/energy_management/blocs/total_energy_consumption/total_energy_consumption_state.dart new file mode 100644 index 00000000..832fd2fc --- /dev/null +++ b/lib/pages/analytics/modules/energy_management/blocs/total_energy_consumption/total_energy_consumption_state.dart @@ -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 [], + this.errorMessage, + }); + + final List chartData; + final TotalEnergyConsumptionStatus status; + final String? errorMessage; + + TotalEnergyConsumptionState copyWith({ + List? chartData, + TotalEnergyConsumptionStatus? status, + String? errorMessage, + }) { + return TotalEnergyConsumptionState( + chartData: chartData ?? this.chartData, + status: status ?? this.status, + errorMessage: errorMessage ?? this.errorMessage, + ); + } + + @override + List get props => [chartData, status, errorMessage]; +} diff --git a/lib/pages/analytics/modules/energy_management/helpers/energy_consumption_by_phases_chart_helper.dart b/lib/pages/analytics/modules/energy_management/helpers/energy_consumption_by_phases_chart_helper.dart new file mode 100644 index 00000000..e79fe48d --- /dev/null +++ b/lib/pages/analytics/modules/energy_management/helpers/energy_consumption_by_phases_chart_helper.dart @@ -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(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), + ]; +} diff --git a/lib/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart b/lib/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart new file mode 100644 index 00000000..7e948d21 --- /dev/null +++ b/lib/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart @@ -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 getTooltipItems(List 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(), + ); + } +} diff --git a/lib/pages/analytics/modules/energy_management/helpers/fetch_energy_management_data_helper.dart b/lib/pages/analytics/modules/energy_management/helpers/fetch_energy_management_data_helper.dart new file mode 100644 index 00000000..4f329104 --- /dev/null +++ b/lib/pages/analytics/modules/energy_management/helpers/fetch_energy_management_data_helper.dart @@ -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 selectedCommunities, List selectedSpaces) + getSelectedCommunitiesAndSpaces(BuildContext context) { + final spaceTreeState = context.read().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().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().add( + TotalEnergyConsumptionLoadEvent(param: param), + ); + } + + static void loadEnergyConsumptionPerDevice(BuildContext context) { + const param = GetEnergyConsumptionPerDeviceParam(); + context.read().add( + const LoadEnergyConsumptionPerDeviceEvent(param), + ); + } + + static void loadPowerClampInfo(BuildContext context) { + context.read().add( + const LoadPowerClampInfoEvent('cb71d6ad-6e29-4eaa-ae3e-1a0d1c5f60fa'), + ); + } + + static void loadRealtimeDeviceChanges(BuildContext context) { + context.read().add( + const RealtimeDeviceChangesStarted('cb71d6ad-6e29-4eaa-ae3e-1a0d1c5f60fa'), + ); + } + + static void clearAllData(BuildContext context) { + context.read().add( + const RealtimeDeviceChangesClosed(), + ); + + context.read().add( + const ClearPowerClampInfoEvent(), + ); + context.read().add( + const ClearEnergyConsumptionPerDeviceEvent(), + ); + + context.read().add( + const ClearTotalEnergyConsumptionEvent(), + ); + + context.read().add( + const ClearEnergyConsumptionByPhasesEvent(), + ); + } +} diff --git a/lib/pages/analytics/modules/energy_management/views/analytics_energy_management_view.dart b/lib/pages/analytics/modules/energy_management/views/analytics_energy_management_view.dart new file mode 100644 index 00000000..cba5eea5 --- /dev/null +++ b/lib/pages/analytics/modules/energy_management/views/analytics_energy_management_view.dart @@ -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()), + ], + ), + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/lib/pages/analytics/modules/energy_management/widgets/chart_title.dart b/lib/pages/analytics/modules/energy_management/widgets/chart_title.dart new file mode 100644 index 00000000..914066bd --- /dev/null +++ b/lib/pages/analytics/modules/energy_management/widgets/chart_title.dart @@ -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, + ); + } +} diff --git a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_by_phases_chart.dart b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_by_phases_chart.dart new file mode 100644 index 00000000..c94755bb --- /dev/null +++ b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_by_phases_chart.dart @@ -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 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, + ); + } +} diff --git a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_by_phases_chart_box.dart b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_by_phases_chart_box.dart new file mode 100644 index 00000000..1766266c --- /dev/null +++ b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_by_phases_chart_box.dart @@ -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( + 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, + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_by_phases_title.dart b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_by_phases_title.dart new file mode 100644 index 00000000..e05450a6 --- /dev/null +++ b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_by_phases_title.dart @@ -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), + ], + ), + ), + ); + } +} diff --git a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart.dart b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart.dart new file mode 100644 index 00000000..8a779d63 --- /dev/null +++ b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart.dart @@ -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 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 spots, + }) { + return LineChartBarData( + spots: spots, + dashArray: [12, 18], + isCurved: true, + color: color, + barWidth: 3, + isStrokeCapRound: true, + dotData: const FlDotData(show: false), + ); + } +} diff --git a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart_box.dart b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart_box.dart new file mode 100644 index 00000000..b62ebe54 --- /dev/null +++ b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart_box.dart @@ -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( + 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), + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_devices_list.dart b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_devices_list.dart new file mode 100644 index 00000000..da7a59a8 --- /dev/null +++ b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_devices_list.dart @@ -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 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, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/analytics/modules/energy_management/widgets/power_clamp_energy_data_device_dropdown.dart b/lib/pages/analytics/modules/energy_management/widgets/power_clamp_energy_data_device_dropdown.dart new file mode 100644 index 00000000..20540328 --- /dev/null +++ b/lib/pages/analytics/modules/energy_management/widgets/power_clamp_energy_data_device_dropdown.dart @@ -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: () {}, + ); + } +} diff --git a/lib/pages/analytics/modules/energy_management/widgets/power_clamp_energy_data_widget.dart b/lib/pages/analytics/modules/energy_management/widgets/power_clamp_energy_data_widget.dart new file mode 100644 index 00000000..6bb56071 --- /dev/null +++ b/lib/pages/analytics/modules/energy_management/widgets/power_clamp_energy_data_widget.dart @@ -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( + listenWhen: (previous, current) => + previous.deviceStatusList != current.deviceStatusList || + previous.status != current.status, + listener: (context, state) => context.read().add( + UpdatePowerClampStatusEvent(state.deviceStatusList), + ), + child: BlocBuilder( + 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 points) { + return points + .firstWhere((e) => e.code == code, orElse: () => DataPoint(value: '--')) + .value + .toString(); + } +} diff --git a/lib/pages/analytics/modules/energy_management/widgets/power_clamp_energy_status_widget.dart b/lib/pages/analytics/modules/energy_management/widgets/power_clamp_energy_status_widget.dart new file mode 100644 index 00000000..27d74ae0 --- /dev/null +++ b/lib/pages/analytics/modules/energy_management/widgets/power_clamp_energy_status_widget.dart @@ -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 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, + ), + ], + ), + ); + } +} diff --git a/lib/pages/analytics/modules/energy_management/widgets/power_clamp_phase.dart b/lib/pages/analytics/modules/energy_management/widgets/power_clamp_phase.dart new file mode 100644 index 00000000..ca921422 --- /dev/null +++ b/lib/pages/analytics/modules/energy_management/widgets/power_clamp_phase.dart @@ -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), + ), + ); + } +} diff --git a/lib/pages/analytics/modules/energy_management/widgets/power_clamp_phases_data_widget.dart b/lib/pages/analytics/modules/energy_management/widgets/power_clamp_phases_data_widget.dart new file mode 100644 index 00000000..1cb20aac --- /dev/null +++ b/lib/pages/analytics/modules/energy_management/widgets/power_clamp_phases_data_widget.dart @@ -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? points, + }) { + final element = points?.firstWhere( + (e) => e.code == code, + orElse: () => DataPoint(value: '--'), + ); + + return element?.value.toString() ?? '--'; + } +} diff --git a/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart.dart b/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart.dart new file mode 100644 index 00000000..ddf016be --- /dev/null +++ b/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart.dart @@ -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 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 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, + ), + ]; + } +} diff --git a/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart_box.dart b/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart_box.dart new file mode 100644 index 00000000..9e70e45e --- /dev/null +++ b/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart_box.dart @@ -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( + 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), + ], + ), + ), + ); + } +} diff --git a/lib/pages/analytics/modules/occupancy/views/analytics_occupancy_view.dart b/lib/pages/analytics/modules/occupancy/views/analytics_occupancy_view.dart new file mode 100644 index 00000000..8042ff8b --- /dev/null +++ b/lib/pages/analytics/modules/occupancy/views/analytics_occupancy_view.dart @@ -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!'), + ); + } +} diff --git a/lib/pages/analytics/params/get_energy_consumption_by_phases_param.dart b/lib/pages/analytics/params/get_energy_consumption_by_phases_param.dart new file mode 100644 index 00000000..169e2753 --- /dev/null +++ b/lib/pages/analytics/params/get_energy_consumption_by_phases_param.dart @@ -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 toJson() { + return { + 'startDate': startDate?.toIso8601String(), + 'endDate': endDate?.toIso8601String(), + 'spaceId': spaceId, + }; + } + + @override + List get props => [startDate, endDate, spaceId]; +} diff --git a/lib/pages/analytics/params/get_energy_consumption_per_device_param.dart b/lib/pages/analytics/params/get_energy_consumption_per_device_param.dart new file mode 100644 index 00000000..4995b843 --- /dev/null +++ b/lib/pages/analytics/params/get_energy_consumption_per_device_param.dart @@ -0,0 +1,3 @@ +class GetEnergyConsumptionPerDeviceParam { + const GetEnergyConsumptionPerDeviceParam(); +} diff --git a/lib/pages/analytics/params/get_total_energy_consumption_param.dart b/lib/pages/analytics/params/get_total_energy_consumption_param.dart new file mode 100644 index 00000000..47b75cb8 --- /dev/null +++ b/lib/pages/analytics/params/get_total_energy_consumption_param.dart @@ -0,0 +1,19 @@ +class GetTotalEnergyConsumptionParam { + final DateTime? startDate; + final DateTime? endDate; + final String? spaceId; + + const GetTotalEnergyConsumptionParam({ + this.startDate, + this.endDate, + this.spaceId, + }); + + Map toJson() { + return { + 'startDate': startDate?.toIso8601String(), + 'endDate': endDate?.toIso8601String(), + 'spaceId': spaceId, + }; + } +} diff --git a/lib/pages/analytics/services/energy_consumption_by_phases/energy_consumption_by_phases_service.dart b/lib/pages/analytics/services/energy_consumption_by_phases/energy_consumption_by_phases_service.dart new file mode 100644 index 00000000..21362b65 --- /dev/null +++ b/lib/pages/analytics/services/energy_consumption_by_phases/energy_consumption_by_phases_service.dart @@ -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> load( + GetEnergyConsumptionByPhasesParam param, + ); +} diff --git a/lib/pages/analytics/services/energy_consumption_by_phases/fake_energy_consumption_by_phases_service.dart b/lib/pages/analytics/services/energy_consumption_by_phases/fake_energy_consumption_by_phases_service.dart new file mode 100644 index 00000000..f6ce67c9 --- /dev/null +++ b/lib/pages/analytics/services/energy_consumption_by_phases/fake_energy_consumption_by_phases_service.dart @@ -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> 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), + ], + ); + } +} diff --git a/lib/pages/analytics/services/energy_consumption_by_phases/remote_energy_consumption_by_phases_service.dart b/lib/pages/analytics/services/energy_consumption_by_phases/remote_energy_consumption_by_phases_service.dart new file mode 100644 index 00000000..f0ac31ed --- /dev/null +++ b/lib/pages/analytics/services/energy_consumption_by_phases/remote_energy_consumption_by_phases_service.dart @@ -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> load( + GetEnergyConsumptionByPhasesParam param, + ) async { + try { + final response = await _httpService.get( + path: 'endpoint', + showServerMessage: true, + expectedResponseModel: (data) { + final json = data as Map? ?? {}; + final mappedData = json['data'] as List? ?? []; + return mappedData.map((e) { + final jsonData = e as Map; + return PhasesEnergyConsumption.fromJson(jsonData); + }).toList(); + }, + ); + return response; + } catch (e) { + throw Exception('Failed to load energy consumption per device: $e'); + } + } +} diff --git a/lib/pages/analytics/services/energy_consumption_per_device/energy_consumption_per_device_service.dart b/lib/pages/analytics/services/energy_consumption_per_device/energy_consumption_per_device_service.dart new file mode 100644 index 00000000..63dde8dc --- /dev/null +++ b/lib/pages/analytics/services/energy_consumption_per_device/energy_consumption_per_device_service.dart @@ -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> load( + GetEnergyConsumptionPerDeviceParam param, + ); +} diff --git a/lib/pages/analytics/services/energy_consumption_per_device/fake_energy_consumption_per_device_service.dart b/lib/pages/analytics/services/energy_consumption_per_device/fake_energy_consumption_per_device_service.dart new file mode 100644 index 00000000..b1608eea --- /dev/null +++ b/lib/pages/analytics/services/energy_consumption_per_device/fake_energy_consumption_per_device_service.dart @@ -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> 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(); + }); + } +} diff --git a/lib/pages/analytics/services/energy_consumption_per_device/remote_energy_consumption_per_device_service.dart b/lib/pages/analytics/services/energy_consumption_per_device/remote_energy_consumption_per_device_service.dart new file mode 100644 index 00000000..2c43bb23 --- /dev/null +++ b/lib/pages/analytics/services/energy_consumption_per_device/remote_energy_consumption_per_device_service.dart @@ -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> load( + GetEnergyConsumptionPerDeviceParam param, + ) async { + try { + final response = await _httpService.get( + path: 'endpoint', + showServerMessage: true, + expectedResponseModel: (data) { + final json = data as Map? ?? {}; + final mappedData = json['data'] as List? ?? []; + return mappedData.map((e) { + final jsonData = e as Map; + return DeviceEnergyDataModel.fromJson(jsonData); + }).toList(); + }, + ); + return response; + } catch (e) { + throw Exception('Failed to load energy consumption per device: $e'); + } + } +} diff --git a/lib/pages/analytics/services/power_clamp_info/power_clamp_info_service.dart b/lib/pages/analytics/services/power_clamp_info/power_clamp_info_service.dart new file mode 100644 index 00000000..26361313 --- /dev/null +++ b/lib/pages/analytics/services/power_clamp_info/power_clamp_info_service.dart @@ -0,0 +1,5 @@ +import 'package:syncrow_web/pages/device_managment/power_clamp/models/power_clamp_model.dart'; + +abstract interface class PowerClampInfoService { + Future getInfo(String deviceId); +} diff --git a/lib/pages/analytics/services/power_clamp_info/remote_power_clamp_info_service.dart b/lib/pages/analytics/services/power_clamp_info/remote_power_clamp_info_service.dart new file mode 100644 index 00000000..17d5a7fc --- /dev/null +++ b/lib/pages/analytics/services/power_clamp_info/remote_power_clamp_info_service.dart @@ -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 getInfo(String deviceId) async { + try { + final response = await _httpService.get( + path: '/devices/$deviceId/functions/status', + showServerMessage: true, + expectedResponseModel: (data) { + final json = data as Map? ?? {}; + final mappedData = json['data'] as Map? ?? {}; + return PowerClampModel.fromJson(mappedData); + }, + ); + return response; + } catch (e) { + throw Exception('Failed to fetch power clamp info: $e'); + } + } +} diff --git a/lib/pages/analytics/services/realtime_device_service/firebase_realtime_device_service.dart b/lib/pages/analytics/services/realtime_device_service/firebase_realtime_device_service.dart new file mode 100644 index 00000000..2fd379e4 --- /dev/null +++ b/lib/pages/analytics/services/realtime_device_service/firebase_realtime_device_service.dart @@ -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> subscribe(String deviceId) { + try { + final ref = FirebaseDatabase.instance.ref('device-status/$deviceId'); + + return ref.onValue.asyncMap((event) { + final data = event.snapshot.value as Map?; + + if (data == null || data['status'] == null) { + throw Exception('Invalid data received from Firebase'); + } + + final statusMap = data['status'] as List; + return statusMap.map((status) { + if (status is! Map) { + 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'); + } + } +} diff --git a/lib/pages/analytics/services/realtime_device_service/realtime_device_service.dart b/lib/pages/analytics/services/realtime_device_service/realtime_device_service.dart new file mode 100644 index 00000000..7afece6a --- /dev/null +++ b/lib/pages/analytics/services/realtime_device_service/realtime_device_service.dart @@ -0,0 +1,5 @@ +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; + +abstract interface class RealtimeDeviceService { + Stream> subscribe(String deviceId); +} \ No newline at end of file diff --git a/lib/pages/analytics/services/total_energy_consumption/fake_total_energy_consumption_service.dart b/lib/pages/analytics/services/total_energy_consumption/fake_total_energy_consumption_service.dart new file mode 100644 index 00000000..a4a8a62d --- /dev/null +++ b/lib/pages/analytics/services/total_energy_consumption/fake_total_energy_consumption_service.dart @@ -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> load( + GetTotalEnergyConsumptionParam param, + ) { + return Future.value( + List.generate(30, (index) { + return EnergyDataModel( + date: DateTime(2025, 1, index + 1), + value: 20000 + (index * 1000) % 5000, + ); + }), + ); + } +} diff --git a/lib/pages/analytics/services/total_energy_consumption/remote_total_energy_consumption_service.dart b/lib/pages/analytics/services/total_energy_consumption/remote_total_energy_consumption_service.dart new file mode 100644 index 00000000..193404ca --- /dev/null +++ b/lib/pages/analytics/services/total_energy_consumption/remote_total_energy_consumption_service.dart @@ -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> load( + GetTotalEnergyConsumptionParam param, + ) async { + try { + final response = await _httpService.get( + path: 'endpoint', + showServerMessage: true, + expectedResponseModel: (data) { + final json = data as Map? ?? {}; + final mappedData = json['data'] as List? ?? []; + return mappedData.map((e) { + final jsonData = e as Map; + return EnergyDataModel.fromJson(jsonData); + }).toList(); + }, + ); + return response; + } catch (e) { + throw Exception('Failed to load total energy consumption: $e'); + } + } +} diff --git a/lib/pages/analytics/services/total_energy_consumption/total_energy_consumption_service.dart b/lib/pages/analytics/services/total_energy_consumption/total_energy_consumption_service.dart new file mode 100644 index 00000000..5b7e0286 --- /dev/null +++ b/lib/pages/analytics/services/total_energy_consumption/total_energy_consumption_service.dart @@ -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> load( + GetTotalEnergyConsumptionParam param, + ); +} diff --git a/lib/pages/analytics/widgets/analytics_error_widget.dart b/lib/pages/analytics/widgets/analytics_error_widget.dart new file mode 100644 index 00000000..354eb31a --- /dev/null +++ b/lib/pages/analytics/widgets/analytics_error_widget.dart @@ -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, + ), + ), + ); + } +} diff --git a/lib/pages/analytics/widgets/charts_loading_widget.dart b/lib/pages/analytics/widgets/charts_loading_widget.dart new file mode 100644 index 00000000..cf81fee7 --- /dev/null +++ b/lib/pages/analytics/widgets/charts_loading_widget.dart @@ -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(), + ), + ), + ), + ); + } +} diff --git a/lib/pages/device_managment/power_clamp/models/power_clamp_model.dart b/lib/pages/device_managment/power_clamp/models/power_clamp_model.dart index 4318d5ee..b6592f4d 100644 --- a/lib/pages/device_managment/power_clamp/models/power_clamp_model.dart +++ b/lib/pages/device_managment/power_clamp/models/power_clamp_model.dart @@ -1,4 +1,6 @@ // PowerClampModel class to represent the response +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; + class PowerClampModel { String productUuid; String productType; @@ -46,11 +48,27 @@ class PowerStatus { factory PowerStatus.fromJson(Map json) { return PowerStatus( - phaseA: Phase.fromJson(json['phaseA']as List? ?? []), - phaseB: Phase.fromJson(json['phaseB']as List? ?? []), - phaseC: Phase.fromJson(json['phaseC']as List? ?? []), - general: Phase.fromJson(json['general']as List? ?? [] - )); + phaseA: Phase.fromJson(json['phaseA'] as List? ?? []), + phaseB: Phase.fromJson(json['phaseB'] as List? ?? []), + phaseC: Phase.fromJson(json['phaseC'] as List? ?? []), + general: Phase.fromJson(json['general'] as List? ?? []), + ); + } + + factory PowerStatus.fromStatusList(List statuses) { + List 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('')), + ); } } diff --git a/lib/pages/home/bloc/home_bloc.dart b/lib/pages/home/bloc/home_bloc.dart index 584a6e17..df9304bc 100644 --- a/lib/pages/home/bloc/home_bloc.dart +++ b/lib/pages/home/bloc/home_bloc.dart @@ -142,7 +142,7 @@ class HomeBloc extends Bloc { }, color: ColorsManager.primaryColor, ), - + // HomeItemModel( // title: 'Move in', // icon: Assets.moveinIcon, diff --git a/lib/pages/home/view/home_page_mobile.dart b/lib/pages/home/view/home_page_mobile.dart index 17735409..d0719c3e 100644 --- a/lib/pages/home/view/home_page_mobile.dart +++ b/lib/pages/home/view/home_page_mobile.dart @@ -50,8 +50,9 @@ class HomeMobilePage extends StatelessWidget { height: size.height * 0.6, width: size.width * 0.68, child: GridView.builder( - itemCount: 3, - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + itemCount: homeItems.length, + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, crossAxisSpacing: 20.0, mainAxisSpacing: 20.0, @@ -60,10 +61,11 @@ class HomeMobilePage extends StatelessWidget { itemBuilder: (context, index) { return HomeCard( index: index, - active: homeItems[index]['active'], - name: homeItems[index]['title'], - img: homeItems[index]['icon'], - onTap: () => homeBloc.homeItems[index].onPress(context), + active: homeBloc.homeItems[index].active!, + name: homeBloc.homeItems[index].title!, + img: homeBloc.homeItems[index].icon!, + onTap: () => + homeBloc.homeItems[index].onPress(context), ); }, ), @@ -94,6 +96,11 @@ class HomeMobilePage extends StatelessWidget { 'icon': Assets.devicesIcon, 'active': true, }, + { + 'title': 'Syncrow Analytics', + 'icon': Assets.iconEdit, + 'active': true, + }, // { // 'title': 'Move in', // 'icon': Assets.moveinIcon, diff --git a/lib/pages/home/view/home_page_web.dart b/lib/pages/home/view/home_page_web.dart index aad4a3be..9a59f51c 100644 --- a/lib/pages/home/view/home_page_web.dart +++ b/lib/pages/home/view/home_page_web.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.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_event.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/utils/constants/assets.dart'; import 'package:syncrow_web/web_layout/web_scaffold.dart'; @@ -24,7 +24,7 @@ class _HomeWebPageState extends State { void initState() { super.initState(); final homeBloc = BlocProvider.of(context); - homeBloc.add(FetchUserInfo()); + homeBloc.add(const FetchUserInfo()); } @override @@ -97,7 +97,7 @@ class _HomeWebPageState extends State { height: size.height * 0.6, width: size.width * 0.68, child: GridView.builder( - itemCount: 3, // Change this count if needed. + itemCount: homeBloc.homeItems.length, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, // Adjust as needed. crossAxisSpacing: 20.0, diff --git a/lib/pages/space_tree/bloc/space_tree_bloc.dart b/lib/pages/space_tree/bloc/space_tree_bloc.dart index ab20b430..a3a29004 100644 --- a/lib/pages/space_tree/bloc/space_tree_bloc.dart +++ b/lib/pages/space_tree/bloc/space_tree_bloc.dart @@ -23,6 +23,7 @@ class SpaceTreeBloc extends Bloc { on(_onCommunityUpdate); on(_fetchPaginationSpaces); on(_onDebouncedSearch); + on(_onSpaceTreeClearSelectionEvent); } Timer _timer = Timer(const Duration(microseconds: 0), () {}); @@ -36,8 +37,8 @@ class SpaceTreeBloc extends Bloc { final updatedCommunity = event.updatedCommunity; final updatedCommunities = List.from(state.communityList); - final index = - updatedCommunities.indexWhere((community) => community.uuid == updatedCommunity.uuid); + final index = updatedCommunities + .indexWhere((community) => community.uuid == updatedCommunity.uuid); if (index != -1) { updatedCommunities[index] = updatedCommunity; @@ -93,8 +94,11 @@ class SpaceTreeBloc extends Bloc { if (paginationModel.hasNext && state.searchQuery.isEmpty) { final projectUuid = await ProjectManager.getProjectUUID() ?? ''; - paginationModel = await CommunitySpaceManagementApi().fetchCommunitiesAndSpaces( - projectId: projectUuid, page: paginationModel.pageNum, search: state.searchQuery); + paginationModel = await CommunitySpaceManagementApi() + .fetchCommunitiesAndSpaces( + projectId: projectUuid, + page: paginationModel.pageNum, + search: state.searchQuery); communities.addAll(paginationModel.communities); } @@ -107,16 +111,19 @@ class SpaceTreeBloc extends Bloc { paginationIsLoading: false)); } - void _onCommunityAdded(OnCommunityAdded event, Emitter emit) async { + void _onCommunityAdded( + OnCommunityAdded event, Emitter emit) async { final updatedCommunities = List.from(state.communityList); updatedCommunities.add(event.newCommunity); emit(state.copyWith(communitiesList: updatedCommunities)); } - _onCommunityExpanded(OnCommunityExpanded event, Emitter emit) async { + _onCommunityExpanded( + OnCommunityExpanded event, Emitter emit) async { try { - List updatedExpandedCommunityList = List.from(state.expandedCommunities); + List updatedExpandedCommunityList = + List.from(state.expandedCommunities); if (updatedExpandedCommunityList.contains(event.communityId)) { updatedExpandedCommunityList.remove(event.communityId); @@ -148,14 +155,18 @@ class SpaceTreeBloc extends Bloc { } } - _onCommunitySelected(OnCommunitySelected event, Emitter emit) async { + _onCommunitySelected( + OnCommunitySelected event, Emitter emit) async { try { List updatedSelectedCommunities = List.from(state.selectedCommunities.toSet().toList()); - List updatedSelectedSpaces = List.from(state.selectedSpaces.toSet().toList()); + List updatedSelectedSpaces = + List.from(state.selectedSpaces.toSet().toList()); List updatedSoldChecks = List.from(state.soldCheck.toSet().toList()); - Map> communityAndSpaces = Map.from(state.selectedCommunityAndSpaces); - List selectedSpacesInCommunity = communityAndSpaces[event.communityId] ?? []; + Map> communityAndSpaces = + Map.from(state.selectedCommunityAndSpaces); + List selectedSpacesInCommunity = + communityAndSpaces[event.communityId] ?? []; List childrenIds = _getAllChildIds(event.children); @@ -188,11 +199,14 @@ class SpaceTreeBloc extends Bloc { try { List updatedSelectedCommunities = List.from(state.selectedCommunities.toSet().toList()); - List updatedSelectedSpaces = List.from(state.selectedSpaces.toSet().toList()); + List updatedSelectedSpaces = + List.from(state.selectedSpaces.toSet().toList()); List updatedSoldChecks = List.from(state.soldCheck.toSet().toList()); - Map> communityAndSpaces = Map.from(state.selectedCommunityAndSpaces); + Map> communityAndSpaces = + Map.from(state.selectedCommunityAndSpaces); - List selectedSpacesInCommunity = communityAndSpaces[event.communityModel.uuid] ?? []; + List selectedSpacesInCommunity = + communityAndSpaces[event.communityModel.uuid] ?? []; List childrenIds = _getAllChildIds(event.children); bool isChildSelected = false; @@ -215,9 +229,11 @@ class SpaceTreeBloc extends Bloc { selectedSpacesInCommunity.addAll(childrenIds); } - List spaces = _getThePathToChild(event.communityModel.uuid, event.spaceId); + List spaces = + _getThePathToChild(event.communityModel.uuid, event.spaceId); for (String space in spaces) { - if (!updatedSelectedSpaces.contains(space) && !updatedSoldChecks.contains(space)) { + if (!updatedSelectedSpaces.contains(space) && + !updatedSoldChecks.contains(space)) { updatedSoldChecks.add(space); } } @@ -240,7 +256,9 @@ class SpaceTreeBloc extends Bloc { updatedSoldChecks.remove(event.spaceId); List parents = - _getThePathToChild(event.communityModel.uuid, event.spaceId).toSet().toList(); + _getThePathToChild(event.communityModel.uuid, event.spaceId) + .toSet() + .toList(); if (updatedSelectedSpaces.isEmpty) { updatedSoldChecks.removeWhere(parents.contains); @@ -248,7 +266,8 @@ class SpaceTreeBloc extends Bloc { } else { // Check if any parent has selected children for (String space in parents) { - if (!_noChildrenSelected(event.communityModel, space, updatedSelectedSpaces, parents)) { + if (!_noChildrenSelected( + event.communityModel, space, updatedSelectedSpaces, parents)) { updatedSoldChecks.remove(space); } } @@ -273,8 +292,8 @@ class SpaceTreeBloc extends Bloc { } } - _noChildrenSelected( - CommunityModel community, String spaceId, List selectedSpaces, List parents) { + _noChildrenSelected(CommunityModel community, String spaceId, + List selectedSpaces, List parents) { if (selectedSpaces.contains(spaceId)) { return true; } @@ -300,7 +319,8 @@ class SpaceTreeBloc extends Bloc { if (_timer.isActive) { _timer.cancel(); // clear timer } - _timer = Timer(duration, () async => add(DebouncedSearchEvent(event.searchQuery))); + _timer = + Timer(duration, () async => add(DebouncedSearchEvent(event.searchQuery))); // List communities = List.from(state.communityList); // List filteredCommunity = []; @@ -324,7 +344,8 @@ class SpaceTreeBloc extends Bloc { } } - _onDebouncedSearch(DebouncedSearchEvent event, Emitter emit) async { + _onDebouncedSearch( + DebouncedSearchEvent event, Emitter emit) async { emit(state.copyWith( isSearching: true, )); @@ -333,7 +354,8 @@ class SpaceTreeBloc extends Bloc { final projectUuid = await ProjectManager.getProjectUUID() ?? ''; paginationModel = await CommunitySpaceManagementApi() - .fetchCommunitiesAndSpaces(projectId: projectUuid, page: 1, search: event.searchQuery); + .fetchCommunitiesAndSpaces( + projectId: projectUuid, page: 1, search: event.searchQuery); } catch (_) {} emit(state.copyWith( @@ -405,8 +427,8 @@ class SpaceTreeBloc extends Bloc { return children; } - bool _anySpacesSelectedInCommunity( - CommunityModel community, List selectedSpaces, List partialCheckedList) { + bool _anySpacesSelectedInCommunity(CommunityModel community, + List selectedSpaces, List partialCheckedList) { bool result = false; List ids = _getAllChildIds(community.spaces); for (var id in ids) { @@ -435,7 +457,8 @@ class SpaceTreeBloc extends Bloc { return ids; } - List _getAllParentsIds(SpaceModel child, String spaceId, List listIds) { + List _getAllParentsIds( + SpaceModel child, String spaceId, List listIds) { List ids = listIds; ids.add(child.uuid ?? ''); @@ -457,6 +480,19 @@ class SpaceTreeBloc extends Bloc { return []; } + void _onSpaceTreeClearSelectionEvent( + SpaceTreeClearSelectionEvent event, + Emitter emit, + ) async { + emit( + state.copyWith( + selectedCommunities: [], + selectedCommunityAndSpaces: {}, + selectedSpaces: [], + ), + ); + } + @override Future close() async { _timer.cancel(); diff --git a/lib/pages/space_tree/bloc/space_tree_event.dart b/lib/pages/space_tree/bloc/space_tree_event.dart index 22254ce0..9c2342fc 100644 --- a/lib/pages/space_tree/bloc/space_tree_event.dart +++ b/lib/pages/space_tree/bloc/space_tree_event.dart @@ -108,3 +108,7 @@ class OnCommunityUpdated extends SpaceTreeEvent { class ClearAllData extends SpaceTreeEvent {} class ClearCachedData extends SpaceTreeEvent {} + +class SpaceTreeClearSelectionEvent extends SpaceTreeEvent { + const SpaceTreeClearSelectionEvent(); +} diff --git a/lib/pages/space_tree/view/space_tree_view.dart b/lib/pages/space_tree/view/space_tree_view.dart index bfe3a2bd..fadcdc0c 100644 --- a/lib/pages/space_tree/view/space_tree_view.dart +++ b/lib/pages/space_tree/view/space_tree_view.dart @@ -17,7 +17,15 @@ import 'package:syncrow_web/utils/style.dart'; class SpaceTreeView extends StatefulWidget { final bool? isSide; 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 State createState() => _SpaceTreeViewState(); @@ -41,17 +49,31 @@ class _SpaceTreeViewState extends State { @override Widget build(BuildContext context) { return BlocBuilder(builder: (context, state) { - final communities = - state.searchQuery.isNotEmpty ? state.filteredCommunity : state.communityList; + final communities = state.searchQuery.isNotEmpty + ? state.filteredCommunity + : state.communityList; return Container( height: MediaQuery.sizeOf(context).height, decoration: widget.isSide == true - ? subSectionContainerDecoration.copyWith(color: ColorsManager.whiteColors) + ? subSectionContainerDecoration.copyWith( + color: ColorsManager.whiteColors) : const BoxDecoration(color: ColorsManager.whiteColors), child: state is SpaceTreeLoadingState ? const Center(child: CircularProgressIndicator()) : Column( 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) Container( decoration: const BoxDecoration( @@ -79,10 +101,12 @@ class _SpaceTreeViewState extends State { style: context.textTheme.bodyMedium?.copyWith( color: ColorsManager.blackColor, ), - onChanged: (value) => context.read().add( - SearchQueryEvent(value), - ), - decoration: textBoxDecoration(radios: 20)?.copyWith( + onChanged: (value) => + context.read().add( + SearchQueryEvent(value), + ), + decoration: + textBoxDecoration(radios: 20)?.copyWith( fillColor: Colors.white, suffixIcon: Padding( padding: const EdgeInsets.only(right: 16), @@ -92,7 +116,8 @@ class _SpaceTreeViewState extends State { height: 24, ), ), - hintStyle: context.textTheme.bodyMedium?.copyWith( + hintStyle: + context.textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w400, fontSize: 12, color: ColorsManager.textGray, @@ -131,33 +156,46 @@ class _SpaceTreeViewState extends State { itemBuilder: (context, index) { return CustomExpansionTileSpaceTree( title: communities[index].name, - isSelected: - state.selectedCommunities.contains(communities[index].uuid), - isSoldCheck: - state.selectedCommunities.contains(communities[index].uuid), - onExpansionChanged: () => context.read().add( - OnCommunityExpanded( - communities[index].uuid, - ), - ), + isSelected: state.selectedCommunities + .contains(communities[index].uuid), + isSoldCheck: state.selectedCommunities + .contains(communities[index].uuid), + onExpansionChanged: () => + context.read().add( + OnCommunityExpanded( + communities[index].uuid, + ), + ), isExpanded: state.expandedCommunities.contains( communities[index].uuid, ), onItemSelected: () { + widget.onSelect(); context.read().add( OnCommunitySelected( communities[index].uuid, communities[index].spaces, ), ); - widget.onSelect(); }, children: communities[index].spaces.map( (space) { return CustomExpansionTileSpaceTree( title: space.name, - isExpanded: state.expandedSpaces.contains(space.uuid), + isExpanded: + state.expandedSpaces.contains(space.uuid), onItemSelected: () { + final isParentSelected = _isParentSelected( + state, + communities[index], + space, + ); + if (widget + .shouldDisableDeselectingChildrenOfSelectedParent && + isParentSelected) { + return; + } + widget.onSelect(); context.read().add( OnSpaceSelected( communities[index], @@ -165,17 +203,19 @@ class _SpaceTreeViewState extends State { space.children, ), ); - widget.onSelect(); }, - onExpansionChanged: () => context.read().add( - OnSpaceExpanded( - communities[index].uuid, - space.uuid ?? '', - ), - ), - isSelected: state.selectedSpaces.contains(space.uuid) || + onExpansionChanged: () => + context.read().add( + OnSpaceExpanded( + communities[index].uuid, + space.uuid ?? '', + ), + ), + isSelected: state.selectedSpaces + .contains(space.uuid) || + state.soldCheck.contains(space.uuid), + isSoldCheck: state.soldCheck.contains(space.uuid), - isSoldCheck: state.soldCheck.contains(space.uuid), children: _buildNestedSpaces( context, state, @@ -196,6 +236,13 @@ class _SpaceTreeViewState extends State { }); } + 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 _buildNestedSpaces( BuildContext context, SpaceTreeState state, @@ -204,8 +251,8 @@ class _SpaceTreeViewState extends State { ) { return space.children.map((child) { return CustomExpansionTileSpaceTree( - isSelected: - state.selectedSpaces.contains(child.uuid) || state.soldCheck.contains(child.uuid), + isSelected: state.selectedSpaces.contains(child.uuid) || + state.soldCheck.contains(child.uuid), isSoldCheck: state.soldCheck.contains(child.uuid), title: child.name, isExpanded: state.expandedSpaces.contains(child.uuid), diff --git a/lib/pages/spaces_management/all_spaces/bloc/space_management_bloc.dart b/lib/pages/spaces_management/all_spaces/bloc/space_management_bloc.dart index 5df5c36d..759cea27 100644 --- a/lib/pages/spaces_management/all_spaces/bloc/space_management_bloc.dart +++ b/lib/pages/spaces_management/all_spaces/bloc/space_management_bloc.dart @@ -457,16 +457,17 @@ class SpaceManagementBloc extends Bloc().state; + final updatedSpaces = await saveSpacesHierarchically(event.context, event.spaces, event.communityUuid); - final allSpaces = await _fetchSpacesForCommunity(event.communityUuid); emit(SpaceCreationSuccess(spaces: updatedSpaces)); if (previousState is SpaceManagementLoaded) { await _updateLoadedState( - event.context, + spaceTreeState, previousState, allSpaces, event.communityUuid, @@ -483,35 +484,39 @@ class SpaceManagementBloc extends Bloc _updateLoadedState( - BuildContext context, + SpaceTreeState spaceTreeState, SpaceManagementLoaded previousState, List allSpaces, String communityUuid, Emitter emit, ) async { - var prevSpaceModels = await fetchSpaceModels(); - await fetchTags(); - final spaceTreeState = context.read().state; - final communities = spaceTreeState.searchQuery.isNotEmpty - ? spaceTreeState.filteredCommunity - : spaceTreeState.communityList; + try { + var prevSpaceModels = await fetchSpaceModels(); - for (var community in communities) { - if (community.uuid == communityUuid) { - community.spaces = allSpaces; - _spaceTreeBloc.add(InitialEvent()); + await fetchTags(); - emit(SpaceManagementLoaded( + final communities = spaceTreeState.searchQuery.isNotEmpty + ? spaceTreeState.filteredCommunity + : spaceTreeState.communityList; + + for (var community in communities) { + if (community.uuid == communityUuid) { + community.spaces = allSpaces; + _spaceTreeBloc.add(InitialEvent()); + + emit(SpaceManagementLoaded( communities: communities, products: _cachedProducts ?? [], selectedCommunity: community, selectedSpace: null, spaceModels: prevSpaceModels, - allTags: _cachedTags ?? [])); - return; - } else { - print("Community not found"); + allTags: _cachedTags ?? [], + )); + return; + } } + } catch (e, stackTrace) { + rethrow; } } diff --git a/lib/pages/spaces_management/all_spaces/view/spaces_management_page.dart b/lib/pages/spaces_management/all_spaces/view/spaces_management_page.dart index 00b2cf44..291e6235 100644 --- a/lib/pages/spaces_management/all_spaces/view/spaces_management_page.dart +++ b/lib/pages/spaces_management/all_spaces/view/spaces_management_page.dart @@ -53,6 +53,9 @@ class SpaceManagementPageState extends State { builder: (context, state) { if (state is SpaceManagementLoading) { return const Center(child: CircularProgressIndicator()); + } + if (state is SpaceManagementInitial) { + return const Center(child: CircularProgressIndicator()); } else if (state is BlankState) { return LoadedSpaceView( communities: state.communities, diff --git a/lib/pages/spaces_management/all_spaces/widgets/dialogs/create_space_dialog.dart b/lib/pages/spaces_management/all_spaces/widgets/dialogs/create_space_dialog.dart index a7f6e2ea..e9dde6f8 100644 --- a/lib/pages/spaces_management/all_spaces/widgets/dialogs/create_space_dialog.dart +++ b/lib/pages/spaces_management/all_spaces/widgets/dialogs/create_space_dialog.dart @@ -526,6 +526,8 @@ class CreateSpaceDialogState extends State { isNameFieldInvalid = true; }); return; + } else if (isNameFieldExist) { + return; } else { String newName = enteredName.isNotEmpty ? enteredName : (widget.name ?? ''); if (newName.isNotEmpty) { diff --git a/lib/pages/spaces_management/all_spaces/widgets/loaded_space_widget.dart b/lib/pages/spaces_management/all_spaces/widgets/loaded_space_widget.dart index 40876dd8..d2c72fa7 100644 --- a/lib/pages/spaces_management/all_spaces/widgets/loaded_space_widget.dart +++ b/lib/pages/spaces_management/all_spaces/widgets/loaded_space_widget.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.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_event.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_event.dart'; diff --git a/lib/pages/spaces_management/all_spaces/widgets/sidebar_widget.dart b/lib/pages/spaces_management/all_spaces/widgets/sidebar_widget.dart index 8c184c13..6149dbda 100644 --- a/lib/pages/spaces_management/all_spaces/widgets/sidebar_widget.dart +++ b/lib/pages/spaces_management/all_spaces/widgets/sidebar_widget.dart @@ -2,10 +2,10 @@ import 'dart:async'; import 'package:flutter/material.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/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_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_event.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart'; @@ -114,41 +114,48 @@ class _SidebarWidgetState extends State { return Container( width: _width, decoration: subSectionContainerDecoration, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SidebarHeader(onAddCommunity: _onAddCommunity), - CustomSearchBar( - onSearchChanged: _onSearchChanged, - ), - const SizedBox(height: 16), - Expanded( - child: Visibility( - visible: filteredCommunities.isNotEmpty, - replacement: const EmptySearchResultWidget(), - child: SidebarCommunitiesList( - scrollController: _scrollController, - onScrollToEnd: () {}, - communities: filteredCommunities, - itemBuilder: (context, index) { - if (index == filteredCommunities.length) { - final spaceTreeState = context.read().state; - if (spaceTreeState.paginationIsLoading) { - return const Padding( - padding: EdgeInsets.all(8.0), - child: Center(child: CircularProgressIndicator()), - ); - } else { - return const SizedBox.shrink(); - } - } - return _buildCommunityTile(context, filteredCommunities[index]); - }), + child: spaceTreeState is SpaceTreeLoadingState + ? const Center(child: CircularProgressIndicator()) + : Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SidebarHeader(onAddCommunity: _onAddCommunity), + CustomSearchBar( + onSearchChanged: _onSearchChanged, + ), + const SizedBox(height: 16), + Expanded( + child: Builder( + builder: (_) { + return SidebarCommunitiesList( + scrollController: _scrollController, + onScrollToEnd: () {}, + communities: filteredCommunities, + itemBuilder: (context, index) { + if (index == filteredCommunities.length) { + final spaceTreeState = context.read().state; + if (spaceTreeState.paginationIsLoading) { + return const Padding( + padding: EdgeInsets.all(8.0), + child: Center(child: CircularProgressIndicator()), + ); + } else { + return const SizedBox.shrink(); + } + } + return _buildCommunityTile(context, filteredCommunities[index]); + }, + ); + }, + ), + ), + if (spaceTreeState.paginationIsLoading || spaceTreeState.isSearching) + Center( + child: CircularProgressIndicator(), + ) + ], ), - ), - ], - ), ); } diff --git a/lib/utils/app_routes.dart b/lib/utils/app_routes.dart index ccc450fe..263bdbd6 100644 --- a/lib/utils/app_routes.dart +++ b/lib/utils/app_routes.dart @@ -1,5 +1,6 @@ import 'package:go_router/go_router.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/device_managment/all_devices/view/device_managment_page.dart'; import 'package:syncrow_web/pages/home/view/home_page.dart'; @@ -37,6 +38,11 @@ class AppRoutes { GoRoute( path: RoutesConst.rolesAndPermissions, builder: (context, state) => const RolesAndPermissionPage()), + GoRoute( + path: RoutesConst.analytics, + name: 'analytics', + builder: (context, state) => const AnalyticsPage(), + ), ]; } } diff --git a/lib/utils/color_manager.dart b/lib/utils/color_manager.dart index a4bcc0da..5a892aa6 100644 --- a/lib/utils/color_manager.dart +++ b/lib/utils/color_manager.dart @@ -72,4 +72,5 @@ abstract class ColorsManager { //background: #F8F8F8; static const Color vividBlue = Color(0xFF023DFE); static const Color semiTransparentRed = Color(0x99FF0000); + static const Color grey700 = Color(0xFF2D3748); } diff --git a/lib/utils/constants/assets.dart b/lib/utils/constants/assets.dart index 43f7bd92..f857a357 100644 --- a/lib/utils/constants/assets.dart +++ b/lib/utils/constants/assets.dart @@ -480,4 +480,5 @@ class Assets { static const String DisappeDelayIcon = 'assets/icons/disappe_delay_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 blankCalendar = 'assets/icons/blank_calendar.svg'; } diff --git a/lib/utils/constants/routes_const.dart b/lib/utils/constants/routes_const.dart index 8a65e9ae..83b1896f 100644 --- a/lib/utils/constants/routes_const.dart +++ b/lib/utils/constants/routes_const.dart @@ -6,4 +6,5 @@ class RoutesConst { static const String deviceManagementPage = '/device-management-page'; static const String spacesManagementPage = '/spaces_management-page'; static const String rolesAndPermissions = '/roles_and_Permissions-page'; + static const String analytics = '/syncrow_analytics'; } diff --git a/lib/utils/style.dart b/lib/utils/style.dart index b5ea59ee..905bb7a8 100644 --- a/lib/utils/style.dart +++ b/lib/utils/style.dart @@ -2,8 +2,7 @@ import 'package:flutter/material.dart'; import 'color_manager.dart'; -InputDecoration? textBoxDecoration( - {bool suffixIcon = false, double radios = 8}) => +InputDecoration? textBoxDecoration({bool suffixIcon = false, double radios = 8}) => InputDecoration( focusColor: ColorsManager.grayColor, 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( hintText: hintText, border: const OutlineInputBorder( - borderSide: BorderSide( width: 1, color: ColorsManager.textGray, // Border color for unfocused state diff --git a/pubspec.yaml b/pubspec.yaml index ec8660b6..7decc506 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -35,21 +35,21 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.6 - flutter_bloc: ^8.1.5 + flutter_bloc: ^9.1.0 equatable: ^2.0.5 graphview: ^1.2.0 flutter_svg: ^2.0.10+1 dio: ^5.5.0+1 - get_it: ^7.6.7 + get_it: ^8.0.3 flutter_secure_storage: ^9.2.2 shared_preferences: ^2.3.0 dropdown_button2: ^2.3.9 data_table_2: ^2.5.15 go_router: - intl: ^0.19.0 - dropdown_search: ^5.0.6 + intl: ^0.20.2 + dropdown_search: ^6.0.2 flutter_dotenv: ^5.1.0 - fl_chart: ^0.69.0 + fl_chart: ^0.71.0 uuid: ^4.4.2 time_picker_spinner: ^1.0.0 intl_phone_field: ^3.2.0 @@ -60,7 +60,7 @@ dependencies: firebase_core: ^3.11.0 firebase_crashlytics: ^4.3.2 firebase_database: ^11.3.2 - bloc: ^8.1.4 + bloc: ^9.0.0 dev_dependencies: @@ -72,7 +72,7 @@ dev_dependencies: # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # 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 # following page: https://dart.dev/tools/pub/pubspec