feat(backlog): add board read models
This commit is contained in:
parent
b71d74290f
commit
39326ef3dd
12 changed files with 548 additions and 0 deletions
|
|
@ -23,6 +23,16 @@ part 'application_management/codes/owner_settings_warning_code.dart';
|
||||||
part 'application_management/requests/get_backlog_request.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_item_read_model.dart';
|
||||||
part 'application_management/read_models/backlog_query_result.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/requests/get_project_defaults_request.dart';
|
||||||
part 'application_management/read_models/project_defaults_query_result.dart';
|
part 'application_management/read_models/project_defaults_query_result.dart';
|
||||||
part 'application_management/drafts/project_profile_draft.dart';
|
part 'application_management/drafts/project_profile_draft.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<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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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',
|
test('keeps configured project defaults separate from learned suggestions',
|
||||||
() async {
|
() async {
|
||||||
final project = ProjectProfile(
|
final project = ProjectProfile(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue