feat(reports): controlled ReportDateFilter

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
2026-09-28 14:13:33 +08:00
parent be16a3cd0d
commit 91fb023ef2
2 changed files with 95 additions and 4 deletions
+43 -4
View File
@@ -5,14 +5,47 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../providers/reports_provider.dart';
import '../../utils/app_time.dart';
/// Returns a [ReportDateRange] spanning the calendar month that contains [now].
/// start = DateTime(y, m, 1), end = DateTime(y, m+1, 1), label = 'This Month'.
ReportDateRange thisMonthRange(DateTime now) {
return ReportDateRange(
start: DateTime(now.year, now.month, 1),
end: DateTime(now.year, now.month + 1, 1),
label: 'This Month',
);
}
/// A Metabase-inspired date filter with quick presets, relative offsets,
/// and a custom date-range picker.
///
/// Two construction modes:
/// - Default `ReportDateFilter({key})` — provider-bound, reads/writes
/// [reportDateRangeProvider].
/// - `ReportDateFilter.controlled({key, value, onChanged})` — controlled;
/// displays [value] and calls [onChanged] on apply without touching the
/// provider.
class ReportDateFilter extends ConsumerWidget {
const ReportDateFilter({super.key});
/// Provider-bound mode.
const ReportDateFilter({super.key})
: _value = null,
_onChanged = null;
/// Controlled mode: does NOT read or write [reportDateRangeProvider].
const ReportDateFilter.controlled({
super.key,
required ReportDateRange value,
required ValueChanged<ReportDateRange> onChanged,
}) : _value = value,
_onChanged = onChanged;
final ReportDateRange? _value;
final ValueChanged<ReportDateRange>? _onChanged;
bool get _isControlled => _value != null;
@override
Widget build(BuildContext context, WidgetRef ref) {
final range = ref.watch(reportDateRangeProvider);
final range = _isControlled ? _value! : ref.watch(reportDateRangeProvider);
final theme = Theme.of(context);
final colors = theme.colorScheme;
final text = theme.textTheme;
@@ -51,12 +84,18 @@ class ReportDateFilter extends ConsumerWidget {
}
void _showDateFilterDialog(BuildContext context, WidgetRef ref) {
final current =
_isControlled ? _value! : ref.read(reportDateRangeProvider);
m3ShowDialog(
context: context,
builder: (ctx) => _DateFilterDialog(
current: ref.read(reportDateRangeProvider),
current: current,
onApply: (newRange) {
ref.read(reportDateRangeProvider.notifier).state = newRange;
if (_isControlled) {
_onChanged!(newRange);
} else {
ref.read(reportDateRangeProvider.notifier).state = newRange;
}
},
),
);
+52
View File
@@ -0,0 +1,52 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:tasq/providers/reports_provider.dart';
import 'package:tasq/screens/reports/report_date_filter.dart';
import 'package:tasq/utils/app_time.dart';
void main() {
setUpAll(() {
AppTime.initialize();
});
group('ReportDateFilter.controlled', () {
testWidgets(
'emits via onChanged and never writes provider',
(tester) async {
final now = DateTime(2025, 1, 15);
final captured = <ReportDateRange>[];
final container = ProviderContainer();
addTearDown(container.dispose);
await tester.pumpWidget(
UncontrolledProviderScope(
container: container,
child: MaterialApp(
home: Scaffold(
body: ReportDateFilter.controlled(
value: thisMonthRange(now),
onChanged: captured.add,
),
),
),
),
);
// Tap the "Change" button to open the dialog
await tester.tap(find.text('Change'));
await tester.pumpAndSettle();
// Tap the "Yesterday" preset chip
await tester.tap(find.text('Yesterday'));
await tester.pumpAndSettle();
// The onChanged callback received Yesterday
expect(captured.single.label, 'Yesterday');
// The provider was NOT touched — still at its default label
expect(container.read(reportDateRangeProvider).label, 'Last 30 Days');
},
);
});
}