feat(todo): Reporting, with goals, streaks and what was done (ADR 0033, D36)

A daily and a weekly goal, each with its progress, streak and chart, days
off and vacation mode, and the history of completed tasks by project, all
worked out on the device from what the tasks keep. Today shows a quiet card
once a day when the goal is reached. No karma, nothing to share.

Whats-New: Reporting: set a daily and a weekly goal, follow your streaks and see what you've done, with days off and vacation, under Browse.
Whats-New-De: Auswertung: Lege ein Tages- und ein Wochenziel fest, verfolge deine Serien und sieh, was du erledigt hast, mit freien Tagen und Urlaub, in der Übersicht.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-11-06 19:58:53 +01:00
commit bd1b19ed32
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -28,6 +28,8 @@ import 'package:halkyon_todo_flutter/src/feature/project/widget/month_grid.dart'
import 'package:halkyon_todo_flutter/src/feature/project/widget/project_board.dart';
import 'package:halkyon_todo_flutter/src/feature/project/widget/project_calendar_view.dart';
import 'package:halkyon_todo_flutter/src/feature/project/widget/project_table.dart';
import 'package:halkyon_todo_flutter/src/feature/report/widget/goal_editor.dart';
import 'package:halkyon_todo_flutter/src/feature/report/widget/reporting_screen.dart';
import 'package:halkyon_todo_flutter/src/feature/section/data/section_repository.dart';
import 'package:halkyon_todo_flutter/src/feature/section/model/section.dart';
import 'package:halkyon_todo_flutter/src/feature/settings/data/settings_repository.dart';
@ -411,6 +413,98 @@ void main() {
expect(find.text('No labels yet.'), findsOneWidget);
});
group('Reporting (D36)', () {
Future<void> doneToday(WidgetTester tester, int count) async {
await tester.runAsync(() async {
final inbox = await ProjectRepository(
projectRows,
keys,
).ensureInbox();
final repository = TaskRepository(taskRows, keys);
for (var index = 0; index < count; index += 1) {
await repository.add(
TaskContent(
title: 'Done $index',
projectId: inbox,
completedAt: DateTime.now(),
),
);
}
});
projectRows.bringRemoteChanges();
taskRows.bringRemoteChanges();
await tester.pumpAndSettle();
}
Future<void> openReporting(WidgetTester tester) async {
await goTab(tester, 'Browse');
await tester.ensureVisible(find.byKey(reportingEntryKey));
await tester.pumpAndSettle();
await tester.tap(find.byKey(reportingEntryKey));
await tester.pumpAndSettle();
}
testWidgets('shows the goal, what was done today, and the history by '
'project', (tester) async {
await start(tester, usedBefore: true);
await doneToday(tester, 2);
await openReporting(tester);
expect(find.text('2 of 5 tasks'), findsOneWidget);
expect(find.text('Done in the last 7 days'), findsOneWidget);
await tester.tap(find.text('History'));
await tester.pumpAndSettle();
expect(find.text('Done 0'), findsOneWidget);
expect(find.text('Done 1'), findsOneWidget);
});
testWidgets("a goal of nothing can't be kept; one changed is, with the "
'days off and vacation', (tester) async {
await start(tester, usedBefore: true);
await openReporting(tester);
await tester.tap(find.text('Edit goals'));
await tester.pumpAndSettle();
await tester.enterText(find.byKey(dailyGoalKey), '0');
await tester.pumpAndSettle();
FilledButton save() => tester.widget<FilledButton>(
find.widgetWithText(FilledButton, 'Save'),
);
expect(save().onPressed, isNull);
await tester.enterText(find.byKey(dailyGoalKey), '3');
await tester.tap(find.byKey(vacationKey));
await tester.pumpAndSettle();
await tester.tap(find.text('Save'));
await tester.pumpAndSettle();
final kept = await tester.runAsync(
() => SettingsRepository(settingsRows, keys).read(),
);
expect(kept!.goals.daily, 3);
expect(kept.goals.onVacation, isTrue);
});
testWidgets('the goal reached shows one quiet card on Today, and once '
'dismissed none again that day', (tester) async {
await start(tester, usedBefore: true);
await doneToday(tester, 4);
expect(find.byType(GoalReachedCard), findsOneWidget);
expect(find.byKey(GoalReachedCard.dismissKey), findsNothing);
await doneToday(tester, 1);
expect(find.byKey(GoalReachedCard.dismissKey), findsOneWidget);
expect(find.textContaining('Daily goal reached'), findsOneWidget);
await tester.tap(find.byKey(GoalReachedCard.dismissKey));
await tester.pumpAndSettle();
settingsRows.bringRemoteChanges();
await tester.pumpAndSettle();
expect(find.byKey(GoalReachedCard.dismissKey), findsNothing);
});
});
group('comments (D36)', () {
Future<void> openTask(WidgetTester tester) async {
await tester.runAsync(() async {