feat(todo): the Plan beside Upcoming, with what has no date (ADR 0033, D34)

Upcoming's 3-day grid gets the Plan Today's calendar has, through what
the two tabs now share (PlanHost). Its new last group holds the open
tasks of no day, so a week can be planned from the Inbox by dropping them
on an hour; they aren't counted on the button, which says what is due.

Whats-New: Upcoming's 3-day view has the Plan too, and tasks without a date can be dragged from it onto any day.
Whats-New-De: Auch die 3-Tage-Ansicht unter Demnächst hat den Plan, und Aufgaben ohne Datum lassen sich von dort auf jeden Tag ziehen.

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-05 19:37:55 +01:00
commit 31ff8d7e91
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
2 changed files with 342 additions and 159 deletions

View file

@ -36,6 +36,7 @@ import 'package:halkyon_todo_flutter/src/tabs/hour_grid.dart';
import 'package:halkyon_todo_flutter/src/tabs/plan_panel.dart';
import 'package:halkyon_todo_flutter/src/tabs/today_icon.dart';
import 'package:halkyon_todo_flutter/src/tabs/today_tab.dart';
import 'package:halkyon_todo_flutter/src/tabs/upcoming_tab.dart';
import 'package:halkyon_todo_flutter/src/tabs/week_strip.dart';
import 'package:halkyon_ui/halkyon_ui.dart';
import 'package:intl/intl.dart';
@ -1133,6 +1134,44 @@ void main() {
expect(find.text('Far'), findsOneWidget);
});
testWidgets('its Plan shows in 3-day alone; a task of no date dropped on '
"a later day's hour gets that day and time (D34)", (tester) async {
await start(tester, usedBefore: true);
await seed(
tester,
TaskContent(title: 'Someday', projectId: inbox()),
);
await goTab(tester, 'Upcoming');
expect(find.byKey(UpcomingTab.planKey), findsNothing);
await threeDay(tester);
expect(find.byKey(UpcomingTab.planKey), findsOneWidget);
tester.view.physicalSize = const Size(1400, 900);
await tester.pumpAndSettle();
expect(find.text('No date'), findsOneWidget);
final today = Day.of(DateTime.now());
final row = tester.getCenter(find.text('Someday'));
final column = tester.getTopLeft(
find.byKey(HourGrid.columnKey(today.plus(1))),
);
final gesture = await tester.startGesture(row);
await tester.pump(kLongPressTimeout + const Duration(milliseconds: 50));
final to = column + const Offset(30, 11 * 60 + 5);
for (var step = 1; step <= 10; step++) {
await gesture.moveTo(Offset.lerp(row, to, step / 10)!);
await tester.pump(const Duration(milliseconds: 16));
}
await gesture.up();
await tester.pumpAndSettle();
final date = (await tester.runAsync(
() => tasks().all(),
))!.single.content!.date!;
expect(date.day, today.plus(1));
expect(date.time, inInclusiveRange(10 * 60 + 30, 11 * 60 + 30));
expect(date.time! % 15, 0);
});
testWidgets(
'a click adds a task at that quarter hour, for half an hour',
(tester) async {

View file

@ -135,6 +135,7 @@ void main() {
expect(plan.toPlan.map((t) => t.id), ['open']);
expect(plan.timed.map((t) => t.id), ['eight', 'nine']);
expect(plan.count, 4);
expect(plan.noDate.map((t) => t.id), ['undated']);
final home = planOf(tasks, today, projectIds: {'home'});
expect(home.overdue, isEmpty);
@ -142,4 +143,35 @@ void main() {
expect(home.timed.map((t) => t.id), ['eight']);
expect(home.count, 2);
});
test('the Plan keeps the tasks of no day apart, uncounted (D34)', () {
const today = Day(2026, 11, 5);
OpenTask task(String id, {String? parentId, bool completed = false}) => (
id: id,
content: TaskContent(
title: id,
projectId: id == 'other' ? 'home' : 'inbox',
parentId: parentId,
completedAt: completed ? DateTime(2026, 11, 4) : null,
),
);
final tasks = [
task('free'),
task('other'),
task('sub', parentId: 'free'),
task('done', completed: true),
];
final plan = planOf(tasks, today);
expect([for (final task in plan.noDate) task.id], ['free', 'other']);
expect(plan.count, 0);
expect(plan.isEmpty, isFalse);
expect(
[
for (final task in planOf(tasks, today, projectIds: {'home'}).noDate)
task.id,
],
['other'],
);
});
}