Skip to content

Commit

Permalink
refactors analytics range picker
Browse files Browse the repository at this point in the history
  • Loading branch information
just-seba committed Sep 6, 2024
1 parent 559804f commit 11b717e
Show file tree
Hide file tree
Showing 7 changed files with 255 additions and 169 deletions.
6 changes: 6 additions & 0 deletions app/lib/ui/analytics/analytics_period.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
enum AnalyticsPeriod {
day,
week,
month,
year,
}
127 changes: 127 additions & 0 deletions app/lib/ui/analytics/analytics_range_picker.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import 'package:emma/ui/analytics/analytics_period.dart';
import 'package:emma/ui/analytics/analytics_view_model.dart';
import 'package:emma/ui/app_icons.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:signals/signals_flutter.dart';

class AnalyticsRangePicker extends StatefulWidget {
const AnalyticsRangePicker({
super.key,
required this.viewModel,
});

final AnalyticsViewModel viewModel;

@override
State<AnalyticsRangePicker> createState() => _AnalyticsRangePickerState();
}

class _AnalyticsRangePickerState extends State<AnalyticsRangePicker> {
late final TextEditingController _rangeTextController;
late final void Function() _disposeRangeTextEffect;

AnalyticsViewModel get _vm => widget.viewModel;

@override
void initState() {
_rangeTextController = TextEditingController(
text: _getRangeText(_vm.period.value, _vm.range.value));

_disposeRangeTextEffect = effect(
() => _rangeTextController.text =
_getRangeText(_vm.period.value, _vm.range.value),
debugLabel: "analytics.screen.rangeText",
);

super.initState();
}

@override
void dispose() {
_disposeRangeTextEffect();
_rangeTextController.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Expanded(
child: Watch(
(context) => SegmentedButton<AnalyticsPeriod>(
segments: const [
ButtonSegment(
value: AnalyticsPeriod.day,
label: Text("Tag"),
),
ButtonSegment(
value: AnalyticsPeriod.week,
label: Text("Woche"),
),
ButtonSegment(
value: AnalyticsPeriod.month,
label: Text("Monat"),
),
ButtonSegment(
value: AnalyticsPeriod.year,
label: Text("Jahr"),
)
],
selected: {_vm.period.value},
showSelectedIcon: false,
onSelectionChanged: (selection) =>
_vm.setPeriod(selection.first),
),
),
),
],
),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
IconButton(
onPressed: _vm.setPreviousRange,
icon: const Icon(AppIcons.arrow_prev)),
Expanded(
child: TextField(
controller: _rangeTextController,
readOnly: true,
textAlign: TextAlign.center,
),
),
Watch(
(context) => IconButton(
onPressed:
_vm.canSetNextRange.value ? _vm.setNextRange : null,
icon: const Icon(AppIcons.arrow_next)),
),
],
),
],
);
}

static String _getRangeText(AnalyticsPeriod period, DateTimeRange range) {
final start = range.start;
switch (period) {
case AnalyticsPeriod.day:
return DateFormat.yMd().format(start);

case AnalyticsPeriod.week:
final end = range.end.subtract(const Duration(seconds: 1));
final f = DateFormat.yMd();
return "${f.format(start)} - ${f.format(end)}";

case AnalyticsPeriod.month:
return DateFormat.yMMM().format(start);

case AnalyticsPeriod.year:
return DateFormat.y().format(start);
}
}
}
161 changes: 15 additions & 146 deletions app/lib/ui/analytics/analytics_screen.dart
Original file line number Diff line number Diff line change
@@ -1,167 +1,36 @@
import 'package:emma/ui/analytics/analytics_range_picker.dart';
import 'package:emma/ui/analytics/analytics_view_model.dart';
import 'package:emma/ui/analytics/consumption_overview.dart';
import 'package:emma/ui/analytics/production_overview.dart';
import 'package:emma/ui/app_icons.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';

// AutomaticKeepAliveClientMixin to keep alive
// https://pub.dev/packages/month_year_picker
class AnalyticsScreen extends StatefulWidget {
const AnalyticsScreen({super.key});

@override
State<AnalyticsScreen> createState() => _AnalyticsScreenState();
}

enum AnalyticsPeriod {
day,
week,
month,
year,
}

class _AnalyticsScreenState extends State<AnalyticsScreen> {
var _period = AnalyticsPeriod.day;
var _range = DateTimeRange(start: _today(), end: _today());

String get _periodText {
return switch (_period) {
AnalyticsPeriod.day => "Tag",
AnalyticsPeriod.week => "Woche",
AnalyticsPeriod.month => "Monat",
AnalyticsPeriod.year => "Jahr",
};
}

String get _rangeText {
final start = _range.start;
switch (_period) {
case AnalyticsPeriod.day:
return DateFormat.yMd().format(start);
case AnalyticsPeriod.week:
final end = _range.end.subtract(const Duration(seconds: 1));
final f = DateFormat.yMd();
return "${f.format(start)} - ${f.format(end)}";
case AnalyticsPeriod.month:
return DateFormat.yMMM().format(start);
case AnalyticsPeriod.year:
return DateFormat.y().format(start);
}
}
final _vm = AnalyticsViewModel();

@override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Wrap(
runSpacing: 32,
children: [
Column(
children: [
Row(
children: [
IconButton(
onPressed: _setPreviousPeriod,
icon: const Icon(AppIcons.arrow_prev)),
Expanded(
child: Center(
child: Text(_periodText),
)),
IconButton(
onPressed: _setNextPeriod,
icon: const Icon(AppIcons.arrow_next)),
],
),
Row(
children: [
IconButton(
onPressed: _setPreviousRange,
icon: const Icon(AppIcons.arrow_prev)),
Expanded(
child: Center(
child: Text(_rangeText),
)),
IconButton(
onPressed: _canSetNextRange() ? _setNextRange : null,
icon: const Icon(AppIcons.arrow_next)),
],
),
],
),
const ProductionOverview(),
const ConsumptionOverview(),
],
),
body: ListView(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
children: [
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: AnalyticsRangePicker(viewModel: _vm),
),
const SizedBox(height: 32),
const ConsumptionOverview(),
const SizedBox(height: 32),
const ProductionOverview(),
],
),
);
}

void _setNextPeriod() {
var index = AnalyticsPeriod.values.indexOf(_period);
index = (index + 1) % AnalyticsPeriod.values.length;
_setPeriod(AnalyticsPeriod.values[index]);
}

void _setPreviousPeriod() {
var index = AnalyticsPeriod.values.indexOf(_period);
index = (index - 1) % AnalyticsPeriod.values.length;
_setPeriod(AnalyticsPeriod.values[index]);
}

void _setPeriod(AnalyticsPeriod period) {
setState(() {
_period = period;
_range = _computeDateRange(period, _today());
});
}

void _setPreviousRange() {
final DateTimeRange range = _computeDateRange(
_period, _range.start.subtract(const Duration(days: 1)));

setState(() {
_range = range;
});
}

bool _canSetNextRange() {
final max = _computeDateRange(_period, _today());
return _range.start.isBefore(max.start);
}

void _setNextRange() {
final DateTimeRange range =
_computeDateRange(_period, _range.end.add(const Duration(days: 1)));

setState(() {
_range = range;
});
}

static DateTimeRange _computeDateRange(
AnalyticsPeriod period, DateTime seed) {
switch (period) {
case AnalyticsPeriod.day:
return DateTimeRange(start: seed, end: seed);
case AnalyticsPeriod.week:
// Monday = 1
final dayOfWeek = seed.weekday;
final start = seed.subtract(Duration(days: dayOfWeek - 1));
final end = start.add(const Duration(days: 7));
return DateTimeRange(start: start, end: end);
case AnalyticsPeriod.month:
return DateTimeRange(
start: DateTime(seed.year, seed.month),
end: DateTime(seed.year, seed.month + 1));
case AnalyticsPeriod.year:
return DateTimeRange(
start: DateTime(seed.year), end: DateTime(seed.year + 1));
}
}

static DateTime _today() {
final now = DateTime.now();
return DateTime(now.year, now.month, now.day);
}
}
76 changes: 76 additions & 0 deletions app/lib/ui/analytics/analytics_view_model.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import 'package:emma/ui/analytics/analytics_period.dart';
import 'package:flutter/material.dart';
import 'package:signals/signals.dart';

class AnalyticsViewModel {
final _period = signal(
AnalyticsPeriod.day,
debugLabel: "analytics.vm.period",
);

final _range = signal(
DateTimeRange(start: _today(), end: _today()),
debugLabel: "analytics.vm.range",
);

ReadonlySignal<AnalyticsPeriod> get period => _period;
ReadonlySignal<DateTimeRange> get range => _range;
late final ReadonlySignal<bool> canSetNextRange = computed(
() {
final max = _computeDateRange(_period.value, _today());
return _range.value.start.isBefore(max.start);
},
debugLabel: "analytics.vm.canSetNextRange",
);

void setPeriod(AnalyticsPeriod period) {
batch(() {
_period.value = period;
_range.value = _computeDateRange(period, _today());
});
}

void setNextRange() {
if (!canSetNextRange.value) {
return;
}

_range.value = _computeDateRange(
_period.value,
_range.value.end.add(const Duration(days: 1)),
);
}

void setPreviousRange() {
_range.value = _computeDateRange(
_period.value,
_range.value.start.subtract(const Duration(days: 1)),
);
}

static DateTimeRange _computeDateRange(
AnalyticsPeriod period, DateTime seed) {
switch (period) {
case AnalyticsPeriod.day:
return DateTimeRange(start: seed, end: seed);
case AnalyticsPeriod.week:
// Monday = 1
final dayOfWeek = seed.weekday;
final start = seed.subtract(Duration(days: dayOfWeek - 1));
final end = start.add(const Duration(days: 7));
return DateTimeRange(start: start, end: end);
case AnalyticsPeriod.month:
return DateTimeRange(
start: DateTime(seed.year, seed.month),
end: DateTime(seed.year, seed.month + 1));
case AnalyticsPeriod.year:
return DateTimeRange(
start: DateTime(seed.year), end: DateTime(seed.year + 1));
}
}

static DateTime _today() {
final now = DateTime.now();
return DateTime(now.year, now.month, now.day);
}
}
Loading

0 comments on commit 11b717e

Please sign in to comment.