diff --git a/packages/scheduler_core/lib/src/application/application_management.dart b/packages/scheduler_core/lib/src/application/application_management.dart index 258d5e8..3e29293 100644 --- a/packages/scheduler_core/lib/src/application/application_management.dart +++ b/packages/scheduler_core/lib/src/application/application_management.dart @@ -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'; diff --git a/packages/scheduler_core/lib/src/application/application_management/read_models/backlog_board/backlog_board_column_read_model.dart b/packages/scheduler_core/lib/src/application/application_management/read_models/backlog_board/backlog_board_column_read_model.dart new file mode 100644 index 0000000..5ab53c9 --- /dev/null +++ b/packages/scheduler_core/lib/src/application/application_management/read_models/backlog_board/backlog_board_column_read_model.dart @@ -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 items, + int? count, + }) : items = List.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 items; +} diff --git a/packages/scheduler_core/lib/src/application/application_management/read_models/backlog_board/backlog_board_item_read_model.dart b/packages/scheduler_core/lib/src/application/application_management/read_models/backlog_board/backlog_board_item_read_model.dart new file mode 100644 index 0000000..9ed5128 --- /dev/null +++ b/packages/scheduler_core/lib/src/application/application_management/read_models/backlog_board/backlog_board_item_read_model.dart @@ -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 childPreview, + required List tags, + this.subtitle, + this.durationMinutes, + this.priority, + bool? hasChildren, + }) : childPreview = + List.unmodifiable(childPreview), + tags = List.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 childPreview; + + /// Display-ready task-local tag labels. + final List tags; +} diff --git a/packages/scheduler_core/lib/src/application/application_management/read_models/backlog_board/backlog_board_query_result.dart b/packages/scheduler_core/lib/src/application/application_management/read_models/backlog_board/backlog_board_query_result.dart new file mode 100644 index 0000000..51eceed --- /dev/null +++ b/packages/scheduler_core/lib/src/application/application_management/read_models/backlog_board/backlog_board_query_result.dart @@ -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 columns, + required List projectOptions, + }) : columns = List.unmodifiable(columns), + projectOptions = List.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 columns; + + /// Project choices available to board and drawer controls. + final List projectOptions; +} diff --git a/packages/scheduler_core/lib/src/application/application_management/read_models/backlog_board/backlog_board_summary_read_model.dart b/packages/scheduler_core/lib/src/application/application_management/read_models/backlog_board/backlog_board_summary_read_model.dart new file mode 100644 index 0000000..d06f59d --- /dev/null +++ b/packages/scheduler_core/lib/src/application/application_management/read_models/backlog_board/backlog_board_summary_read_model.dart @@ -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; +} diff --git a/packages/scheduler_core/lib/src/application/application_management/read_models/backlog_board/backlog_child_preview_read_model.dart b/packages/scheduler_core/lib/src/application/application_management/read_models/backlog_board/backlog_child_preview_read_model.dart new file mode 100644 index 0000000..d7f3444 --- /dev/null +++ b/packages/scheduler_core/lib/src/application/application_management/read_models/backlog_board/backlog_child_preview_read_model.dart @@ -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; +} diff --git a/packages/scheduler_core/lib/src/application/application_management/read_models/backlog_board/backlog_distribution_entry_read_model.dart b/packages/scheduler_core/lib/src/application/application_management/read_models/backlog_board/backlog_distribution_entry_read_model.dart new file mode 100644 index 0000000..cc82a30 --- /dev/null +++ b/packages/scheduler_core/lib/src/application/application_management/read_models/backlog_board/backlog_distribution_entry_read_model.dart @@ -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; +} diff --git a/packages/scheduler_core/lib/src/application/application_management/read_models/backlog_board/backlog_distribution_read_model.dart b/packages/scheduler_core/lib/src/application/application_management/read_models/backlog_board/backlog_distribution_read_model.dart new file mode 100644 index 0000000..5100dd3 --- /dev/null +++ b/packages/scheduler_core/lib/src/application/application_management/read_models/backlog_board/backlog_distribution_read_model.dart @@ -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 entries, + int? totalCount, + }) : entries = List.unmodifiable( + entries, + ), + totalCount = totalCount ?? + entries.fold(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 entries; +} diff --git a/packages/scheduler_core/lib/src/application/application_management/read_models/backlog_board/backlog_task_detail_read_model.dart b/packages/scheduler_core/lib/src/application/application_management/read_models/backlog_board/backlog_task_detail_read_model.dart new file mode 100644 index 0000000..11fb872 --- /dev/null +++ b/packages/scheduler_core/lib/src/application/application_management/read_models/backlog_board/backlog_task_detail_read_model.dart @@ -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 tags, + required List projectOptions, + required List suggestedSlots, + this.notes, + this.suggestedSlotUnavailableReason, + }) : tags = List.unmodifiable(tags), + projectOptions = List.unmodifiable( + projectOptions, + ), + suggestedSlots = List.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 tags; + + /// Available project choices for the selected task. + final List projectOptions; + + /// Non-mutating suggested schedule slots. + final List suggestedSlots; + + /// Display reason when slot suggestions are unavailable. + final String? suggestedSlotUnavailableReason; +} diff --git a/packages/scheduler_core/lib/src/application/application_management/read_models/backlog_board/project_option_read_model.dart b/packages/scheduler_core/lib/src/application/application_management/read_models/backlog_board/project_option_read_model.dart new file mode 100644 index 0000000..708b081 --- /dev/null +++ b/packages/scheduler_core/lib/src/application/application_management/read_models/backlog_board/project_option_read_model.dart @@ -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; +} diff --git a/packages/scheduler_core/lib/src/application/application_management/read_models/backlog_board/suggested_slot_read_model.dart b/packages/scheduler_core/lib/src/application/application_management/read_models/backlog_board/suggested_slot_read_model.dart new file mode 100644 index 0000000..0d02565 --- /dev/null +++ b/packages/scheduler_core/lib/src/application/application_management/read_models/backlog_board/suggested_slot_read_model.dart @@ -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; +} diff --git a/packages/scheduler_core/test/application_management_test.dart b/packages/scheduler_core/test/application_management_test.dart index 92750d8..b79d414 100644 --- a/packages/scheduler_core/test/application_management_test.dart +++ b/packages/scheduler_core/test/application_management_test.dart @@ -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(