Wire Backlog nav, Break up, Settings dialog + merge Backlog Board feature #20

Open
pyr0ball wants to merge 26 commits from Circuit-Forge/focus-flow:feat/9-12-13-wire-backlog-nav-and-modal-actions into main
4 changed files with 476 additions and 14 deletions
Showing only changes of commit 82f1fd43b7 - Show all commits

View file

@ -83,3 +83,20 @@ Freeform Tags Metadata.md` for a later migration-backed feature.
4. Focused application-management tests cover stable empty columns, bucket and
summary consistency, search/filter behavior, child previews, not-found detail
responses, read-only slot suggestions, and missing-duration handling.
## Block 02 Demo Seed Data
1. The Flutter demo composition now seeds Backlog board data as real scheduler
`Task` and `ProjectProfile` records, not widget-local mock rows.
2. The seed matches the feasible textual targets from Chunk 2.7: 24 backlog
tasks, project counts of Home 5 / Work 8 / Personal 5 / Learning 4 / Side
Project 2, priority counts of High 7 / Medium 9 / Low 8, and 12h 15m total
estimated time.
3. The plan/mockup contains a count conflict: the mockup summary says 24 tasks,
while the listed column badge counts add to 22. The current seed keeps 24
repository-backed backlog tasks so the summary target stays truthful through
public read models.
4. `Review Q3 budget` is seeded with Work, 60 minutes, High priority,
medium reward, and easy difficulty. Finance/review freeform tags, notes text,
and exact drawer slot copy remain deferred with the approved metadata scope
decision above.

View file

@ -3,6 +3,30 @@
part of '../demo_scheduler_composition.dart';
/// Top-level helper that performs the `_demoProjects` operation for this file.
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
List<ProjectProfile> _demoProjects() {
return [
ProjectProfile(id: 'home', name: 'Home', colorKey: 'project-home'),
ProjectProfile(id: 'work', name: 'Work', colorKey: 'project-work'),
ProjectProfile(
id: 'personal',
name: 'Personal',
colorKey: 'project-personal',
),
ProjectProfile(
id: 'learning',
name: 'Learning',
colorKey: 'project-learning',
),
ProjectProfile(
id: 'side-project',
name: 'Side Project',
colorKey: 'project-side-project',
),
];
}
/// Top-level helper that performs the `_todaySeedTasks` operation for this file.
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
List<Task> _todaySeedTasks(CivilDate date, DateTime createdAt) {
@ -123,6 +147,284 @@ List<Task> _todaySeedTasks(CivilDate date, DateTime createdAt) {
];
}
/// Top-level helper that performs the `_backlogSeedTasks` operation for this file.
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
List<Task> _backlogSeedTasks(CivilDate date, DateTime createdAt) {
return [
_backlogSeedTask(
id: 'reply-to-3-emails',
title: 'Reply to 3 emails',
projectId: 'home',
order: 1,
priority: PriorityLevel.medium,
reward: RewardLevel.low,
difficulty: DifficultyLevel.easy,
durationMinutes: 10,
createdAt: createdAt,
),
_backlogSeedTask(
id: 'update-project-status',
title: 'Update project status',
projectId: 'work',
order: 2,
priority: PriorityLevel.medium,
reward: RewardLevel.low,
difficulty: DifficultyLevel.easy,
durationMinutes: 15,
createdAt: createdAt,
),
_backlogSeedTask(
id: 'schedule-dentist-appt',
title: 'Schedule dentist appt',
projectId: 'personal',
order: 3,
priority: PriorityLevel.medium,
reward: RewardLevel.low,
difficulty: DifficultyLevel.easy,
durationMinutes: 10,
createdAt: createdAt,
),
_backlogSeedTask(
id: 'clean-up-downloads-folder',
title: 'Clean up downloads folder',
projectId: 'home',
order: 4,
priority: PriorityLevel.medium,
reward: RewardLevel.low,
difficulty: DifficultyLevel.easy,
durationMinutes: 10,
createdAt: createdAt,
),
_backlogSeedTask(
id: 'plan-tomorrow',
title: 'Plan tomorrow',
projectId: 'work',
order: 5,
priority: PriorityLevel.medium,
reward: RewardLevel.low,
difficulty: DifficultyLevel.easy,
durationMinutes: 10,
createdAt: createdAt,
),
_backlogSeedTask(
id: 'review-q3-budget',
title: 'Review Q3 budget',
projectId: 'work',
order: 6,
priority: PriorityLevel.high,
reward: RewardLevel.medium,
difficulty: DifficultyLevel.easy,
durationMinutes: 60,
createdAt: createdAt,
),
_backlogSeedTask(
id: 'plan-next-week',
title: 'Plan next week',
projectId: 'work',
order: 7,
priority: PriorityLevel.high,
reward: RewardLevel.high,
difficulty: DifficultyLevel.medium,
durationMinutes: 30,
createdAt: createdAt,
),
_backlogSeedTask(
id: 'define-weekly-goals',
title: 'Define weekly goals',
projectId: 'work',
order: 8,
priority: PriorityLevel.high,
reward: RewardLevel.medium,
difficulty: DifficultyLevel.medium,
durationMinutes: 15,
createdAt: createdAt,
),
_backlogSeedTask(
id: 'time-block-schedule',
title: 'Time block schedule',
projectId: 'work',
order: 9,
priority: PriorityLevel.high,
reward: RewardLevel.medium,
difficulty: DifficultyLevel.medium,
durationMinutes: 15,
createdAt: createdAt,
),
_backlogSeedTask(
id: 'organize-kitchen-drawers',
title: 'Organize kitchen drawers',
projectId: 'home',
order: 10,
priority: PriorityLevel.low,
reward: RewardLevel.low,
difficulty: DifficultyLevel.medium,
durationMinutes: 45,
createdAt: createdAt,
),
_backlogSeedTask(
id: 'renew-car-registration',
title: 'Renew car registration',
projectId: 'home',
order: 11,
priority: PriorityLevel.high,
reward: RewardLevel.low,
difficulty: DifficultyLevel.medium,
durationMinutes: 25,
createdAt: createdAt,
),
_backlogSeedTask(
id: 'build-analytics-dashboard',
title: 'Build analytics dashboard',
projectId: 'work',
order: 12,
priority: PriorityLevel.high,
reward: RewardLevel.high,
difficulty: DifficultyLevel.hard,
durationMinutes: 120,
createdAt: createdAt,
),
_backlogSeedTask(
id: 'write-marketing-strategy',
title: 'Write marketing strategy',
projectId: 'work',
order: 13,
priority: PriorityLevel.high,
reward: RewardLevel.high,
difficulty: DifficultyLevel.medium,
durationMinutes: 90,
createdAt: createdAt,
),
_backlogSeedTask(
id: 'complete-online-course',
title: 'Complete online course',
projectId: 'learning',
order: 14,
priority: PriorityLevel.medium,
reward: RewardLevel.high,
difficulty: DifficultyLevel.medium,
durationMinutes: 90,
createdAt: createdAt,
),
_backlogSeedTask(
id: 'module-1-foundations',
title: 'Module 1: Foundations',
projectId: 'learning',
order: 15,
priority: PriorityLevel.medium,
reward: RewardLevel.medium,
difficulty: DifficultyLevel.hard,
durationMinutes: 15,
createdAt: createdAt,
),
_backlogSeedTask(
id: 'module-2-deep-dive',
title: 'Module 2: Deep Dive',
projectId: 'learning',
order: 16,
priority: PriorityLevel.low,
reward: RewardLevel.medium,
difficulty: DifficultyLevel.hard,
durationMinutes: 15,
createdAt: createdAt,
),
_backlogSeedTask(
id: 'module-3-practice',
title: 'Module 3: Practice',
projectId: 'learning',
order: 17,
priority: PriorityLevel.low,
reward: RewardLevel.medium,
difficulty: DifficultyLevel.hard,
durationMinutes: 15,
createdAt: createdAt,
),
_backlogSeedTask(
id: 'learn-guitar',
title: 'Learn guitar',
projectId: 'personal',
order: 18,
priority: PriorityLevel.low,
reward: RewardLevel.low,
difficulty: DifficultyLevel.medium,
durationMinutes: 30,
backlogTags: const {BacklogTag.wishlist},
createdAt: createdAt,
),
_backlogSeedTask(
id: 'start-youtube-channel',
title: 'Start YouTube channel',
projectId: 'side-project',
order: 19,
priority: PriorityLevel.low,
reward: RewardLevel.low,
difficulty: DifficultyLevel.medium,
durationMinutes: 20,
backlogTags: const {BacklogTag.wishlist},
createdAt: createdAt,
),
_backlogSeedTask(
id: 'travel-to-japan',
title: 'Travel to Japan',
projectId: 'personal',
order: 20,
priority: PriorityLevel.medium,
reward: RewardLevel.medium,
difficulty: DifficultyLevel.medium,
durationMinutes: 30,
backlogTags: const {BacklogTag.wishlist},
createdAt: createdAt,
),
_backlogSeedTask(
id: 'home-office-upgrade',
title: 'Home office upgrade',
projectId: 'home',
order: 21,
priority: PriorityLevel.low,
reward: RewardLevel.low,
difficulty: DifficultyLevel.medium,
durationMinutes: 20,
backlogTags: const {BacklogTag.wishlist},
createdAt: createdAt,
),
_backlogSeedTask(
id: 'read-2-books',
title: 'Read 2 books',
projectId: 'personal',
order: 22,
priority: PriorityLevel.low,
reward: RewardLevel.low,
difficulty: DifficultyLevel.medium,
durationMinutes: 15,
backlogTags: const {BacklogTag.wishlist},
createdAt: createdAt,
),
_backlogSeedTask(
id: 'sketch-app-icon-ideas',
title: 'Sketch app icon ideas',
projectId: 'side-project',
order: 23,
priority: PriorityLevel.medium,
reward: RewardLevel.medium,
difficulty: DifficultyLevel.easy,
durationMinutes: 15,
backlogTags: const {BacklogTag.wishlist},
createdAt: createdAt,
),
_backlogSeedTask(
id: 'archive-photo-backlog',
title: 'Archive photo backlog',
projectId: 'personal',
order: 24,
priority: PriorityLevel.low,
reward: RewardLevel.low,
difficulty: DifficultyLevel.medium,
durationMinutes: 15,
backlogTags: const {BacklogTag.wishlist},
createdAt: createdAt,
),
];
}
/// Top-level helper that performs the `_seedTask` operation for this file.
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
Task _seedTask({
@ -161,3 +463,34 @@ Task _seedTask({
updatedAt: completedAt ?? createdAt,
);
}
/// Top-level helper that performs the `_backlogSeedTask` operation for this file.
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
Task _backlogSeedTask({
required String id,
required String title,
required String projectId,
required int order,
required PriorityLevel priority,
required RewardLevel reward,
required DifficultyLevel difficulty,
required int durationMinutes,
required DateTime createdAt,
Set<BacklogTag> backlogTags = const <BacklogTag>{},
}) {
final orderedCreatedAt = createdAt.add(Duration(minutes: order));
return Task(
id: id,
title: title,
projectId: projectId,
type: TaskType.flexible,
status: TaskStatus.backlog,
priority: priority,
reward: reward,
difficulty: difficulty,
durationMinutes: durationMinutes,
backlogTags: backlogTags,
createdAt: orderedCreatedAt,
updatedAt: orderedCreatedAt,
);
}

View file

@ -91,24 +91,13 @@ class DemoSchedulerComposition implements SchedulerAppComposition {
CivilDate.fromDateTime((clock ?? const SystemClock()).now());
final readAt = _instant(resolvedDate, 15, 55);
final createdAt = _instant(resolvedDate, 8);
final project = ProjectProfile(
id: projectId,
name: 'Home',
colorKey: 'project-home',
);
final projects = _demoProjects();
final tasks = <Task>[
if (includeTodayItems) ..._todaySeedTasks(resolvedDate, createdAt),
if (includeBacklogItems)
Task.quickCapture(
id: 'later-backlog-note',
title: 'Review reusable grocery list',
projectId: projectId,
createdAt: createdAt,
updatedAt: createdAt,
),
if (includeBacklogItems) ..._backlogSeedTasks(resolvedDate, createdAt),
];
final store = InMemoryApplicationUnitOfWork(
initialProjects: [project],
initialProjects: projects,
initialOwnerSettings: [
OwnerSettings(
ownerId: ownerId,
@ -191,6 +180,22 @@ class DemoSchedulerComposition implements SchedulerAppComposition {
);
}
/// Creates a generic read controller for the Backlog board.
UiReadController<BacklogBoardQueryResult> createBacklogBoardController() {
return ApplicationReadController<BacklogBoardQueryResult>(
read: () {
return managementUseCases.getBacklogBoard(
GetBacklogBoardRequest(
context: context('ui-read-backlog-board'),
localDate: date,
sortKey: BacklogSortKey.age,
),
);
},
isEmpty: (state) => state.summary.totalTaskCount == 0,
);
}
/// Creates the command controller used by interactive scheduler controls.
@override
SchedulerCommandController createCommandController({

View file

@ -0,0 +1,107 @@
// SPDX-FileCopyrightText: 2026 FocusFlow contributors
// SPDX-License-Identifier: AGPL-3.0-only
/// Tests Demo Backlog Seed behavior in the FocusFlow Flutter app.
library;
import 'package:flutter_test/flutter_test.dart';
import 'package:scheduler_core/scheduler_core.dart';
import 'package:focus_flow_flutter/app/demo_scheduler_composition.dart';
/// Runs demo Backlog seed tests.
void main() {
test('seeded backlog board exposes mockup-scale domain data', () async {
final composition = DemoSchedulerComposition.seeded(
selectedDate: CivilDate(2025, 5, 20),
);
addTearDown(composition.dispose);
final board = (await composition.managementUseCases.getBacklogBoard(
GetBacklogBoardRequest(
context: composition.context('demo-backlog-board'),
localDate: composition.date,
sortKey: BacklogSortKey.age,
),
)).requireValue;
final items = board.columns.expand((column) => column.items).toList();
expect(board.summary.totalTaskCount, 24);
expect(board.summary.totalEstimatedMinutes, 735);
expect(_distributionCounts(board.summary.projectDistribution), {
'Home': 5,
'Learning': 4,
'Personal': 5,
'Side Project': 2,
'Work': 8,
});
expect(_distributionCounts(board.summary.priorityDistribution), {
'High': 7,
'Medium': 9,
'Low': 8,
});
expect(items.map((item) => item.title), containsAll(_visibleSeedTitles));
final review = items.singleWhere(
(item) => item.title == 'Review Q3 budget',
);
expect(review.projectName, 'Work');
expect(review.durationMinutes, 60);
expect(review.priority, PriorityLevel.high);
expect(review.reward, RewardLevel.medium);
expect(review.difficulty, DifficultyLevel.easy);
});
test('seeded review detail stays within deferred metadata scope', () async {
final composition = DemoSchedulerComposition.seeded(
selectedDate: CivilDate(2025, 5, 20),
);
addTearDown(composition.dispose);
final detail = (await composition.managementUseCases.getBacklogTaskDetail(
GetBacklogTaskDetailRequest(
context: composition.context('demo-review-detail'),
localDate: composition.date,
taskId: 'review-q3-budget',
),
)).requireValue;
expect(detail.item.title, 'Review Q3 budget');
expect(detail.notes, isNull);
expect(detail.tags, isEmpty);
expect(detail.suggestedSlots, isNotEmpty);
});
}
/// Visible task titles from the Backlog board mockup that must be present in the seed.
const _visibleSeedTitles = <String>[
'Reply to 3 emails',
'Update project status',
'Schedule dentist appt',
'Clean up downloads folder',
'Plan tomorrow',
'Review Q3 budget',
'Plan next week',
'Define weekly goals',
'Time block schedule',
'Organize kitchen drawers',
'Build analytics dashboard',
'Write marketing strategy',
'Complete online course',
'Module 1: Foundations',
'Module 2: Deep Dive',
'Module 3: Practice',
'Learn guitar',
'Start YouTube channel',
'Travel to Japan',
'Home office upgrade',
'Read 2 books',
];
/// Top-level helper that performs the `_distributionCounts` operation for this file.
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
Map<String, int> _distributionCounts(
BacklogDistributionReadModel distribution,
) {
return {for (final entry in distribution.entries) entry.label: entry.count};
}