Compare commits

..

14 Commits

Author SHA1 Message Date
d6ef06c1b3 Simplify widget structure in TotalEnergyConsumptionChart by removing unnecessary FittedBox wrapper around month title text. 2025-04-30 16:07:37 +03:00
c9aaf2580f Refactor TotalEnergyConsumptionChart to accept chartData as a parameter that it takes from TotalEnergyConsumptionBlocand update TotalEnergyConsumptionChartBox to use Bloc for state management. 2025-04-30 15:56:17 +03:00
d9cd5d0438 Injected TotalEnergyConsumptionBloc into AnalyticsPage. 2025-04-30 15:46:05 +03:00
3eb87dfde1 Created TotalEnergyConsumptionBloc and its implementation. 2025-04-30 15:45:07 +03:00
f29ff2551f Rename method getTotalEnergyConsumption to load in TotalEnergyConsumptionService and FakeTotalEnergyConsumptionService for consistency. 2025-04-30 15:34:10 +03:00
67dd59ee9c Add GetTotalEnergyConsumptionParam and FakeTotalEnergyConsumptionService implementations 2025-04-30 15:30:25 +03:00
bb3c3906d1 Add EnergyDataModel and update TotalEnergyConsumptionChart to use it 2025-04-30 15:15:05 +03:00
3873deca90 Created EnergyData model. 2025-04-30 15:09:30 +03:00
9431dd4500 extracted reusable helper methods into global extensions. 2025-04-30 15:02:53 +03:00
63718185e7 Refactor TotalEnergyConsumptionChart to TotalEnergyConsumptionChartBox for improved layout and encapsulation. 2025-04-30 14:59:01 +03:00
1f4e82d567 Enhance TotalEnergyConsumptionChart layout and tooltip functionality. 2025-04-30 14:56:01 +03:00
9f68d171ff progress towards making TotalEnergyConsumptionChart functional and look like the design. 2025-04-30 13:08:28 +03:00
6eba640037 bump-dependencies 2025-04-30 09:48:02 +03:00
7a088074e3 Prepared the layout of all charts. 2025-04-30 09:44:04 +03:00
17 changed files with 525 additions and 11 deletions

View File

@ -0,0 +1,6 @@
extension FormatNumberToKwh on num {
String get formatNumberToKwh {
final regExp = RegExp(r'(\d)(?=(\d{3})+$)');
return '${toStringAsFixed(0).replaceAllMapped(regExp, (match) => '${match[1]},')} kWh';
}
}

View File

@ -0,0 +1,19 @@
extension GetMonthNameFromNumber on num {
String get getMonthName {
return switch (this) {
1 => 'JAN',
2 => 'FEB',
3 => 'MAR',
4 => 'APR',
5 => 'MAY',
6 => 'JUN',
7 => 'JUL',
8 => 'AUG',
9 => 'SEP',
10 => 'OCT',
11 => 'NOV',
12 => 'DEC',
_ => 'N/A'
};
}
}

View File

@ -0,0 +1,14 @@
import 'package:equatable/equatable.dart';
class EnergyDataModel extends Equatable {
const EnergyDataModel({
required this.date,
required this.value,
});
final DateTime date;
final double value;
@override
List<Object?> get props => [date, value];
}

View File

@ -3,6 +3,8 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/bloc/analytics_tab_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/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/total_energy_consumption/total_energy_consumption_bloc.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/utils/theme/responsive_text_theme.dart';
import 'package:syncrow_web/web_layout/web_scaffold.dart';
@ -12,8 +14,17 @@ class AnalyticsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider<AnalyticsTabBloc>(
create: (context) => AnalyticsTabBloc(),
return MultiBlocProvider(
providers: [
BlocProvider<AnalyticsTabBloc>(
create: (context) => AnalyticsTabBloc(),
),
BlocProvider(
create: (context) => TotalEnergyConsumptionBloc(
FakeTotalEnergyConsumptionService(),
),
),
],
child: const AnalyticsPageForm(),
);
}

View File

@ -0,0 +1,42 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/analytics/models/energy_data_model.dart';
import 'package:syncrow_web/pages/analytics/params/get_total_energy_consumption_param.dart';
import 'package:syncrow_web/pages/analytics/services/total_energy_consumption/total_energy_consumption_service.dart';
part 'total_energy_consumption_event.dart';
part 'total_energy_consumption_state.dart';
class TotalEnergyConsumptionBloc
extends Bloc<TotalEnergyConsumptionEvent, TotalEnergyConsumptionState> {
TotalEnergyConsumptionBloc(
this._totalEnergyConsumptionService,
) : super(const TotalEnergyConsumptionState()) {
on<TotalEnergyConsumptionLoadEvent>(_onTotalEnergyConsumptionLoadEvent);
}
final TotalEnergyConsumptionService _totalEnergyConsumptionService;
Future<void> _onTotalEnergyConsumptionLoadEvent(
TotalEnergyConsumptionLoadEvent event,
Emitter<TotalEnergyConsumptionState> emit,
) async {
emit(state.copyWith(status: TotalEnergyConsumptionStatus.loading));
try {
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,
),
);
}
}
}

View File

@ -0,0 +1,17 @@
part of 'total_energy_consumption_bloc.dart';
sealed class TotalEnergyConsumptionEvent extends Equatable {
const TotalEnergyConsumptionEvent();
@override
List<Object?> get props => [];
}
final class TotalEnergyConsumptionLoadEvent extends TotalEnergyConsumptionEvent {
const TotalEnergyConsumptionLoadEvent({required this.param});
final GetTotalEnergyConsumptionParam param ;
@override
List<Object?> get props => [param];
}

View File

@ -0,0 +1,35 @@
part of 'total_energy_consumption_bloc.dart';
enum TotalEnergyConsumptionStatus {
initial,
loading,
loaded,
failure,
}
final class TotalEnergyConsumptionState extends Equatable {
const TotalEnergyConsumptionState({
this.status = TotalEnergyConsumptionStatus.initial,
this.chartData = const <EnergyDataModel>[],
this.errorMessage,
});
final List<EnergyDataModel> chartData;
final TotalEnergyConsumptionStatus status;
final String? errorMessage;
TotalEnergyConsumptionState copyWith({
List<EnergyDataModel>? chartData,
TotalEnergyConsumptionStatus? status,
String? errorMessage,
}) {
return TotalEnergyConsumptionState(
chartData: chartData ?? this.chartData,
status: status ?? this.status,
errorMessage: errorMessage ?? this.errorMessage,
);
}
@override
List<Object?> get props => [chartData, status, errorMessage];
}

View File

@ -1,12 +1,47 @@
import 'package:flutter/material.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_per_device_chart.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/power_clamp_energy_data_widget.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart_box.dart';
import 'package:syncrow_web/utils/style.dart';
class AnalyticsEnergyManagementView extends StatelessWidget {
const AnalyticsEnergyManagementView({super.key});
@override
Widget build(BuildContext context) {
return const Center(
child: Text('EnergyManagementView is Working!'),
return Padding(
padding: const EdgeInsets.all(32),
child: Row(
spacing: 20,
children: [
const Expanded(
flex: 2,
child: Column(
spacing: 20,
children: [
Expanded(child: TotalEnergyConsumptionChartBox()),
Expanded(child: EnergyConsumptionPerDeviceChart()),
],
),
),
Expanded(
child: Container(
decoration: subSectionContainerDecoration.copyWith(
borderRadius: BorderRadius.circular(30),
),
padding: const EdgeInsets.all(30),
child: const Column(
spacing: 10,
children: [
Expanded(flex: 2, child: PowerClampEnergyDataWidget()),
Expanded(child: EnergyConsumptionByPhasesChart()),
],
),
),
),
],
),
);
}
}

View File

@ -0,0 +1,10 @@
import 'package:flutter/material.dart';
class EnergyConsumptionByPhasesChart extends StatelessWidget {
const EnergyConsumptionByPhasesChart({super.key});
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}

View File

@ -0,0 +1,32 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/style.dart';
class EnergyConsumptionPerDeviceChart extends StatelessWidget {
const EnergyConsumptionPerDeviceChart({super.key});
@override
Widget build(BuildContext context) {
return Container(
decoration: subSectionContainerDecoration.copyWith(
borderRadius: BorderRadius.circular(30),
),
padding: const EdgeInsets.all(30),
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Energy Consumption per Device',
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.w700,
),
),
SizedBox(height: 20),
Expanded(
child: Placeholder(),
),
],
),
);
}
}

View File

@ -0,0 +1,10 @@
import 'package:flutter/material.dart';
class PowerClampEnergyDataWidget extends StatelessWidget {
const PowerClampEnergyDataWidget({super.key});
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}

View File

@ -0,0 +1,175 @@
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/pages/analytics/helpers/get_month_name_from_int.dart';
import 'package:syncrow_web/pages/analytics/models/energy_data_model.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
// energy_consumption_chart will return id, name and consumption
const phasesJson = {
"1": {
"phaseOne": 1000,
"phaseTwo": 2000,
"phaseThree": 3000,
}
};
class TotalEnergyConsumptionChart extends StatelessWidget {
const TotalEnergyConsumptionChart({required this.chartData, super.key});
final List<EnergyDataModel> chartData;
@override
Widget build(BuildContext context) {
return Expanded(
child: LineChart(
LineChartData(
titlesData: _titlesData(context),
lineBarsData: _lineBarsData,
gridData: FlGridData(
show: true,
drawVerticalLine: false,
drawHorizontalLine: true,
),
borderData: FlBorderData(
show: true,
border: const Border.symmetric(
horizontal: BorderSide(
color: ColorsManager.greyColor,
width: 1,
style: BorderStyle.solid,
),
),
),
lineTouchData: LineTouchData(
handleBuiltInTouches: true,
touchSpotThreshold: 2,
touchTooltipData: _lineTouchTooltipData(),
),
),
duration: Durations.extralong1,
curve: Curves.easeIn,
),
);
}
List<LineChartBarData> get _lineBarsData {
return [
LineChartBarData(
preventCurveOvershootingThreshold: 0.1,
curveSmoothness: 0.55,
preventCurveOverShooting: true,
spots: chartData
.asMap()
.entries
.map(
(entry) => FlSpot(
entry.key.toDouble(),
entry.value.value,
),
)
.toList(),
color: ColorsManager.blueColor.withValues(alpha: 0.6),
shadow: 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: FlDotData(show: false),
isStrokeCapRound: true,
barWidth: 3,
),
];
}
FlTitlesData _titlesData(BuildContext context) {
return FlTitlesData(
show: true,
bottomTitles: AxisTitles(
drawBelowEverything: true,
sideTitles: SideTitles(
interval: 1,
reservedSize: 32,
showTitles: true,
maxIncluded: true,
getTitlesWidget: (value, meta) => Padding(
padding: const EdgeInsets.only(top: 20.0),
child: Text(
chartData.elementAtOrNull(value.toInt())?.date.month.getMonthName ??
'',
style: context.textTheme.bodySmall?.copyWith(
fontSize: 12,
color: ColorsManager.greyColor,
),
),
),
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
maxIncluded: true,
reservedSize: 110,
getTitlesWidget: (value, meta) => Padding(
padding: const EdgeInsetsDirectional.only(end: 12),
child: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
value.formatNumberToKwh,
style: context.textTheme.bodySmall?.copyWith(
fontSize: 12,
color: ColorsManager.greyColor,
),
),
),
),
),
),
rightTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
);
}
LineTouchTooltipData _lineTouchTooltipData() {
return LineTouchTooltipData(
getTooltipColor: (touchTooltipItem) => ColorsManager.whiteColors,
tooltipBorder: const BorderSide(color: ColorsManager.semiTransparentBlack),
tooltipRoundedRadius: 16,
showOnTopOfTheChartBoxArea: false,
tooltipPadding: const EdgeInsets.all(8.0),
getTooltipItems: _getTooltipItems,
);
}
List<LineTooltipItem?> _getTooltipItems(List<LineBarSpot> touchedSpots) {
return touchedSpots.map((spot) {
return LineTooltipItem(
_getToolTipLabel(spot.x + 1, spot.y),
const TextStyle(
color: ColorsManager.textPrimaryColor,
fontWeight: FontWeight.w600,
fontSize: 12,
),
);
}).toList();
}
String _getToolTipLabel(num month, double value) {
final monthLabel = month.getMonthName;
final valueLabel = value.formatNumberToKwh;
final labels = [monthLabel, valueLabel];
return labels.where((element) => element.isNotEmpty).join(', ');
}
}

View File

@ -0,0 +1,56 @@
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/total_energy_consumption_chart.dart';
import 'package:syncrow_web/pages/analytics/params/get_total_energy_consumption_param.dart';
import 'package:syncrow_web/utils/style.dart';
class TotalEnergyConsumptionChartBox extends StatefulWidget {
const TotalEnergyConsumptionChartBox({super.key});
@override
State<TotalEnergyConsumptionChartBox> createState() =>
_TotalEnergyConsumptionChartBoxState();
}
class _TotalEnergyConsumptionChartBoxState
extends State<TotalEnergyConsumptionChartBox> {
@override
void initState() {
final param = GetTotalEnergyConsumptionParam(
startDate: DateTime.now().subtract(const Duration(days: 30)),
endDate: DateTime.now(),
spaceId: '123',
);
context.read<TotalEnergyConsumptionBloc>().add(
TotalEnergyConsumptionLoadEvent(param: param),
);
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<TotalEnergyConsumptionBloc, TotalEnergyConsumptionState>(
builder: (context, state) => Container(
decoration: subSectionContainerDecoration.copyWith(
borderRadius: BorderRadius.circular(30),
),
padding: const EdgeInsets.all(30),
child: Column(
spacing: 20,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Total Energy Consumption',
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.w700,
),
),
TotalEnergyConsumptionChart(chartData: state.chartData),
],
),
),
);
}
}

View File

@ -0,0 +1,19 @@
class GetTotalEnergyConsumptionParam {
final DateTime? startDate;
final DateTime? endDate;
final String? spaceId;
GetTotalEnergyConsumptionParam({
this.startDate,
this.endDate,
this.spaceId,
});
Map<String, dynamic> toJson() {
return {
'startDate': startDate?.toIso8601String(),
'endDate': endDate?.toIso8601String(),
'spaceId': spaceId,
};
}
}

View File

@ -0,0 +1,25 @@
import 'package:syncrow_web/pages/analytics/models/energy_data_model.dart';
import 'package:syncrow_web/pages/analytics/params/get_total_energy_consumption_param.dart';
import 'package:syncrow_web/pages/analytics/services/total_energy_consumption/total_energy_consumption_service.dart';
class FakeTotalEnergyConsumptionService implements TotalEnergyConsumptionService {
@override
Future<List<EnergyDataModel>> load(
GetTotalEnergyConsumptionParam param,
) {
return Future.value([
EnergyDataModel(date: DateTime(2025, 1), value: 18000),
EnergyDataModel(date: DateTime(2025, 2), value: 25000),
EnergyDataModel(date: DateTime(2025, 3), value: 22000),
EnergyDataModel(date: DateTime(2025, 4), value: 21000),
EnergyDataModel(date: DateTime(2025, 5), value: 30000),
EnergyDataModel(date: DateTime(2025, 6), value: 23000),
EnergyDataModel(date: DateTime(2025, 7), value: 21000),
EnergyDataModel(date: DateTime(2025, 8), value: 25000),
EnergyDataModel(date: DateTime(2025, 9), value: 21100),
EnergyDataModel(date: DateTime(2025, 10), value: 22000),
EnergyDataModel(date: DateTime(2025, 11), value: 21000),
EnergyDataModel(date: DateTime(2025, 12), value: 27500),
]);
}
}

View File

@ -0,0 +1,8 @@
import 'package:syncrow_web/pages/analytics/models/energy_data_model.dart';
import 'package:syncrow_web/pages/analytics/params/get_total_energy_consumption_param.dart';
abstract interface class TotalEnergyConsumptionService {
Future<List<EnergyDataModel>> load(
GetTotalEnergyConsumptionParam param,
);
}

View File

@ -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