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
2 changed files with 21845 additions and 314 deletions
|
|
@ -0,0 +1,78 @@
|
|||
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/day_grid.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/model/task.dart';
|
||||
|
||||
/// A day of the 3-day grid (ADR 0033, D17): its timed tasks as blocks, side
|
||||
/// by side where they overlap.
|
||||
void main() {
|
||||
const day = Day(2026, 11, 2);
|
||||
|
||||
CalendarEntry entry(String id, {int? time, Duration? duration}) => (
|
||||
task: (
|
||||
id: id,
|
||||
content: TaskContent(
|
||||
title: id,
|
||||
projectId: 'p',
|
||||
date: TaskDate(day, time: time, duration: duration),
|
||||
),
|
||||
),
|
||||
occurrence: false,
|
||||
);
|
||||
|
||||
String laid(List<GridBlock> blocks) => [
|
||||
for (final b in blocks)
|
||||
'${b.entry.task.id} ${b.start}-${b.end} ${b.lane}/${b.lanes}',
|
||||
].join('; ');
|
||||
|
||||
test('a task without a time is all day, not a block', () {
|
||||
final (:allDay, :blocks) = layOutDay([
|
||||
entry('Shop'),
|
||||
entry('Call', time: 9 * 60),
|
||||
]);
|
||||
expect([for (final e in allDay) e.task.id], ['Shop']);
|
||||
expect(laid(blocks), 'Call 540-570 0/1');
|
||||
});
|
||||
|
||||
test('a block lasts its duration, half an hour at least', () {
|
||||
final (:allDay, :blocks) = layOutDay([
|
||||
entry('Run', time: 7 * 60, duration: const Duration(minutes: 90)),
|
||||
entry('Pills', time: 12 * 60, duration: const Duration(minutes: 5)),
|
||||
]);
|
||||
expect(laid(blocks), 'Run 420-510 0/1; Pills 720-750 0/1');
|
||||
});
|
||||
|
||||
test('overlapping blocks share their width; one after them has it all', () {
|
||||
final (:allDay, :blocks) = layOutDay([
|
||||
entry('B', time: 9 * 60 + 30),
|
||||
entry('A', time: 9 * 60, duration: const Duration(hours: 1)),
|
||||
entry('C', time: 9 * 60 + 30),
|
||||
entry('D', time: 10 * 60 + 15),
|
||||
entry('E', time: 11 * 60),
|
||||
]);
|
||||
expect(
|
||||
laid(blocks),
|
||||
'A 540-600 0/3; B 570-600 1/3; C 570-600 2/3; '
|
||||
'D 615-645 0/1; E 660-690 0/1',
|
||||
);
|
||||
});
|
||||
|
||||
test('a lane freed is taken again within the same cluster', () {
|
||||
final (:allDay, :blocks) = layOutDay([
|
||||
entry('Long', time: 9 * 60, duration: const Duration(hours: 2)),
|
||||
entry('First', time: 9 * 60),
|
||||
entry('Second', time: 10 * 60),
|
||||
]);
|
||||
expect(
|
||||
laid(blocks),
|
||||
'Long 540-660 0/2; First 540-570 1/2; Second 600-630 1/2',
|
||||
);
|
||||
});
|
||||
|
||||
test('a block running past midnight ends at it', () {
|
||||
final (:allDay, :blocks) = layOutDay([
|
||||
entry('Late', time: 23 * 60 + 30, duration: const Duration(hours: 2)),
|
||||
]);
|
||||
expect(laid(blocks), 'Late 1410-1440 0/1');
|
||||
});
|
||||
}
|
||||
|
|
@ -219,7 +219,11 @@ void main() {
|
|||
);
|
||||
final weekdays = parse('Stand-up every weekday 9am').date;
|
||||
expect(weekdays?.repeat?.weekdays, {1, 2, 3, 4, 5});
|
||||
expect(weekdays?.day, const Day(2026, 11, 2), reason: 'Monday, the first');
|
||||
expect(
|
||||
weekdays?.day,
|
||||
const Day(2026, 11, 2),
|
||||
reason: 'Monday, the first',
|
||||
);
|
||||
});
|
||||
|
||||
test('every! counts from when it was done', () {
|
||||
|
|
@ -237,7 +241,10 @@ void main() {
|
|||
expect(parse('Zahlen heute', words: de).date?.day, today);
|
||||
expect(parse('Zahlen morgen', words: de).date?.day, today.plus(1));
|
||||
expect(parse('Zahlen übermorgen', words: de).date?.day, today.plus(2));
|
||||
expect(parse('Sport Montag', words: de).date?.day, const Day(2026, 11, 2));
|
||||
expect(
|
||||
parse('Sport Montag', words: de).date?.day,
|
||||
const Day(2026, 11, 2),
|
||||
);
|
||||
expect(parse('Anrufen in 3 Tagen', words: de).date?.day, today.plus(3));
|
||||
expect(parse('Zahlen morgen', words: de).title, 'Zahlen');
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue