mirror of
https://github.com/SyncrowIOT/web.git
synced 2025-07-10 15:17:31 +00:00
Sp 1511 fe build occupancy heat map weekly monthly intensity view (#178)
* set the default tab to occupancy for ease of development. * Implemented an initial design for the occupancy chart. * Add Occupacy model and service for occupancy data handling. * Created `OccupancyBloc`. * Implemented the sidebar of Occupancy view. * Moved `OccupancyEndSideBar` widget to its own file. * Removed unnecessary widgets. * Matched the `OccupancyChart` with the figma design. * Added `AnalyticsDateFilterButton` to `OccupancyChartBox`. * Hides `AnalyticsDateFilterButton` that is in the page header, when the selected tab isn't `AnalyticsPageTab.energyManagement`. * Added animation to`AnalyticsDateFilterButton`. * modified the implementation of `FakeOccupacyService` to clamp all the generated values to less than a 100. * Injected `OccupancyBloc` into `AnalyticsPage`. * Made `OccupancyChart` read its data from `OccupancyBloc`. * Refactor AnalyticsCommunitiesSidebar to load data based on selected tab and implement loadEnergyManagementData method * Refactor Analytics views to use StatefulWidget and load data in initState * Created `OccupancyHeatMapModel`. * Add FakeOccupancyHeatMapService implementation. * Created `OccupancyHeatMapBloc`. * Injected `OccupancyHeatMapBloc` into `AnalyticsPage`. * Add OccupancyHeatMapBox widget and integrate into AnalyticsOccupancyView * Matching the heat map with the design, and added week days. * Made the HeatMap cells have a dashed border. * shows months. * responsiveness. * Integrate OccupancyHeatMapBloc and update OccupancyHeatMapBox to display heat map data with error handling * Integrate OccupancyHeatMapBloc and update OccupancyHeatMapBox to display heat map data with error handling * made the heatmap loading fast af by using painters instead of individually creating a widget for every single event. * Extracted `OccupancyHeatMapMonths` into its own widgte. * Moved `OccupancyHeatMapMonths` to its own file. * Adjusted design of `OccupancyHeatMapMonths`. * Adjust layout flex properties for `OccupancyEndSideBar` and its parent column in `AnalyticsOccupancyView`. * moved `OccupancyPaintItem` to `OccupancyPainter`s file. * removed comments from `OccupancyPainter`. * used color.withValues instead of .withOpacity. * re-added `OccupancyHeatMapGradient`. * Revert initial tab to `energyManagement`. * Made datepicker dynamic for multiple states. * Add year picker functionality to date filter button and implement dynamic date selection * Align date filter button to the end in occupancy chart and heat map boxes for improved UI consistency. * Enahnced color of border in `OccupancyPainter`. * Add ClearOccupancyHeatMapEvent to reset heat map state and update occupancy data helper to trigger event on empty selections * show percentage of value in tool tip of `OccupancyChart`.
This commit is contained in:
56
lib/pages/analytics/helpers/dashed_border_painter.dart
Normal file
56
lib/pages/analytics/helpers/dashed_border_painter.dart
Normal file
@ -0,0 +1,56 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class DashedBorderPainter extends CustomPainter {
|
||||
final double dashWidth;
|
||||
final double dashSpace;
|
||||
final Color color;
|
||||
|
||||
DashedBorderPainter({
|
||||
this.dashWidth = 4.0,
|
||||
this.dashSpace = 2.0,
|
||||
this.color = Colors.black,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paint = Paint()
|
||||
..color = color
|
||||
..strokeWidth = 0.5
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
final Path topPath = Path()
|
||||
..moveTo(0, 0)
|
||||
..lineTo(size.width, 0);
|
||||
|
||||
final Path bottomPath = Path()
|
||||
..moveTo(0, size.height)
|
||||
..lineTo(size.width, size.height);
|
||||
|
||||
final dashedTopPath = _createDashedPath(topPath, dashWidth, dashSpace);
|
||||
final dashedBottomPath = _createDashedPath(bottomPath, dashWidth, dashSpace);
|
||||
|
||||
canvas.drawPath(dashedTopPath, paint);
|
||||
canvas.drawPath(dashedBottomPath, paint);
|
||||
}
|
||||
|
||||
Path _createDashedPath(Path source, double dashWidth, double dashSpace) {
|
||||
final Path dashedPath = Path();
|
||||
for (PathMetric pathMetric in source.computeMetrics()) {
|
||||
double distance = 0.0;
|
||||
while (distance < pathMetric.length) {
|
||||
final double nextDistance = distance + dashWidth;
|
||||
dashedPath.addPath(
|
||||
pathMetric.extractPath(distance, nextDistance),
|
||||
Offset.zero,
|
||||
);
|
||||
distance = nextDistance + dashSpace;
|
||||
}
|
||||
}
|
||||
return dashedPath;
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||
}
|
22
lib/pages/analytics/models/occupancy_heat_map_model.dart
Normal file
22
lib/pages/analytics/models/occupancy_heat_map_model.dart
Normal file
@ -0,0 +1,22 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class OccupancyHeatMapModel extends Equatable {
|
||||
final DateTime date;
|
||||
|
||||
final int occupancy;
|
||||
|
||||
const OccupancyHeatMapModel({
|
||||
required this.date,
|
||||
required this.occupancy,
|
||||
});
|
||||
|
||||
factory OccupancyHeatMapModel.fromJson(Map<String, dynamic> json) {
|
||||
return OccupancyHeatMapModel(
|
||||
date: DateTime.parse(json['date'] as String),
|
||||
occupancy: json['occupancy'] as int,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [date, occupancy];
|
||||
}
|
@ -2,16 +2,23 @@ import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
part 'analytics_date_picker_event.dart';
|
||||
part 'analytics_date_picker_state.dart';
|
||||
|
||||
class AnalyticsDatePickerBloc extends Bloc<AnalyticsDatePickerEvent, DateTime> {
|
||||
AnalyticsDatePickerBloc() : super(DateTime.now()) {
|
||||
class AnalyticsDatePickerBloc
|
||||
extends Bloc<AnalyticsDatePickerEvent, AnalyticsDatePickerState> {
|
||||
AnalyticsDatePickerBloc() : super(AnalyticsDatePickerState()) {
|
||||
on<UpdateAnalyticsDatePickerEvent>(_onUpdateAnalyticsDatePickerEvent);
|
||||
}
|
||||
|
||||
void _onUpdateAnalyticsDatePickerEvent(
|
||||
UpdateAnalyticsDatePickerEvent event,
|
||||
Emitter<DateTime> emit,
|
||||
Emitter<AnalyticsDatePickerState> emit,
|
||||
) {
|
||||
emit(event.date);
|
||||
emit(
|
||||
state.copyWith(
|
||||
monthlyDate: event.montlyDate ?? state.monthlyDate,
|
||||
yearlyDate: event.yearlyDate ?? state.yearlyDate,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -8,10 +8,11 @@ sealed class AnalyticsDatePickerEvent extends Equatable {
|
||||
}
|
||||
|
||||
final class UpdateAnalyticsDatePickerEvent extends AnalyticsDatePickerEvent {
|
||||
const UpdateAnalyticsDatePickerEvent(this.date);
|
||||
const UpdateAnalyticsDatePickerEvent({this.montlyDate, this.yearlyDate});
|
||||
|
||||
final DateTime date;
|
||||
final DateTime? montlyDate;
|
||||
final DateTime? yearlyDate;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [date];
|
||||
List<Object?> get props => [montlyDate, yearlyDate];
|
||||
}
|
||||
|
@ -0,0 +1,25 @@
|
||||
part of 'analytics_date_picker_bloc.dart';
|
||||
|
||||
final class AnalyticsDatePickerState extends Equatable {
|
||||
AnalyticsDatePickerState({
|
||||
DateTime? monthlyDate,
|
||||
DateTime? yearlyDate,
|
||||
}) : monthlyDate = monthlyDate ?? DateTime.now(),
|
||||
yearlyDate = yearlyDate ?? DateTime.now();
|
||||
|
||||
final DateTime monthlyDate;
|
||||
final DateTime yearlyDate;
|
||||
|
||||
AnalyticsDatePickerState copyWith({
|
||||
DateTime? monthlyDate,
|
||||
DateTime? yearlyDate,
|
||||
}) {
|
||||
return AnalyticsDatePickerState(
|
||||
monthlyDate: monthlyDate ?? this.monthlyDate,
|
||||
yearlyDate: yearlyDate ?? this.yearlyDate,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [monthlyDate, yearlyDate];
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_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';
|
||||
@ -9,9 +10,11 @@ import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/powe
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/total_energy_consumption/total_energy_consumption_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy/occupancy_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy_heat_map/occupancy_heat_map_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/energy_consumption_by_phases/fake_energy_consumption_by_phases_service.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/energy_consumption_per_device/fake_energy_consumption_per_device_service.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/occupacy/fake_occupacy_service.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/occupancy_heat_map/fake_occupancy_heat_map_service.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/power_clamp_info/remote_power_clamp_info_service.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/realtime_device_service/firebase_realtime_device_service.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/total_energy_consumption/remote_total_energy_consumption_service.dart';
|
||||
@ -56,6 +59,10 @@ class AnalyticsPage extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
BlocProvider(create: (context) => OccupancyBloc(FakeOccupacyService())),
|
||||
BlocProvider(
|
||||
create: (context) => OccupancyHeatMapBloc(FakeOccupancyHeatMapService()),
|
||||
),
|
||||
BlocProvider(create: (context) => AnalyticsDatePickerBloc()),
|
||||
],
|
||||
child: const AnalyticsPageForm(),
|
||||
);
|
||||
|
@ -1,15 +1,24 @@
|
||||
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/pages/analytics/modules/analytics/widgets/year_picker_widget.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
|
||||
enum DatePickerType { month, year }
|
||||
|
||||
class AnalyticsDateFilterButton extends StatefulWidget {
|
||||
const AnalyticsDateFilterButton({super.key});
|
||||
const AnalyticsDateFilterButton({
|
||||
required this.selectedDate,
|
||||
required this.onDateSelected,
|
||||
this.datePickerType = DatePickerType.month,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final DateTime selectedDate;
|
||||
final void Function(DateTime)? onDateSelected;
|
||||
final DatePickerType datePickerType;
|
||||
|
||||
static final _color = ColorsManager.blackColor.withValues(alpha: 0.8);
|
||||
|
||||
@ -19,79 +28,69 @@ class AnalyticsDateFilterButton extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _AnalyticsDateFilterButtonState extends State<AnalyticsDateFilterButton> {
|
||||
late final AnalyticsDatePickerBloc _analyticsDatePickerBloc;
|
||||
@override
|
||||
void initState() {
|
||||
_analyticsDatePickerBloc = AnalyticsDatePickerBloc();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_analyticsDatePickerBloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _analyticsDatePickerBloc,
|
||||
child: Builder(builder: (context) {
|
||||
final selectedDate = context.watch<AnalyticsDatePickerBloc>().state;
|
||||
return TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: AnalyticsDateFilterButton._color,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: const BorderSide(
|
||||
color: ColorsManager.greyColor,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
backgroundColor: ColorsManager.transparentColor,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 32,
|
||||
vertical: 16,
|
||||
),
|
||||
return TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: AnalyticsDateFilterButton._color,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: const BorderSide(
|
||||
color: ColorsManager.greyColor,
|
||||
width: 1,
|
||||
),
|
||||
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,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
),
|
||||
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(widget.selectedDate),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) {
|
||||
return switch (widget.datePickerType) {
|
||||
DatePickerType.month => MonthPickerWidget(
|
||||
selectedDate: widget.selectedDate,
|
||||
onDateSelected: (value) {
|
||||
widget.onDateSelected?.call(value);
|
||||
},
|
||||
),
|
||||
DatePickerType.year => YearPickerWidget(
|
||||
selectedDate: widget.selectedDate,
|
||||
onDateSelected: (value) {
|
||||
widget.onDateSelected?.call(value);
|
||||
},
|
||||
),
|
||||
};
|
||||
},
|
||||
);
|
||||
}),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
String _formatDate(DateTime? date) {
|
||||
final formatter = DateFormat('MMMM yyyy');
|
||||
final formattedDate = formatter.format(date ?? DateTime.now());
|
||||
final formatterBasedOnDatePickerType = switch (widget.datePickerType) {
|
||||
DatePickerType.month => DateFormat('MMMM yyyy'),
|
||||
DatePickerType.year => DateFormat('yyyy'),
|
||||
};
|
||||
final formattedDate = formatterBasedOnDatePickerType.format(
|
||||
date ?? DateTime.now(),
|
||||
);
|
||||
return formattedDate;
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_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/pages/analytics/modules/energy_management/helpers/fetch_energy_management_data_helper.dart';
|
||||
import 'package:syncrow_web/utils/style.dart';
|
||||
|
||||
class AnalyticsPageTabsAndChildren extends StatelessWidget {
|
||||
@ -51,14 +53,33 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Visibility(
|
||||
visible: selectedTab == AnalyticsPageTab.energyManagement,
|
||||
child: const Expanded(
|
||||
flex: 2,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
child: AnalyticsDateFilterButton(),
|
||||
_buildAnimation(
|
||||
child: Visibility(
|
||||
key: ValueKey(selectedTab),
|
||||
visible: selectedTab == AnalyticsPageTab.energyManagement,
|
||||
child: Expanded(
|
||||
flex: 2,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
child: AnalyticsDateFilterButton(
|
||||
onDateSelected: (DateTime value) {
|
||||
context.read<AnalyticsDatePickerBloc>().add(
|
||||
UpdateAnalyticsDatePickerEvent(
|
||||
montlyDate: value),
|
||||
);
|
||||
FetchEnergyManagementDataHelper
|
||||
.fetchEnergyManagementData(
|
||||
context,
|
||||
selectedDate: value,
|
||||
);
|
||||
},
|
||||
selectedDate: context
|
||||
.watch<AnalyticsDatePickerBloc>()
|
||||
.state
|
||||
.monthlyDate,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -0,0 +1,147 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class YearPickerWidget extends StatefulWidget {
|
||||
const YearPickerWidget({
|
||||
super.key,
|
||||
required this.selectedDate,
|
||||
required this.onDateSelected,
|
||||
});
|
||||
|
||||
final DateTime selectedDate;
|
||||
final ValueChanged<DateTime>? onDateSelected;
|
||||
|
||||
@override
|
||||
State<YearPickerWidget> createState() => _YearPickerWidgetState();
|
||||
}
|
||||
|
||||
class _YearPickerWidgetState extends State<YearPickerWidget> {
|
||||
late int _currentYear;
|
||||
|
||||
static final years = List.generate(
|
||||
DateTime.now().year - 2020 + 1,
|
||||
(index) => (2020 + index),
|
||||
);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_currentYear = widget.selectedDate.year;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
child: Container(
|
||||
padding: const EdgeInsetsDirectional.all(20),
|
||||
width: 320,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildMonthsGrid(),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
spacing: 12,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
style: FilledButton.styleFrom(
|
||||
fixedSize: const Size(106, 40),
|
||||
backgroundColor: const Color(0xFFEDF2F7),
|
||||
padding: const EdgeInsetsDirectional.symmetric(
|
||||
vertical: 10,
|
||||
horizontal: 16,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'Cancel',
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: ColorsManager.grey700,
|
||||
),
|
||||
),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
final date = DateTime(_currentYear);
|
||||
widget.onDateSelected?.call(date);
|
||||
},
|
||||
style: FilledButton.styleFrom(
|
||||
fixedSize: const Size(106, 40),
|
||||
backgroundColor: ColorsManager.vividBlue.withValues(
|
||||
alpha: 0.7,
|
||||
),
|
||||
padding: const EdgeInsetsDirectional.symmetric(
|
||||
vertical: 10,
|
||||
horizontal: 16,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'Done',
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: ColorsManager.whiteColors,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMonthsGrid() {
|
||||
return GridView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: years.length,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.all(8),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
childAspectRatio: 2.5,
|
||||
mainAxisSpacing: 8,
|
||||
mainAxisExtent: 30,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final isSelected = _currentYear == years[index];
|
||||
return InkWell(
|
||||
onTap: () => setState(() => _currentYear = years[index]),
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? ColorsManager.vividBlue.withValues(alpha: 0.7)
|
||||
: const Color(0xFFEDF2F7),
|
||||
borderRadius:
|
||||
isSelected ? BorderRadius.circular(15) : BorderRadius.zero,
|
||||
),
|
||||
child: Text(
|
||||
years[index].toString(),
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
fontSize: 12,
|
||||
color: isSelected
|
||||
? ColorsManager.whiteColors
|
||||
: ColorsManager.blackColor.withValues(alpha: 0.8),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/energy_consumption_by_phases/energy_consumption_by_phases_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/energy_consumption_per_device/energy_consumption_per_device_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/power_clamp_info/power_clamp_info_bloc.dart';
|
||||
@ -24,9 +25,13 @@ abstract final class FetchEnergyManagementDataHelper {
|
||||
clearAllData(context);
|
||||
return;
|
||||
}
|
||||
final datePickerState = context.read<AnalyticsDatePickerBloc>().state;
|
||||
|
||||
loadTotalEnergyConsumption(context, selectedDate: selectedDate);
|
||||
loadEnergyConsumptionByPhases(context);
|
||||
loadTotalEnergyConsumption(context, selectedDate: datePickerState.monthlyDate);
|
||||
loadEnergyConsumptionByPhases(
|
||||
context,
|
||||
selectedDate: datePickerState.monthlyDate,
|
||||
);
|
||||
loadEnergyConsumptionPerDevice(context);
|
||||
return;
|
||||
}
|
||||
|
@ -0,0 +1,49 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/occupancy_heat_map_model.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_occupancy_heat_map_param.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/occupancy_heat_map/occupancy_heat_map_service.dart';
|
||||
|
||||
part 'occupancy_heat_map_event.dart';
|
||||
part 'occupancy_heat_map_state.dart';
|
||||
|
||||
class OccupancyHeatMapBloc
|
||||
extends Bloc<OccupancyHeatMapEvent, OccupancyHeatMapState> {
|
||||
OccupancyHeatMapBloc(
|
||||
this._occupancyHeatMapService,
|
||||
) : super(const OccupancyHeatMapState()) {
|
||||
on<LoadOccupancyHeatMapEvent>(_onLoadOccupancyHeatMapEvent);
|
||||
on<ClearOccupancyHeatMapEvent>(_onClearOccupancyHeatMapEvent);
|
||||
}
|
||||
final OccupancyHeatMapService _occupancyHeatMapService;
|
||||
|
||||
Future<void> _onLoadOccupancyHeatMapEvent(
|
||||
LoadOccupancyHeatMapEvent event,
|
||||
Emitter<OccupancyHeatMapState> emit,
|
||||
) async {
|
||||
emit(state.copyWith(status: OccupancyHeatMapStatus.loading));
|
||||
try {
|
||||
final occupancyHeatMap = await _occupancyHeatMapService.load(event.param);
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: OccupancyHeatMapStatus.loaded,
|
||||
heatMapData: occupancyHeatMap,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: OccupancyHeatMapStatus.failure,
|
||||
errorMessage: e.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onClearOccupancyHeatMapEvent(
|
||||
ClearOccupancyHeatMapEvent event,
|
||||
Emitter<OccupancyHeatMapState> emit,
|
||||
) {
|
||||
emit(const OccupancyHeatMapState());
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
part of 'occupancy_heat_map_bloc.dart';
|
||||
|
||||
sealed class OccupancyHeatMapEvent extends Equatable {
|
||||
const OccupancyHeatMapEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
final class LoadOccupancyHeatMapEvent extends OccupancyHeatMapEvent {
|
||||
const LoadOccupancyHeatMapEvent(this.param);
|
||||
|
||||
final GetOccupancyHeatMapParam param;
|
||||
|
||||
@override
|
||||
List<Object> get props => [param];
|
||||
}
|
||||
|
||||
final class ClearOccupancyHeatMapEvent extends OccupancyHeatMapEvent {
|
||||
const ClearOccupancyHeatMapEvent();
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
part of 'occupancy_heat_map_bloc.dart';
|
||||
|
||||
enum OccupancyHeatMapStatus { initial, loading, loaded, failure }
|
||||
|
||||
final class OccupancyHeatMapState extends Equatable {
|
||||
const OccupancyHeatMapState({
|
||||
this.status = OccupancyHeatMapStatus.initial,
|
||||
this.heatMapData = const [],
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
final OccupancyHeatMapStatus status;
|
||||
final String? errorMessage;
|
||||
final List<OccupancyHeatMapModel> heatMapData;
|
||||
|
||||
OccupancyHeatMapState copyWith({
|
||||
OccupancyHeatMapStatus? status,
|
||||
List<OccupancyHeatMapModel>? heatMapData,
|
||||
String? errorMessage,
|
||||
}) {
|
||||
return OccupancyHeatMapState(
|
||||
status: status ?? this.status,
|
||||
heatMapData: heatMapData ?? this.heatMapData,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [status, errorMessage, heatMapData];
|
||||
}
|
@ -1,7 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/fetch_energy_management_data_helper.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy/occupancy_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy_heat_map/occupancy_heat_map_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_occupancy_heat_map_param.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_occupancy_param.dart';
|
||||
|
||||
abstract final class FetchOccupancyDataHelper {
|
||||
@ -10,14 +13,36 @@ abstract final class FetchOccupancyDataHelper {
|
||||
static void loadOccupancyData(BuildContext context) {
|
||||
final (selectedCommunities, selectedSpaces) =
|
||||
FetchEnergyManagementDataHelper.getSelectedCommunitiesAndSpaces(context);
|
||||
if (selectedCommunities.isEmpty && selectedSpaces.isEmpty) return;
|
||||
if (selectedCommunities.isEmpty && selectedSpaces.isEmpty) {
|
||||
context.read<OccupancyBloc>().add(
|
||||
const ClearOccupancyEvent(),
|
||||
);
|
||||
context.read<OccupancyHeatMapBloc>().add(
|
||||
const ClearOccupancyHeatMapEvent(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final datePickerState = context.read<AnalyticsDatePickerBloc>().state;
|
||||
|
||||
context.read<OccupancyBloc>().add(
|
||||
LoadOccupancyEvent(
|
||||
GetOccupancyParam(
|
||||
monthDate: '04-2022',
|
||||
spaceUuid: '',
|
||||
communityUuid: '',
|
||||
monthDate:
|
||||
'${datePickerState.monthlyDate.year}-${datePickerState.monthlyDate.month}',
|
||||
spaceUuid: selectedSpaces.firstOrNull,
|
||||
communityUuid: selectedCommunities.first,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
context.read<OccupancyHeatMapBloc>().add(
|
||||
LoadOccupancyHeatMapEvent(
|
||||
GetOccupancyHeatMapParam(
|
||||
spaceId: selectedSpaces.isNotEmpty ? selectedSpaces.first : '',
|
||||
communityId:
|
||||
selectedCommunities.isNotEmpty ? selectedCommunities.first : '',
|
||||
year: datePickerState.yearlyDate,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_chart_box.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_end_side_bar.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_box.dart';
|
||||
|
||||
class AnalyticsOccupancyView extends StatefulWidget {
|
||||
const AnalyticsOccupancyView({super.key});
|
||||
@ -31,9 +32,9 @@ class _AnalyticsOccupancyViewState extends State<AnalyticsOccupancyView> {
|
||||
child: Column(
|
||||
spacing: 32,
|
||||
children: [
|
||||
SizedBox(height: height * 1.2, child: const OccupancyEndSideBar()),
|
||||
SizedBox(height: height * 0.5, child: const OccupancyChartBox()),
|
||||
SizedBox(height: height * 0.5, child: const Placeholder()),
|
||||
SizedBox(height: height * 0.4, child: const OccupancyEndSideBar()),
|
||||
SizedBox(height: height * 0.4, child: const OccupancyChartBox()),
|
||||
SizedBox(height: height * 0.4, child: const OccupancyHeatMapBox()),
|
||||
],
|
||||
),
|
||||
);
|
||||
@ -42,21 +43,21 @@ class _AnalyticsOccupancyViewState extends State<AnalyticsOccupancyView> {
|
||||
return SingleChildScrollView(
|
||||
child: Container(
|
||||
padding: AnalyticsOccupancyView._padding,
|
||||
height: height * 1,
|
||||
height: height * 0.9,
|
||||
child: const Row(
|
||||
spacing: 32,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
flex: 5,
|
||||
child: Column(
|
||||
spacing: 20,
|
||||
children: [
|
||||
Expanded(child: OccupancyChartBox()),
|
||||
Expanded(child: Placeholder()),
|
||||
Expanded(child: OccupancyHeatMapBox()),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(child: OccupancyEndSideBar()),
|
||||
Expanded(flex: 2, child: OccupancyEndSideBar()),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -80,10 +80,11 @@ class OccupancyChart extends StatelessWidget {
|
||||
}) {
|
||||
final data = chartData;
|
||||
|
||||
final month = double.parse(data[group.x.toInt()].occupancy).toStringAsFixed(2);
|
||||
final occupancyValue = double.parse(data[group.x.toInt()].occupancy);
|
||||
final percentage = '${(occupancyValue * 100).toStringAsFixed(0)}%';
|
||||
|
||||
return BarTooltipItem(
|
||||
month,
|
||||
percentage,
|
||||
context.textTheme.bodyMedium!.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
fontSize: 14,
|
||||
|
@ -1,8 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/analytics_date_filter_button.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy/occupancy_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart';
|
||||
import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart';
|
||||
import 'package:syncrow_web/utils/style.dart';
|
||||
@ -23,9 +25,9 @@ class OccupancyChartBox extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AnalyticsErrorWidget(state.errorMessage),
|
||||
const Row(
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
const Expanded(
|
||||
flex: 3,
|
||||
child: FittedBox(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
@ -33,11 +35,23 @@ class OccupancyChartBox extends StatelessWidget {
|
||||
child: ChartTitle(title: Text('Occupancy')),
|
||||
),
|
||||
),
|
||||
Spacer(),
|
||||
const Spacer(),
|
||||
Expanded(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: AnalyticsDateFilterButton(),
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
child: AnalyticsDateFilterButton(
|
||||
onDateSelected: (DateTime value) {
|
||||
context.read<AnalyticsDatePickerBloc>().add(
|
||||
UpdateAnalyticsDatePickerEvent(montlyDate: value),
|
||||
);
|
||||
FetchOccupancyDataHelper.loadOccupancyData(context);
|
||||
},
|
||||
selectedDate: context
|
||||
.watch<AnalyticsDatePickerBloc>()
|
||||
.state
|
||||
.monthlyDate,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -0,0 +1,84 @@
|
||||
import 'dart:math' as math show max;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_days.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_gradient.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_months.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_painter.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class OccupancyHeatMap extends StatelessWidget {
|
||||
const OccupancyHeatMap({required this.heatMapData, super.key});
|
||||
final Map<DateTime, int> heatMapData;
|
||||
|
||||
static const _cellSize = 16.0;
|
||||
static const _totalWeeks = 53;
|
||||
|
||||
int get _maxValue => heatMapData.isNotEmpty
|
||||
? heatMapData.keys.map((key) => heatMapData[key]!).reduce(math.max)
|
||||
: 0;
|
||||
|
||||
DateTime _getStartingDate() {
|
||||
final jan1 = DateTime(DateTime.now().year, 1, 1);
|
||||
final startOfWeek = jan1.subtract(Duration(days: jan1.weekday - 1));
|
||||
return startOfWeek;
|
||||
}
|
||||
|
||||
List<OccupancyPaintItem> _generatePaintItems(DateTime startDate) {
|
||||
return List.generate(_totalWeeks * 7, (index) {
|
||||
final date = startDate.add(Duration(days: index));
|
||||
final value = heatMapData[date] ?? 0;
|
||||
return OccupancyPaintItem(index: index, value: value);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final startDate = _getStartingDate();
|
||||
final paintItems = _generatePaintItems(startDate);
|
||||
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
OccupancyHeatMapMonths(startDate: startDate, cellSize: _cellSize),
|
||||
Container(
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(color: ColorsManager.grayBorder),
|
||||
top: BorderSide(color: ColorsManager.grayBorder),
|
||||
),
|
||||
),
|
||||
width: double.infinity,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.fill,
|
||||
child: Row(
|
||||
children: [
|
||||
const OccupancyHeatMapDays(cellSize: _cellSize),
|
||||
CustomPaint(
|
||||
size: const Size(_totalWeeks * _cellSize, 7 * _cellSize),
|
||||
child: CustomPaint(
|
||||
isComplex: true,
|
||||
size: const Size(_totalWeeks * _cellSize, 7 * _cellSize),
|
||||
painter: OccupancyPainter(
|
||||
items: paintItems,
|
||||
maxValue: _maxValue,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
OccupancyHeatMapGradient(maxValue: _maxValue),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/analytics_date_filter_button.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy_heat_map/occupancy_heat_map_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart';
|
||||
import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart';
|
||||
import 'package:syncrow_web/utils/style.dart';
|
||||
|
||||
class OccupancyHeatMapBox extends StatelessWidget {
|
||||
const OccupancyHeatMapBox({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<OccupancyHeatMapBloc, OccupancyHeatMapState>(
|
||||
builder: (context, state) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(30),
|
||||
decoration: containerWhiteDecoration,
|
||||
child: Column(
|
||||
spacing: 20,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AnalyticsErrorWidget(state.errorMessage),
|
||||
Row(
|
||||
children: [
|
||||
const Expanded(
|
||||
flex: 3,
|
||||
child: FittedBox(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
fit: BoxFit.scaleDown,
|
||||
child: ChartTitle(title: Text('Occupancy Heat Map')),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Expanded(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
child: AnalyticsDateFilterButton(
|
||||
onDateSelected: (DateTime value) {
|
||||
context.read<AnalyticsDatePickerBloc>().add(
|
||||
UpdateAnalyticsDatePickerEvent(yearlyDate: value),
|
||||
);
|
||||
FetchOccupancyDataHelper.loadOccupancyData(context);
|
||||
},
|
||||
datePickerType: DatePickerType.year,
|
||||
selectedDate: context
|
||||
.watch<AnalyticsDatePickerBloc>()
|
||||
.state
|
||||
.yearlyDate,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(height: 0),
|
||||
Expanded(
|
||||
child: OccupancyHeatMap(
|
||||
heatMapData: state.heatMapData.asMap().map(
|
||||
(_, value) => MapEntry(value.date, value.occupancy),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class OccupancyHeatMapDays extends StatelessWidget {
|
||||
const OccupancyHeatMapDays({
|
||||
required this.cellSize,
|
||||
this.textColor = ColorsManager.blackColor,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final double cellSize;
|
||||
final Color textColor;
|
||||
|
||||
static const _weekDayLabels = [
|
||||
'Monday',
|
||||
'Tuesday',
|
||||
'Wednesday',
|
||||
'Thursday',
|
||||
'Friday',
|
||||
'Saturday',
|
||||
'Sunday',
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: List.generate(7, (i) {
|
||||
final dayLabel = _weekDayLabels[i];
|
||||
return Container(
|
||||
height: cellSize,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
margin: const EdgeInsetsDirectional.all(0.5).add(
|
||||
const EdgeInsetsDirectional.only(end: 4),
|
||||
),
|
||||
padding: const EdgeInsets.only(right: 6),
|
||||
child: Text(
|
||||
dayLabel,
|
||||
textAlign: TextAlign.start,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: textColor,
|
||||
fontSize: 8,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class OccupancyHeatMapGradient extends StatelessWidget {
|
||||
const OccupancyHeatMapGradient({super.key, required this.maxValue});
|
||||
|
||||
final int maxValue;
|
||||
List<Color> _heatMapColors() {
|
||||
if (maxValue == 0) {
|
||||
return [
|
||||
ColorsManager.vividBlue.withValues(alpha: 0),
|
||||
ColorsManager.vividBlue.withValues(alpha: 0),
|
||||
];
|
||||
}
|
||||
return List.generate(
|
||||
maxValue + 1,
|
||||
(index) => ColorsManager.vividBlue.withValues(alpha: index / maxValue),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
const Spacer(),
|
||||
Tooltip(
|
||||
message: 'Min: 0 - Max: $maxValue',
|
||||
child: Container(
|
||||
width: 150,
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: ColorsManager.grayBorder,
|
||||
width: 1,
|
||||
),
|
||||
gradient: LinearGradient(
|
||||
begin: AlignmentDirectional.centerEnd,
|
||||
end: AlignmentDirectional.centerStart,
|
||||
colors: _heatMapColors(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_days.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class OccupancyHeatMapMonths extends StatelessWidget {
|
||||
const OccupancyHeatMapMonths({
|
||||
required this.startDate,
|
||||
required this.cellSize,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final DateTime startDate;
|
||||
final double cellSize;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 48,
|
||||
width: double.infinity,
|
||||
color: Colors.transparent,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
OccupancyHeatMapDays(
|
||||
cellSize: cellSize / 3,
|
||||
textColor: Colors.transparent,
|
||||
),
|
||||
...List.generate(12, (monthIndex) {
|
||||
final monthStartDate = DateTime(startDate.year, monthIndex + 1, 1);
|
||||
final monthName = DateFormat.MMM().format(monthStartDate);
|
||||
return Expanded(
|
||||
child: RotatedBox(
|
||||
quarterTurns: 3,
|
||||
child: Container(
|
||||
padding: EdgeInsetsDirectional.zero,
|
||||
margin: EdgeInsetsDirectional.zero,
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(color: ColorsManager.borderColor),
|
||||
),
|
||||
),
|
||||
width: cellSize * 4,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 4, top: 2),
|
||||
child: Text(
|
||||
monthName,
|
||||
style: const TextStyle(fontSize: 8),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class OccupancyPaintItem {
|
||||
final int index;
|
||||
final int value;
|
||||
|
||||
const OccupancyPaintItem({required this.index, required this.value});
|
||||
}
|
||||
|
||||
class OccupancyPainter extends CustomPainter {
|
||||
OccupancyPainter({
|
||||
required this.items,
|
||||
required this.maxValue,
|
||||
});
|
||||
|
||||
final List<OccupancyPaintItem> items;
|
||||
final int maxValue;
|
||||
|
||||
static const double cellSize = 16.0;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final Paint fillPaint = Paint();
|
||||
final Paint borderPaint = Paint()
|
||||
..color = ColorsManager.grayBorder.withValues(alpha: 0.4)
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
for (final item in items) {
|
||||
final column = item.index ~/ 7;
|
||||
final row = item.index % 7;
|
||||
|
||||
final x = column * cellSize;
|
||||
final y = row * cellSize;
|
||||
|
||||
fillPaint.color = _getColor(item.value);
|
||||
final rect = Rect.fromLTWH(x, y, cellSize, cellSize);
|
||||
canvas.drawRect(rect, fillPaint);
|
||||
|
||||
_drawDashedLine(
|
||||
canvas,
|
||||
Offset(x, y),
|
||||
Offset(x + cellSize, y),
|
||||
borderPaint,
|
||||
);
|
||||
_drawDashedLine(
|
||||
canvas,
|
||||
Offset(x, y + cellSize),
|
||||
Offset(x + cellSize, y + cellSize),
|
||||
borderPaint,
|
||||
);
|
||||
|
||||
canvas.drawLine(Offset(x, y), Offset(x, y + cellSize), borderPaint);
|
||||
canvas.drawLine(
|
||||
Offset(x + cellSize, y), Offset(x + cellSize, y + cellSize), borderPaint);
|
||||
}
|
||||
}
|
||||
|
||||
void _drawDashedLine(Canvas canvas, Offset start, Offset end, Paint paint) {
|
||||
const double dashWidth = 2.0;
|
||||
const double dashSpace = 4.0;
|
||||
final double totalLength = (end - start).distance;
|
||||
final Offset direction = (end - start) / (end - start).distance;
|
||||
|
||||
double currentLength = 0.0;
|
||||
while (currentLength < totalLength) {
|
||||
final Offset dashStart = start + direction * currentLength;
|
||||
final double nextLength = currentLength + dashWidth;
|
||||
final Offset dashEnd =
|
||||
start + direction * (nextLength < totalLength ? nextLength : totalLength);
|
||||
canvas.drawLine(dashStart, dashEnd, paint);
|
||||
currentLength = nextLength + dashSpace;
|
||||
}
|
||||
}
|
||||
|
||||
Color _getColor(int value) {
|
||||
if (maxValue == 0) return ColorsManager.vividBlue.withValues(alpha: 0);
|
||||
final opacity = value.clamp(0, maxValue) / maxValue;
|
||||
return ColorsManager.vividBlue.withValues(alpha: opacity);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||
}
|
19
lib/pages/analytics/params/get_occupancy_heat_map_param.dart
Normal file
19
lib/pages/analytics/params/get_occupancy_heat_map_param.dart
Normal file
@ -0,0 +1,19 @@
|
||||
class GetOccupancyHeatMapParam {
|
||||
final DateTime year;
|
||||
final String communityId;
|
||||
final String spaceId;
|
||||
|
||||
const GetOccupancyHeatMapParam({
|
||||
required this.year,
|
||||
required this.communityId,
|
||||
required this.spaceId,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'year': year.toIso8601String(),
|
||||
'communityId': communityId,
|
||||
'spaceId': spaceId,
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import 'package:syncrow_web/pages/analytics/models/occupancy_heat_map_model.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_occupancy_heat_map_param.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/occupancy_heat_map/occupancy_heat_map_service.dart';
|
||||
|
||||
class FakeOccupancyHeatMapService implements OccupancyHeatMapService {
|
||||
@override
|
||||
Future<List<OccupancyHeatMapModel>> load(GetOccupancyHeatMapParam param) {
|
||||
return Future.delayed(const Duration(milliseconds: 200), () {
|
||||
final now = DateTime.now();
|
||||
final startOfYear = DateTime(now.year, 1, 1);
|
||||
final endOfYear = DateTime(now.year, 12, 31);
|
||||
final daysInYear = endOfYear.difference(startOfYear).inDays + 1;
|
||||
|
||||
final List<OccupancyHeatMapModel> data = List.generate(
|
||||
daysInYear,
|
||||
(index) => OccupancyHeatMapModel(
|
||||
date: startOfYear.add(Duration(days: index)),
|
||||
occupancy: ((index + 1) * 10) % 100,
|
||||
),
|
||||
);
|
||||
|
||||
return data;
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
import 'package:syncrow_web/pages/analytics/models/occupancy_heat_map_model.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_occupancy_heat_map_param.dart';
|
||||
|
||||
abstract interface class OccupancyHeatMapService {
|
||||
Future<List<OccupancyHeatMapModel>> load(GetOccupancyHeatMapParam param);
|
||||
}
|
Reference in New Issue
Block a user