diff --git a/lib/screens/overview_screen.dart b/lib/screens/overview_screen.dart index c4da88f..30fd913 100644 --- a/lib/screens/overview_screen.dart +++ b/lib/screens/overview_screen.dart @@ -487,6 +487,16 @@ class _OverviewScreenState extends State { : null), periodMode: favourite.period, ), + onChartTap: () => _openChartFilter( + context, + labelIds: { + for (final id in favourite.labelIds) id, + }, + flow: favourite.metric == OverviewMetricMode.labels + ? favourite.flow + : null, + periodMode: favourite.period, + ), ); } @@ -609,6 +619,10 @@ class _OverviewScreenState extends State { flow: flow ?? (_metric == OverviewMetricMode.labels ? _labelFlow : null), ), + onChartTap: () => _openChartFilter( + context, + flow: _metric == OverviewMetricMode.labels ? _labelFlow : null, + ), ), const SizedBox(height: 14), if (_metric == OverviewMetricMode.flow) ...[ @@ -784,6 +798,34 @@ class _OverviewScreenState extends State { ), ); } + + /// Open Transactions with this chart's filters (no specific bar/column). + void _openChartFilter( + BuildContext context, { + Set? labelIds, + TxFlowFilter? flow, + OverviewPeriodMode? periodMode, + }) { + final mode = periodMode ?? _period; + final yearly = mode == OverviewPeriodMode.yearly; + final resolvedLabelIds = labelIds ?? + { + for (final id in _includedLabelIds) id, + }; + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => TransactionsScreen( + // Monthly charts show a year — open that whole year with the chart filter. + initialMonth: yearly ? null : DateTime(_cursor.year, 12), + initialYear: yearly ? null : _cursor.year, + initialLabelIds: resolvedLabelIds, + initialLabelMatch: LabelMatchMode.or, + initialFlow: flow, + initialSearchInMonthOnly: false, + ), + ), + ); + } } class _MonthBucket { @@ -1183,6 +1225,7 @@ class _ChartCard extends StatelessWidget { required this.availableLabelCount, required this.onLabelTap, required this.onMonthTap, + required this.onChartTap, this.onPeriodModeChanged, this.onEdit, }); @@ -1211,6 +1254,7 @@ class _ChartCard extends StatelessWidget { String? labelId, TxFlowFilter? flow, }) onMonthTap; + final VoidCallback onChartTap; final ValueChanged? onPeriodModeChanged; final VoidCallback? onEdit; @@ -1551,24 +1595,29 @@ class _ChartCard extends StatelessWidget { ], SizedBox( height: chartHeight, - child: metric == OverviewMetricMode.flow - ? _FlowBars( - months: months, - monthly: monthly, - currency: currency, - period: period, - onMonthTap: onMonthTap, - ) - : _LabelMonthBars( - months: months, - labelSpend: labelSpend, - amountByMonth: labelAmountByMonth, - currency: currency, - period: period, - barStyle: labelBarStyle, - invertChart: invertChart, - onMonthTap: onMonthTap, - ), + child: _TappableChartArea( + onChartTap: onChartTap, + child: metric == OverviewMetricMode.flow + ? _FlowBars( + months: months, + monthly: monthly, + currency: currency, + period: period, + onMonthTap: onMonthTap, + onChartTap: onChartTap, + ) + : _LabelMonthBars( + months: months, + labelSpend: labelSpend, + amountByMonth: labelAmountByMonth, + currency: currency, + period: period, + barStyle: labelBarStyle, + invertChart: invertChart, + onMonthTap: onMonthTap, + onChartTap: onChartTap, + ), + ), ), ], ), @@ -1577,6 +1626,69 @@ class _ChartCard extends StatelessWidget { } } +/// Lets empty-chart taps open the chart filter without double-firing when a bar +/// was just selected (fl_chart + parent [GestureDetector] both see the tap). +class _TappableChartArea extends StatefulWidget { + const _TappableChartArea({ + required this.onChartTap, + required this.child, + }); + + final VoidCallback onChartTap; + final Widget child; + + @override + State<_TappableChartArea> createState() => _TappableChartAreaState(); +} + +class _TappableChartAreaState extends State<_TappableChartArea> { + var _handling = false; + + void _runExclusive(VoidCallback action) { + if (_handling) return; + _handling = true; + action(); + Future.delayed(const Duration(milliseconds: 400), () { + if (mounted) _handling = false; + }); + } + + void openChartFilter() => _runExclusive(widget.onChartTap); + + void openBar(VoidCallback action) => _runExclusive(action); + + @override + Widget build(BuildContext context) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: openChartFilter, + child: _ChartTapScope( + openChartFilter: openChartFilter, + openBar: openBar, + child: widget.child, + ), + ); + } +} + +class _ChartTapScope extends InheritedWidget { + const _ChartTapScope({ + required this.openChartFilter, + required this.openBar, + required super.child, + }); + + final VoidCallback openChartFilter; + final void Function(VoidCallback action) openBar; + + static _ChartTapScope? maybeOf(BuildContext context) { + return context.getInheritedWidgetOfExactType<_ChartTapScope>(); + } + + @override + bool updateShouldNotify(_ChartTapScope oldWidget) => false; +} + class _FlowBars extends StatelessWidget { const _FlowBars({ required this.months, @@ -1584,6 +1696,7 @@ class _FlowBars extends StatelessWidget { required this.currency, required this.period, required this.onMonthTap, + required this.onChartTap, }); final List months; @@ -1595,6 +1708,7 @@ class _FlowBars extends StatelessWidget { String? labelId, TxFlowFilter? flow, }) onMonthTap; + final VoidCallback onChartTap; @override Widget build(BuildContext context) { @@ -1613,13 +1727,17 @@ class _FlowBars extends StatelessWidget { }); if (empty) { - return Center( - child: Text( - period == OverviewPeriodMode.yearly - ? 'No income or expenses in these years' - : 'No income or expenses in this year', - style: theme.textTheme.bodyMedium, - textAlign: TextAlign.center, + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: onChartTap, + child: Center( + child: Text( + period == OverviewPeriodMode.yearly + ? 'No income or expenses in these years' + : 'No income or expenses in this year', + style: theme.textTheme.bodyMedium, + textAlign: TextAlign.center, + ), ), ); } @@ -1632,15 +1750,25 @@ class _FlowBars extends StatelessWidget { touchCallback: (event, response) { if (event is! FlTapUpEvent) return; final spot = response?.spot; - if (spot == null) return; - final index = spot.touchedBarGroupIndex; - if (index < 0 || index >= months.length) return; + final index = spot?.touchedBarGroupIndex; + if (spot == null || + index == null || + index < 0 || + index >= months.length) { + // Parent GestureDetector also handles empty-area taps. + return; + } final rodIndex = spot.touchedRodDataIndex; // Rods: 0 = expense, 1 = income. final flow = rodIndex == 1 ? TxFlowFilter.income : TxFlowFilter.expense; - onMonthTap(months[index], flow: flow); + final scope = _ChartTapScope.maybeOf(context); + if (scope != null) { + scope.openBar(() => onMonthTap(months[index], flow: flow)); + } else { + onMonthTap(months[index], flow: flow); + } }, touchTooltipData: BarTouchTooltipData( getTooltipItem: (group, groupIndex, rod, rodIndex) { @@ -1755,6 +1883,7 @@ class _LabelMonthBars extends StatelessWidget { required this.barStyle, required this.invertChart, required this.onMonthTap, + required this.onChartTap, }); static const _maxSeriesLabels = 6; @@ -1771,6 +1900,7 @@ class _LabelMonthBars extends StatelessWidget { String? labelId, TxFlowFilter? flow, }) onMonthTap; + final VoidCallback onChartTap; double _plot(double value) => invertChart ? -value : value; @@ -1779,11 +1909,15 @@ class _LabelMonthBars extends StatelessWidget { final theme = Theme.of(context); final colors = theme.colorScheme; if (labelSpend.isEmpty) { - return Center( - child: Text( - 'No labeled activity in this period.\nTag transactions to see them here.', - style: theme.textTheme.bodyMedium, - textAlign: TextAlign.center, + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: onChartTap, + child: Center( + child: Text( + 'No labeled activity in this period.\nTag transactions to see them here.', + style: theme.textTheme.bodyMedium, + textAlign: TextAlign.center, + ), ), ); } @@ -1841,11 +1975,15 @@ class _LabelMonthBars extends StatelessWidget { } } if (maxY == 0 && minY == 0) { - return Center( - child: Text( - 'No labeled activity in this period.\nTag transactions to see them here.', - style: theme.textTheme.bodyMedium, - textAlign: TextAlign.center, + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: onChartTap, + child: Center( + child: Text( + 'No labeled activity in this period.\nTag transactions to see them here.', + style: theme.textTheme.bodyMedium, + textAlign: TextAlign.center, + ), ), ); } @@ -1949,9 +2087,14 @@ class _LabelMonthBars extends StatelessWidget { touchCallback: (event, response) { if (event is! FlTapUpEvent) return; final spot = response?.spot; - if (spot == null) return; - final index = spot.touchedBarGroupIndex; - if (index < 0 || index >= months.length) return; + final index = spot?.touchedBarGroupIndex; + if (spot == null || + index == null || + index < 0 || + index >= months.length) { + // Parent GestureDetector also handles empty-area taps. + return; + } String? labelId; if (stacked) { final stackIdx = spot.touchedStackItemIndex; @@ -1963,9 +2106,16 @@ class _LabelMonthBars extends StatelessWidget { if (rodIdx >= 0 && rodIdx < topLabels.length) { labelId = topLabels[rodIdx].label.id; } - // Last rod may be "Other" — open month with chart labels only. + // Last rod may be "Other" — open month with chart labels only. + } + final scope = _ChartTapScope.maybeOf(context); + if (scope != null) { + scope.openBar( + () => onMonthTap(months[index], labelId: labelId), + ); + } else { + onMonthTap(months[index], labelId: labelId); } - onMonthTap(months[index], labelId: labelId); }, touchTooltipData: BarTouchTooltipData( getTooltipItem: (group, groupIndex, rod, rodIndex) {