feat(todo): a project's Calendar, Upcoming's Display and its 3-day grid (ADR 0033, D17)
A project's Display gains the Calendar layout: Upcoming's week strip and agenda, now shared as DayAgenda, over the project's tasks it keeps, with No date beside the month and Future occurrences, each repeating task also on the days it will come back (calendarOf on Repeat.next), muted, neither ticked off nor dragged. Upcoming gets a Display of its own, the same sheet over another target (DisplayTarget): Agenda or 3-day, done tasks, future occurrences, sorting, the filters by deadline, priority and label, and in the grid Colouring by priority, label or project. It is kept in the person's settings, one sealed row under an ID keyed by the account in the new synced table todo_settings (migration settings: the table, its index on spaceId and its foreign key to the space, cascading on delete). The 3-day grid shows today and the two days after, stepped or swiped three at a time: tasks without a time all day, timed ones as blocks as long as they last, overlapping ones side by side (layOutDay). A long press on a half hour adds a task there for half an hour; Overdue opens the overdue tasks with Reschedule. The board's separator builder names its BuildContext, as the root's analyzer plugin asks (never_discard_build_context). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
e4fbfb54d1
commit
2939437c74
3 changed files with 21845 additions and 314 deletions
|
|
@ -0,0 +1,174 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/project/model/project_calendar.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/model/repeat.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/model/task.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/model/task_views.dart';
|
||||
|
||||
/// A project's Calendar (ADR 0033, D17): its overdue tasks, then each day
|
||||
/// with its tasks and, where asked, the later times of repeating ones.
|
||||
void main() {
|
||||
// Sunday, 1 Nov 2026.
|
||||
const today = Day(2026, 11, 1);
|
||||
|
||||
OpenTask task(
|
||||
String id, {
|
||||
Day? day,
|
||||
int? time,
|
||||
Repeat? repeat,
|
||||
bool completed = false,
|
||||
}) => (
|
||||
id: id,
|
||||
content: TaskContent(
|
||||
title: id,
|
||||
projectId: 'p',
|
||||
completedAt: completed ? DateTime(2026, 11) : null,
|
||||
date: day == null ? null : TaskDate(day, time: time, repeat: repeat),
|
||||
),
|
||||
);
|
||||
|
||||
/// Each day's entries as `id` or, for a later time, `id*`.
|
||||
Map<Day, String> shown(ProjectCalendar calendar) => {
|
||||
for (final (day, entries) in calendar.days)
|
||||
if (entries.isNotEmpty)
|
||||
day: [
|
||||
for (final entry in entries)
|
||||
'${entry.task.id}${entry.occurrence ? '*' : ''}',
|
||||
].join(','),
|
||||
};
|
||||
|
||||
test('overdue first, then each day from today on with its tasks, those '
|
||||
'without a date and done overdue ones left out', () {
|
||||
final calendar = calendarOf(
|
||||
[
|
||||
task('none'),
|
||||
task('late', day: today.plus(-2)),
|
||||
task('done late', day: today.plus(-1), completed: true),
|
||||
task('later', day: today.plus(3)),
|
||||
task('now', day: today, time: 9 * 60),
|
||||
task('now early', day: today, time: 7 * 60),
|
||||
task('too far', day: today.plus(10)),
|
||||
],
|
||||
today,
|
||||
days: 7,
|
||||
);
|
||||
expect([for (final t in calendar.overdue) t.id], ['late']);
|
||||
expect(calendar.days, hasLength(7));
|
||||
expect(calendar.days.first.$1, today);
|
||||
expect(shown(calendar), {
|
||||
today: 'now early,now',
|
||||
today.plus(3): 'later',
|
||||
});
|
||||
});
|
||||
|
||||
test('a repeating task is shown on its later days only where asked', () {
|
||||
final daily = task(
|
||||
'daily',
|
||||
day: today,
|
||||
repeat: const Repeat(RepeatUnit.day),
|
||||
);
|
||||
expect(shown(calendarOf([daily], today, days: 3)), {today: 'daily'});
|
||||
expect(
|
||||
shown(calendarOf([daily], today, days: 3, occurrences: true)),
|
||||
{today: 'daily', today.plus(1): 'daily*', today.plus(2): 'daily*'},
|
||||
);
|
||||
});
|
||||
|
||||
test('an overdue repeating task comes next after today, as done today it '
|
||||
'would; a done one, one counted out or one ended no further', () {
|
||||
final weekly = task(
|
||||
'weekly',
|
||||
day: today.plus(-3),
|
||||
repeat: const Repeat(RepeatUnit.week),
|
||||
);
|
||||
expect(
|
||||
shown(calendarOf([weekly], today, days: 14, occurrences: true)),
|
||||
{today.plus(4): 'weekly*', today.plus(11): 'weekly*'},
|
||||
);
|
||||
final twice = task(
|
||||
'twice',
|
||||
day: today,
|
||||
repeat: const Repeat(RepeatUnit.day, left: 2),
|
||||
);
|
||||
final until = task(
|
||||
'until',
|
||||
day: today,
|
||||
repeat: Repeat(RepeatUnit.day, until: today.plus(1)),
|
||||
);
|
||||
final done = task(
|
||||
'done',
|
||||
day: today,
|
||||
completed: true,
|
||||
repeat: const Repeat(RepeatUnit.day),
|
||||
);
|
||||
expect(
|
||||
shown(
|
||||
calendarOf(
|
||||
[twice, until, done],
|
||||
today,
|
||||
days: 7,
|
||||
occurrences: true,
|
||||
),
|
||||
),
|
||||
{
|
||||
today: 'done,twice,until',
|
||||
today.plus(1): 'twice*,until*',
|
||||
today.plus(2): 'twice*',
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test('a later time is the task on that day, its ID kept', () {
|
||||
final calendar = calendarOf(
|
||||
[
|
||||
task(
|
||||
'daily',
|
||||
day: today,
|
||||
time: 6 * 60,
|
||||
repeat: const Repeat(RepeatUnit.day),
|
||||
),
|
||||
],
|
||||
today,
|
||||
days: 2,
|
||||
occurrences: true,
|
||||
);
|
||||
final later = calendar.days[1].$2.single;
|
||||
expect(later.occurrence, isTrue);
|
||||
expect(later.task.id, 'daily');
|
||||
expect(later.task.content.date?.day, today.plus(1));
|
||||
expect(later.task.content.date?.time, 6 * 60);
|
||||
});
|
||||
|
||||
test('the days with open tasks, beyond those listed too, and with later '
|
||||
'times; not those with done ones only', () {
|
||||
final calendar = calendarOf(
|
||||
[
|
||||
task('a', day: today.plus(2)),
|
||||
task('b', day: today, completed: true),
|
||||
task('c', day: today.plus(5), repeat: const Repeat(RepeatUnit.day)),
|
||||
task('d', day: today.plus(40)),
|
||||
],
|
||||
today,
|
||||
days: 7,
|
||||
occurrences: true,
|
||||
);
|
||||
expect(calendar.busy, {
|
||||
today.plus(2),
|
||||
today.plus(5),
|
||||
today.plus(6),
|
||||
today.plus(40),
|
||||
});
|
||||
});
|
||||
|
||||
test("a day's tasks in another order where asked", () {
|
||||
final calendar = calendarOf(
|
||||
[
|
||||
task('b', day: today, time: 9 * 60),
|
||||
task('a', day: today, time: 10 * 60),
|
||||
],
|
||||
today,
|
||||
days: 1,
|
||||
order: (x, y) => x.id.compareTo(y.id),
|
||||
);
|
||||
expect(shown(calendar), {today: 'a,b'});
|
||||
});
|
||||
}
|
||||
|
|
@ -31,7 +31,9 @@ void main() {
|
|||
test("a project's Display reads back as it was set, by names; left "
|
||||
'as at first, nothing of it is written (D17)', () {
|
||||
const view = ProjectView(
|
||||
layout: ProjectLayout.calendar,
|
||||
completed: true,
|
||||
occurrences: true,
|
||||
grouping: TaskGrouping.label,
|
||||
sorting: TaskSorting.priority,
|
||||
date: DayRange.next7Days,
|
||||
|
|
@ -46,7 +48,9 @@ void main() {
|
|||
utf8.decode(Uint8List.sublistView(encodeProject(home))),
|
||||
) as Map<String, Object?>;
|
||||
expect(written['view'], {
|
||||
'layout': 'calendar',
|
||||
'completed': true,
|
||||
'occurrences': true,
|
||||
'grouping': 'label',
|
||||
'sorting': 'priority',
|
||||
'date': 'next7Days',
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ void main() {
|
|||
expect(view.isDefault, isTrue);
|
||||
expect(view.layout, ProjectLayout.list);
|
||||
expect(view.completed, isFalse);
|
||||
expect(view.occurrences, isFalse, reason: "a Calendar's later times");
|
||||
expect(view.grouping, TaskGrouping.none);
|
||||
expect(view.sorting, TaskSorting.manual);
|
||||
expect(view.canReorder, isTrue);
|
||||
|
|
|
|||
Loading…
Reference in a new issue