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
12 changed files with 548 additions and 0 deletions
Showing only changes of commit 39326ef3dd - Show all commits

View file

@ -23,6 +23,16 @@ part 'application_management/codes/owner_settings_warning_code.dart';
part 'application_management/requests/get_backlog_request.dart';
part 'application_management/read_models/backlog_item_read_model.dart';
part 'application_management/read_models/backlog_query_result.dart';
part 'application_management/read_models/backlog_board/backlog_board_column_read_model.dart';
part 'application_management/read_models/backlog_board/backlog_board_item_read_model.dart';
part 'application_management/read_models/backlog_board/backlog_board_query_result.dart';
part 'application_management/read_models/backlog_board/backlog_board_summary_read_model.dart';
part 'application_management/read_models/backlog_board/backlog_child_preview_read_model.dart';
part 'application_management/read_models/backlog_board/backlog_distribution_entry_read_model.dart';
part 'application_management/read_models/backlog_board/backlog_distribution_read_model.dart';
part 'application_management/read_models/backlog_board/backlog_task_detail_read_model.dart';
part 'application_management/read_models/backlog_board/project_option_read_model.dart';
part 'application_management/read_models/backlog_board/suggested_slot_read_model.dart';
part 'application_management/requests/get_project_defaults_request.dart';
part 'application_management/read_models/project_defaults_query_result.dart';
part 'application_management/drafts/project_profile_draft.dart';

View file

@ -0,0 +1,36 @@
// SPDX-FileCopyrightText: 2026 FocusFlow contributors
// SPDX-License-Identifier: AGPL-3.0-only
part of '../../../application_management.dart';
/// One column in the Backlog board.
class BacklogBoardColumnReadModel {
/// Creates a board column with immutable [items].
BacklogBoardColumnReadModel({
required this.bucket,
required this.title,
required this.subtitle,
required this.accentToken,
required List<BacklogBoardItemReadModel> items,
int? count,
}) : items = List<BacklogBoardItemReadModel>.unmodifiable(items),
count = count ?? items.length;
/// Bucket represented by this column.
final BacklogBoardBucket bucket;
/// Display title for the column header.
final String title;
/// Short display subtitle for the column header.
final String subtitle;
/// UI color token used by Flutter to style the column accent.
final String accentToken;
/// Count shown in the column header.
final int count;
/// Items rendered in this column.
final List<BacklogBoardItemReadModel> items;
}

View file

@ -0,0 +1,82 @@
// SPDX-FileCopyrightText: 2026 FocusFlow contributors
// SPDX-License-Identifier: AGPL-3.0-only
part of '../../../application_management.dart';
/// Card-level read model for one Backlog board task.
class BacklogBoardItemReadModel {
/// Creates a Backlog board item from display-ready task metadata.
BacklogBoardItemReadModel({
required this.taskId,
required this.title,
required this.projectId,
required this.projectName,
required this.projectColorToken,
required this.reward,
required this.difficulty,
required this.bucket,
required this.bucketReason,
required this.createdAt,
required this.updatedAt,
required List<BacklogChildPreviewReadModel> childPreview,
required List<String> tags,
this.subtitle,
this.durationMinutes,
this.priority,
bool? hasChildren,
}) : childPreview =
List<BacklogChildPreviewReadModel>.unmodifiable(childPreview),
tags = List<String>.unmodifiable(tags),
hasChildren = hasChildren ?? childPreview.isNotEmpty;
/// Stable task id used for selection and commands.
final String taskId;
/// User-facing task title.
final String title;
/// Optional subtitle or notes preview for card display.
final String? subtitle;
/// Owning project id.
final String projectId;
/// Display name for the owning project.
final String projectName;
/// UI color token for the owning project.
final String projectColorToken;
/// Estimated task duration in minutes, if captured.
final int? durationMinutes;
/// Optional task priority.
final PriorityLevel? priority;
/// Expected reward or payoff.
final RewardLevel reward;
/// Expected activation difficulty.
final DifficultyLevel difficulty;
/// Board bucket selected by scheduler policy.
final BacklogBoardBucket bucket;
/// User-facing reason for [bucket].
final String bucketReason;
/// Original task creation timestamp.
final DateTime createdAt;
/// Last task update timestamp.
final DateTime updatedAt;
/// Whether the task has child task metadata.
final bool hasChildren;
/// Preview rows for direct child tasks.
final List<BacklogChildPreviewReadModel> childPreview;
/// Display-ready task-local tag labels.
final List<String> tags;
}

View file

@ -0,0 +1,34 @@
// SPDX-FileCopyrightText: 2026 FocusFlow contributors
// SPDX-License-Identifier: AGPL-3.0-only
part of '../../../application_management.dart';
/// Query result for the Backlog board surface.
class BacklogBoardQueryResult {
/// Creates a Backlog board result from summary, columns, and project options.
BacklogBoardQueryResult({
required this.ownerId,
required this.localDate,
required this.summary,
required List<BacklogBoardColumnReadModel> columns,
required List<ProjectOptionReadModel> projectOptions,
}) : columns = List<BacklogBoardColumnReadModel>.unmodifiable(columns),
projectOptions = List<ProjectOptionReadModel>.unmodifiable(
projectOptions,
);
/// Authorization-neutral owner scope used for the read.
final String ownerId;
/// Owner-local planning date used for suggested slot previews.
final CivilDate localDate;
/// Summary counts and planning copy derived from the same filtered item set.
final BacklogBoardSummaryReadModel summary;
/// Stable ordered board columns.
final List<BacklogBoardColumnReadModel> columns;
/// Project choices available to board and drawer controls.
final List<ProjectOptionReadModel> projectOptions;
}

View file

@ -0,0 +1,35 @@
// SPDX-FileCopyrightText: 2026 FocusFlow contributors
// SPDX-License-Identifier: AGPL-3.0-only
part of '../../../application_management.dart';
/// Summary panel read model for the Backlog board.
class BacklogBoardSummaryReadModel {
/// Creates a Backlog summary from count, estimate, and distribution data.
const BacklogBoardSummaryReadModel({
required this.totalTaskCount,
required this.totalEstimatedMinutes,
required this.priorityDistribution,
required this.projectDistribution,
required this.durationDistribution,
required this.planningTip,
});
/// Total number of tasks represented by the filtered board.
final int totalTaskCount;
/// Sum of captured duration estimates across the filtered board.
final int totalEstimatedMinutes;
/// Count distribution by priority label.
final BacklogDistributionReadModel priorityDistribution;
/// Count distribution by project label.
final BacklogDistributionReadModel projectDistribution;
/// Count and percentage distribution by duration bucket.
final BacklogDistributionReadModel durationDistribution;
/// Display copy for the planning tip panel.
final String planningTip;
}

View file

@ -0,0 +1,31 @@
// SPDX-FileCopyrightText: 2026 FocusFlow contributors
// SPDX-License-Identifier: AGPL-3.0-only
part of '../../../application_management.dart';
/// Compact preview for a direct child task.
class BacklogChildPreviewReadModel {
/// Creates a child task preview row.
const BacklogChildPreviewReadModel({
required this.taskId,
required this.title,
required this.isCompleted,
this.durationMinutes,
this.statusLabel,
});
/// Stable child task id.
final String taskId;
/// User-facing child task title.
final String title;
/// Estimated child task duration in minutes, if captured.
final int? durationMinutes;
/// Whether the child task is completed.
final bool isCompleted;
/// Optional display label for the child task lifecycle state.
final String? statusLabel;
}

View file

@ -0,0 +1,31 @@
// SPDX-FileCopyrightText: 2026 FocusFlow contributors
// SPDX-License-Identifier: AGPL-3.0-only
part of '../../../application_management.dart';
/// One entry in a Backlog summary distribution.
class BacklogDistributionEntryReadModel {
/// Creates a distribution entry.
const BacklogDistributionEntryReadModel({
required this.id,
required this.label,
required this.count,
required this.percentage,
this.colorToken,
});
/// Stable entry id for tests and UI keys.
final String id;
/// Display label for the entry.
final String label;
/// Number of tasks in this entry.
final int count;
/// Share of the parent distribution as a value from 0.0 to 1.0.
final double percentage;
/// Optional UI color token for the entry.
final String? colorToken;
}

View file

@ -0,0 +1,27 @@
// SPDX-FileCopyrightText: 2026 FocusFlow contributors
// SPDX-License-Identifier: AGPL-3.0-only
part of '../../../application_management.dart';
/// Named distribution used by Backlog summary panels.
class BacklogDistributionReadModel {
/// Creates a distribution with immutable [entries].
BacklogDistributionReadModel({
required this.title,
required List<BacklogDistributionEntryReadModel> entries,
int? totalCount,
}) : entries = List<BacklogDistributionEntryReadModel>.unmodifiable(
entries,
),
totalCount = totalCount ??
entries.fold<int>(0, (sum, entry) => sum + entry.count);
/// Display title for this distribution.
final String title;
/// Total item count represented by [entries].
final int totalCount;
/// Ordered display entries.
final List<BacklogDistributionEntryReadModel> entries;
}

View file

@ -0,0 +1,45 @@
// SPDX-FileCopyrightText: 2026 FocusFlow contributors
// SPDX-License-Identifier: AGPL-3.0-only
part of '../../../application_management.dart';
/// Drawer-level read model for a selected Backlog task.
class BacklogTaskDetailReadModel {
/// Creates a task detail result for the Backlog drawer.
BacklogTaskDetailReadModel({
required this.item,
required this.addedLabel,
required List<String> tags,
required List<ProjectOptionReadModel> projectOptions,
required List<SuggestedSlotReadModel> suggestedSlots,
this.notes,
this.suggestedSlotUnavailableReason,
}) : tags = List<String>.unmodifiable(tags),
projectOptions = List<ProjectOptionReadModel>.unmodifiable(
projectOptions,
),
suggestedSlots = List<SuggestedSlotReadModel>.unmodifiable(
suggestedSlots,
);
/// Board card model for the selected task.
final BacklogBoardItemReadModel item;
/// Optional long-form notes for the drawer.
final String? notes;
/// Display label describing when the task was added to Backlog.
final String addedLabel;
/// Display-ready task-local tag labels.
final List<String> tags;
/// Available project choices for the selected task.
final List<ProjectOptionReadModel> projectOptions;
/// Non-mutating suggested schedule slots.
final List<SuggestedSlotReadModel> suggestedSlots;
/// Display reason when slot suggestions are unavailable.
final String? suggestedSlotUnavailableReason;
}

View file

@ -0,0 +1,27 @@
// SPDX-FileCopyrightText: 2026 FocusFlow contributors
// SPDX-License-Identifier: AGPL-3.0-only
part of '../../../application_management.dart';
/// Project option exposed to Backlog board and drawer controls.
class ProjectOptionReadModel {
/// Creates a project option read model.
const ProjectOptionReadModel({
required this.projectId,
required this.name,
required this.colorToken,
this.isArchived = false,
});
/// Stable project id.
final String projectId;
/// User-facing project name.
final String name;
/// UI color token for the project.
final String colorToken;
/// Whether this project is archived.
final bool isArchived;
}

View file

@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: 2026 FocusFlow contributors
// SPDX-License-Identifier: AGPL-3.0-only
part of '../../../application_management.dart';
/// Preview row for a possible Backlog scheduling slot.
class SuggestedSlotReadModel {
/// Creates a suggested slot preview.
const SuggestedSlotReadModel({
required this.id,
required this.localDateLabel,
required this.startText,
required this.endText,
required this.durationMinutes,
required this.fitLabel,
required this.fitSeverityToken,
required this.isPrimary,
});
/// Stable suggestion id.
final String id;
/// Display label for the owner-local date.
final String localDateLabel;
/// Display-ready start time.
final String startText;
/// Display-ready end time.
final String endText;
/// Suggested slot length in minutes.
final int durationMinutes;
/// Display label such as `Great Fit` or `Okay`.
final String fitLabel;
/// UI severity token for the fit label.
final String fitSeverityToken;
/// Whether this is the primary/default scheduling target.
final bool isPrimary;
}

View file

@ -70,6 +70,153 @@ void main() {
);
});
test('backlog board read models snapshot collection inputs', () {
final childPreviews = [
const BacklogChildPreviewReadModel(
taskId: 'child-1',
title: 'Gather statements',
isCompleted: false,
durationMinutes: 15,
statusLabel: 'Backlog',
),
];
final itemTags = ['finance'];
final item = BacklogBoardItemReadModel(
taskId: 'review-budget',
title: 'Review Q3 budget',
subtitle: 'Check the draft before planning.',
projectId: 'work',
projectName: 'Work',
projectColorToken: 'project-work',
durationMinutes: 60,
priority: PriorityLevel.high,
reward: RewardLevel.medium,
difficulty: DifficultyLevel.easy,
bucket: BacklogBoardBucket.needTimeBlock,
bucketReason: 'Needs a focused time block.',
createdAt: now,
updatedAt: now,
childPreview: childPreviews,
tags: itemTags,
);
childPreviews.clear();
itemTags.add('late');
expect(item.childPreview, hasLength(1));
expect(item.tags, ['finance']);
expect(() => item.tags.add('new'), throwsUnsupportedError);
final items = [item];
final column = BacklogBoardColumnReadModel(
bucket: BacklogBoardBucket.needTimeBlock,
title: 'Need Time Block',
subtitle: 'Plan deliberately.',
accentToken: 'backlog-time-block',
items: items,
);
items.clear();
expect(column.count, 1);
expect(column.items, [item]);
expect(() => column.items.clear(), throwsUnsupportedError);
final entries = [
const BacklogDistributionEntryReadModel(
id: 'high',
label: 'High',
count: 2,
percentage: 1,
colorToken: 'priority-high',
),
];
final priorityDistribution = BacklogDistributionReadModel(
title: 'Priority',
entries: entries,
);
entries.clear();
expect(priorityDistribution.totalCount, 2);
expect(priorityDistribution.entries, hasLength(1));
expect(
() => priorityDistribution.entries.clear(),
throwsUnsupportedError,
);
final projects = [
const ProjectOptionReadModel(
projectId: 'work',
name: 'Work',
colorToken: 'project-work',
),
];
final summary = BacklogBoardSummaryReadModel(
totalTaskCount: 1,
totalEstimatedMinutes: 60,
priorityDistribution: priorityDistribution,
projectDistribution: BacklogDistributionReadModel(
title: 'Project',
entries: const [
BacklogDistributionEntryReadModel(
id: 'work',
label: 'Work',
count: 1,
percentage: 1,
colorToken: 'project-work',
),
],
),
durationDistribution: BacklogDistributionReadModel(
title: 'Duration',
entries: const [
BacklogDistributionEntryReadModel(
id: '30-60',
label: '30-60',
count: 1,
percentage: 1,
),
],
),
planningTip: 'Pick one focused task before adding more.',
);
final result = BacklogBoardQueryResult(
ownerId: 'owner-1',
localDate: CivilDate(2026, 6, 25),
summary: summary,
columns: [column],
projectOptions: projects,
);
projects.clear();
expect(result.columns, [column]);
expect(result.projectOptions, hasLength(1));
expect(() => result.projectOptions.clear(), throwsUnsupportedError);
final detail = BacklogTaskDetailReadModel(
item: item,
addedLabel: 'Added today',
tags: const ['finance'],
projectOptions: result.projectOptions,
suggestedSlots: const [
SuggestedSlotReadModel(
id: 'today-1',
localDateLabel: 'Today',
startText: '2:00 PM',
endText: '3:00 PM',
durationMinutes: 60,
fitLabel: 'Great Fit',
fitSeverityToken: 'fit-great',
isPrimary: true,
),
],
notes: 'Review budget categories before scheduling.',
);
expect(detail.tags, ['finance']);
expect(detail.suggestedSlots.single.fitLabel, 'Great Fit');
expect(() => detail.suggestedSlots.clear(), throwsUnsupportedError);
});
test('keeps configured project defaults separate from learned suggestions',
() async {
final project = ProjectProfile(