feat(reports): controlled ReportDateFilter
Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -5,14 +5,47 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import '../../providers/reports_provider.dart';
|
import '../../providers/reports_provider.dart';
|
||||||
import '../../utils/app_time.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,
|
/// A Metabase-inspired date filter with quick presets, relative offsets,
|
||||||
/// and a custom date-range picker.
|
/// 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 {
|
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
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final range = ref.watch(reportDateRangeProvider);
|
final range = _isControlled ? _value! : ref.watch(reportDateRangeProvider);
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final colors = theme.colorScheme;
|
final colors = theme.colorScheme;
|
||||||
final text = theme.textTheme;
|
final text = theme.textTheme;
|
||||||
@@ -51,12 +84,18 @@ class ReportDateFilter extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _showDateFilterDialog(BuildContext context, WidgetRef ref) {
|
void _showDateFilterDialog(BuildContext context, WidgetRef ref) {
|
||||||
|
final current =
|
||||||
|
_isControlled ? _value! : ref.read(reportDateRangeProvider);
|
||||||
m3ShowDialog(
|
m3ShowDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => _DateFilterDialog(
|
builder: (ctx) => _DateFilterDialog(
|
||||||
current: ref.read(reportDateRangeProvider),
|
current: current,
|
||||||
onApply: (newRange) {
|
onApply: (newRange) {
|
||||||
|
if (_isControlled) {
|
||||||
|
_onChanged!(newRange);
|
||||||
|
} else {
|
||||||
ref.read(reportDateRangeProvider.notifier).state = newRange;
|
ref.read(reportDateRangeProvider.notifier).state = newRange;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user