forked from eva/focus-flow
528 lines
18 KiB
Dart
528 lines
18 KiB
Dart
// Core domain model for the ADHD scheduling starter project.
|
|
//
|
|
// This file intentionally contains small immutable value objects and enums. It
|
|
// should stay free of UI, persistence, notification, and platform code. Keeping
|
|
// this layer plain makes the scheduler easy to test and easy to move between a
|
|
// command-line prototype, Flutter UI, or future backend service.
|
|
//
|
|
// Reading order for humans:
|
|
// 1. `TaskType` and `TaskStatus` explain the two main axes of a task.
|
|
// 2. `Task` shows the actual data carried through the scheduling engine.
|
|
// 3. `ProjectProfile` explains how project defaults create tasks.
|
|
// 4. `TimeInterval` is the shared time-span helper used by scheduling logic.
|
|
|
|
import 'task_statistics.dart';
|
|
|
|
/// Scheduling behavior category.
|
|
///
|
|
/// This enum is one of the central concepts in the planner. The type answers
|
|
/// the question: "how should the scheduler treat this item?" It is separate
|
|
/// from [TaskStatus], which answers "where is this item in its lifecycle?"
|
|
///
|
|
/// For example, a flexible task can be planned, completed, missed, or moved to
|
|
/// backlog. A locked item, by contrast, acts more like a calendar constraint
|
|
/// than a normal task card. Keeping behavior and lifecycle separate makes later
|
|
/// UI and persistence logic easier to reason about.
|
|
enum TaskType {
|
|
/// Movable planned work.
|
|
flexible,
|
|
|
|
/// Required visible block that should not be moved automatically.
|
|
inflexible,
|
|
|
|
/// Required visible task that remains actionable if missed.
|
|
critical,
|
|
|
|
/// Hidden scheduling constraint, not a task card.
|
|
locked,
|
|
|
|
/// Unplanned completed/logged task.
|
|
surprise,
|
|
|
|
/// Intentional rest time.
|
|
freeSlot,
|
|
}
|
|
|
|
/// Current lifecycle state of a task.
|
|
///
|
|
/// Status is intentionally data-oriented: it describes what happened to a task,
|
|
/// not how important it is or how the scheduler should move it. Scheduler rules
|
|
/// combine [TaskStatus] with [TaskType]. For instance, a planned flexible task
|
|
/// can be pushed, while a planned critical task should remain visible and become
|
|
/// backlog if missed.
|
|
enum TaskStatus {
|
|
/// Scheduled or queued work that has not started.
|
|
planned,
|
|
|
|
/// Work currently in progress.
|
|
active,
|
|
|
|
/// Work finished by the user.
|
|
completed,
|
|
|
|
/// Work that was not completed in its intended time.
|
|
missed,
|
|
|
|
/// Work intentionally removed from the plan.
|
|
cancelled,
|
|
|
|
/// Work intentionally dismissed because it no longer applies.
|
|
noLongerRelevant,
|
|
|
|
/// Unscheduled work kept for later planning.
|
|
backlog,
|
|
}
|
|
|
|
/// User-facing importance level.
|
|
///
|
|
/// Priority is a relative ordering hint. It should help decide what rises to the
|
|
/// top of a queue, but it should not be treated as an absolute promise that the
|
|
/// task must happen at a specific time. The scheduling engine currently ranks
|
|
/// this with simple numeric helpers in `backlog.dart`; more advanced heuristics
|
|
/// can build on the same enum later.
|
|
enum PriorityLevel {
|
|
/// Lowest priority.
|
|
veryLow,
|
|
|
|
/// Low priority.
|
|
low,
|
|
|
|
/// Default middle priority.
|
|
medium,
|
|
|
|
/// High priority.
|
|
high,
|
|
|
|
/// Highest priority.
|
|
veryHigh,
|
|
}
|
|
|
|
/// Expected reward or payoff from completing a task.
|
|
///
|
|
/// Reward is meant to capture motivational payoff, not objective value. In this
|
|
/// app design it supports ADHD-friendly planning: a small, easy, high-reward task
|
|
/// may be a better momentum starter than a large low-reward task.
|
|
enum RewardLevel {
|
|
/// No reward level has been captured; this is not equivalent to low reward.
|
|
notSet,
|
|
|
|
/// Very low expected reward.
|
|
veryLow,
|
|
|
|
/// Low expected reward.
|
|
low,
|
|
|
|
/// Medium expected reward.
|
|
medium,
|
|
|
|
/// High expected reward.
|
|
high,
|
|
|
|
/// Very high expected reward.
|
|
veryHigh,
|
|
}
|
|
|
|
/// Expected effort or activation difficulty for a task.
|
|
///
|
|
/// Difficulty is not the same as duration. A five-minute phone call might be
|
|
/// very hard to start, while an hour of familiar maintenance may be easy. The
|
|
/// backlog view uses this with [RewardLevel] to expose a simple
|
|
/// reward-versus-effort sort.
|
|
enum DifficultyLevel {
|
|
/// No difficulty has been captured yet.
|
|
notSet,
|
|
|
|
/// Very easy to start or complete.
|
|
veryEasy,
|
|
|
|
/// Easy to start or complete.
|
|
easy,
|
|
|
|
/// Medium effort.
|
|
medium,
|
|
|
|
/// Hard to start or complete.
|
|
hard,
|
|
|
|
/// Very hard to start or complete.
|
|
veryHard,
|
|
}
|
|
|
|
/// Reminder intensity preference.
|
|
///
|
|
/// This is currently stored as project metadata rather than enforced by the core
|
|
/// scheduler. Future notification/UI layers can use it to decide how aggressive
|
|
/// reminders should be without adding reminder-specific logic to [Task].
|
|
enum ReminderProfile {
|
|
/// No reminder nudges.
|
|
silent,
|
|
|
|
/// Low-friction reminder nudges.
|
|
gentle,
|
|
|
|
/// Repeated reminder nudges.
|
|
persistent,
|
|
|
|
/// Strong reminder behavior for required items.
|
|
strict,
|
|
}
|
|
|
|
/// Lightweight backlog-only metadata.
|
|
///
|
|
/// Tags here are deliberately narrow. They are not meant to replace a general
|
|
/// tagging system. They identify special backlog behavior that the planner needs
|
|
/// to understand, such as "wishlist/someday" items.
|
|
enum BacklogTag {
|
|
/// Task is intentionally saved as a someday/wishlist item.
|
|
wishlist,
|
|
}
|
|
|
|
/// Starter task model for the scheduling core.
|
|
///
|
|
/// [Task] is the main domain object passed through the scheduler. It is written
|
|
/// as an immutable value object: operations do not mutate an existing task, they
|
|
/// return a copied task with changed fields. That approach makes scheduling
|
|
/// actions easier to test because every function receives an input list and
|
|
/// returns a new output list.
|
|
///
|
|
/// Important modeling choices:
|
|
/// - [type] controls scheduling behavior: flexible, critical, locked, etc.
|
|
/// - [status] controls lifecycle state: planned, completed, backlog, etc.
|
|
/// - [scheduledStart] and [scheduledEnd] are optional because backlog items and
|
|
/// unscheduled captures do not have timeline placement yet.
|
|
/// - [stats] records quiet metadata for future reports. It should not clutter
|
|
/// the everyday UI unless a report or filter specifically needs it.
|
|
/// - [parentTaskId] links child tasks to a larger parent task without requiring
|
|
/// a nested object graph. That keeps persistence simple and avoids recursive
|
|
/// scheduling structures.
|
|
///
|
|
/// This is still a starter V1 model, so behavior changes should be explicit and
|
|
/// backed by tests as the product rules settle.
|
|
class Task {
|
|
const Task({
|
|
required this.id,
|
|
required this.title,
|
|
required this.projectId,
|
|
required this.type,
|
|
required this.status,
|
|
required this.createdAt,
|
|
required this.updatedAt,
|
|
this.priority,
|
|
this.reward = RewardLevel.notSet,
|
|
this.difficulty = DifficultyLevel.notSet,
|
|
this.durationMinutes,
|
|
this.scheduledStart,
|
|
this.scheduledEnd,
|
|
this.parentTaskId,
|
|
this.backlogTags = const <BacklogTag>{},
|
|
this.stats = const TaskStatistics(),
|
|
});
|
|
|
|
/// Create a minimal captured task without requiring planning details.
|
|
///
|
|
/// Quick capture is intentionally forgiving: the user can enter only a title
|
|
/// and the system can still create a valid backlog item. Defaults route the
|
|
/// item into the inbox project as a medium-priority flexible backlog task.
|
|
///
|
|
/// The only hard validation here is that [title] must contain non-whitespace
|
|
/// text. Scheduling validation, such as requiring a positive duration, happens
|
|
/// in `quick_capture.dart` because that depends on the requested capture flow.
|
|
factory Task.quickCapture({
|
|
required String id,
|
|
required String title,
|
|
required DateTime createdAt,
|
|
String projectId = 'inbox',
|
|
TaskType type = TaskType.flexible,
|
|
TaskStatus status = TaskStatus.backlog,
|
|
PriorityLevel priority = PriorityLevel.medium,
|
|
RewardLevel reward = RewardLevel.notSet,
|
|
DifficultyLevel difficulty = DifficultyLevel.notSet,
|
|
Set<BacklogTag> backlogTags = const <BacklogTag>{},
|
|
DateTime? updatedAt,
|
|
}) {
|
|
if (title.trim().isEmpty) {
|
|
throw ArgumentError.value(title, 'title', 'Title is required.');
|
|
}
|
|
|
|
return Task(
|
|
id: id,
|
|
title: title.trim(),
|
|
projectId: projectId,
|
|
type: type,
|
|
status: status,
|
|
priority: priority,
|
|
reward: reward,
|
|
difficulty: difficulty,
|
|
createdAt: createdAt,
|
|
updatedAt: updatedAt ?? createdAt,
|
|
backlogTags: backlogTags,
|
|
);
|
|
}
|
|
|
|
/// Stable identifier used by persistence, UI selection, and scheduler changes.
|
|
final String id;
|
|
|
|
/// User-facing task title. The model expects this to already be trimmed.
|
|
final String title;
|
|
|
|
/// Owning project/profile id. `inbox` is used for uncategorized captures.
|
|
final String projectId;
|
|
|
|
/// Scheduling behavior category. See [TaskType] for rule-level meaning.
|
|
final TaskType type;
|
|
|
|
/// Current lifecycle state. See [TaskStatus] for state-level meaning.
|
|
final TaskStatus status;
|
|
|
|
/// Optional importance. Most creation paths default this to medium, but it is
|
|
/// nullable to leave room for imports or legacy data that have not set it yet.
|
|
final PriorityLevel? priority;
|
|
|
|
/// Motivational payoff used by backlog sorting and future planning hints.
|
|
final RewardLevel reward;
|
|
|
|
/// Activation/effort estimate used by backlog sorting and future planning hints.
|
|
final DifficultyLevel difficulty;
|
|
|
|
/// Estimated task length. Required for most scheduling operations, optional for
|
|
/// backlog capture because not every captured thought has an estimate yet.
|
|
final int? durationMinutes;
|
|
|
|
/// Inclusive scheduled start time. Null means the task is not currently placed.
|
|
final DateTime? scheduledStart;
|
|
|
|
/// Exclusive scheduled end time. Null means the task is not currently placed.
|
|
final DateTime? scheduledEnd;
|
|
|
|
/// Parent task id when this task is a child/subtask. Null means top-level task.
|
|
final String? parentTaskId;
|
|
|
|
/// Backlog-specific flags, such as wishlist/someday behavior.
|
|
final Set<BacklogTag> backlogTags;
|
|
|
|
/// Creation timestamp used for age/staleness sorting.
|
|
final DateTime createdAt;
|
|
|
|
/// Last domain-level update timestamp. Scheduling actions set this when moving
|
|
/// or changing tasks so persistence and reports can detect recent activity.
|
|
final DateTime updatedAt;
|
|
|
|
/// Quiet counters for reporting and later heuristics.
|
|
final TaskStatistics stats;
|
|
|
|
/// Convenience predicate for the task type most scheduler movement operates on.
|
|
bool get isFlexible => type == TaskType.flexible;
|
|
|
|
/// Critical and inflexible tasks are both visible to the user and treated as
|
|
/// blocked time by flexible scheduling.
|
|
bool get isRequiredVisible =>
|
|
type == TaskType.critical || type == TaskType.inflexible;
|
|
|
|
/// Locked tasks behave as timeline constraints, not normal interactive cards.
|
|
bool get isLocked => type == TaskType.locked;
|
|
|
|
/// Backlog status means the task is stored for later and has no active slot.
|
|
bool get isBacklog => status == TaskStatus.backlog;
|
|
|
|
/// Return a copy with selected fields changed.
|
|
///
|
|
/// The core uses this instead of mutation. That matters because scheduling
|
|
/// operations often need to produce an auditable before/after result, including
|
|
/// [SchedulingChange]-style records elsewhere.
|
|
///
|
|
/// [clearSchedule] is a deliberate escape hatch for nullable schedule fields.
|
|
/// Without it, passing null would be ambiguous: it could mean "do not change"
|
|
/// or "clear this value." When [clearSchedule] is true, both schedule fields
|
|
/// are removed even if [scheduledStart] or [scheduledEnd] are omitted.
|
|
Task copyWith({
|
|
String? id,
|
|
String? title,
|
|
String? projectId,
|
|
TaskType? type,
|
|
TaskStatus? status,
|
|
PriorityLevel? priority,
|
|
RewardLevel? reward,
|
|
DifficultyLevel? difficulty,
|
|
int? durationMinutes,
|
|
DateTime? scheduledStart,
|
|
DateTime? scheduledEnd,
|
|
String? parentTaskId,
|
|
Set<BacklogTag>? backlogTags,
|
|
DateTime? createdAt,
|
|
DateTime? updatedAt,
|
|
TaskStatistics? stats,
|
|
bool clearSchedule = false,
|
|
}) {
|
|
return Task(
|
|
id: id ?? this.id,
|
|
title: title ?? this.title,
|
|
projectId: projectId ?? this.projectId,
|
|
type: type ?? this.type,
|
|
status: status ?? this.status,
|
|
priority: priority ?? this.priority,
|
|
reward: reward ?? this.reward,
|
|
difficulty: difficulty ?? this.difficulty,
|
|
durationMinutes: durationMinutes ?? this.durationMinutes,
|
|
scheduledStart:
|
|
clearSchedule ? null : (scheduledStart ?? this.scheduledStart),
|
|
scheduledEnd: clearSchedule ? null : (scheduledEnd ?? this.scheduledEnd),
|
|
parentTaskId: parentTaskId ?? this.parentTaskId,
|
|
backlogTags: backlogTags ?? this.backlogTags,
|
|
createdAt: createdAt ?? this.createdAt,
|
|
updatedAt: updatedAt ?? this.updatedAt,
|
|
stats: stats ?? this.stats,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Starter project defaults used when creating or scheduling tasks.
|
|
///
|
|
/// A project profile represents reusable defaults for a group of tasks. UI code
|
|
/// can let the user pick a project, then call [createTask] so new tasks inherit
|
|
/// a color, default priority, reward, difficulty, reminder profile, and duration.
|
|
///
|
|
/// The scheduler itself mostly cares about the resulting [Task] fields. Keeping
|
|
/// project defaults separate prevents every scheduling function from needing to
|
|
/// know project configuration details.
|
|
class ProjectProfile {
|
|
const ProjectProfile({
|
|
required this.id,
|
|
required this.name,
|
|
required this.colorKey,
|
|
this.defaultPriority = PriorityLevel.medium,
|
|
this.defaultReward = RewardLevel.notSet,
|
|
this.defaultDifficulty = DifficultyLevel.notSet,
|
|
this.defaultReminderProfile = ReminderProfile.gentle,
|
|
this.defaultDurationMinutes,
|
|
});
|
|
|
|
/// Stable project id stored on tasks.
|
|
final String id;
|
|
|
|
/// User-facing project name.
|
|
final String name;
|
|
|
|
/// Theme/color token for UI rendering. This is a key, not a raw color value.
|
|
final String colorKey;
|
|
|
|
/// Default importance assigned when a task does not override priority.
|
|
final PriorityLevel defaultPriority;
|
|
|
|
/// Default motivational payoff assigned when a task does not override reward.
|
|
final RewardLevel defaultReward;
|
|
|
|
/// Default activation difficulty assigned when a task does not override effort.
|
|
final DifficultyLevel defaultDifficulty;
|
|
|
|
/// Default reminder behavior for future notification/UI layers.
|
|
final ReminderProfile defaultReminderProfile;
|
|
|
|
/// Optional duration estimate used for newly created tasks.
|
|
final int? defaultDurationMinutes;
|
|
|
|
/// Create a task using project defaults while allowing explicit overrides.
|
|
///
|
|
/// This keeps capture and project-default behavior in one place. Callers can
|
|
/// pass only the fields the user explicitly set; everything else falls back to
|
|
/// this profile. The created task receives this profile's [id] as [Task.projectId].
|
|
Task createTask({
|
|
required String id,
|
|
required String title,
|
|
required DateTime createdAt,
|
|
TaskType type = TaskType.flexible,
|
|
TaskStatus status = TaskStatus.backlog,
|
|
PriorityLevel? priority,
|
|
RewardLevel? reward,
|
|
DifficultyLevel? difficulty,
|
|
int? durationMinutes,
|
|
DateTime? scheduledStart,
|
|
DateTime? scheduledEnd,
|
|
String? parentTaskId,
|
|
Set<BacklogTag> backlogTags = const <BacklogTag>{},
|
|
DateTime? updatedAt,
|
|
}) {
|
|
final trimmedTitle = title.trim();
|
|
if (trimmedTitle.isEmpty) {
|
|
throw ArgumentError.value(title, 'title', 'Title is required.');
|
|
}
|
|
|
|
return Task(
|
|
id: id,
|
|
title: trimmedTitle,
|
|
projectId: this.id,
|
|
type: type,
|
|
status: status,
|
|
priority: priority ?? defaultPriority,
|
|
reward: reward ?? defaultReward,
|
|
difficulty: difficulty ?? defaultDifficulty,
|
|
durationMinutes: durationMinutes ?? defaultDurationMinutes,
|
|
scheduledStart: scheduledStart,
|
|
scheduledEnd: scheduledEnd,
|
|
parentTaskId: parentTaskId,
|
|
backlogTags: backlogTags,
|
|
createdAt: createdAt,
|
|
updatedAt: updatedAt ?? createdAt,
|
|
);
|
|
}
|
|
|
|
/// Return a copy with selected project defaults changed.
|
|
ProjectProfile copyWith({
|
|
String? id,
|
|
String? name,
|
|
String? colorKey,
|
|
PriorityLevel? defaultPriority,
|
|
RewardLevel? defaultReward,
|
|
DifficultyLevel? defaultDifficulty,
|
|
ReminderProfile? defaultReminderProfile,
|
|
int? defaultDurationMinutes,
|
|
}) {
|
|
return ProjectProfile(
|
|
id: id ?? this.id,
|
|
name: name ?? this.name,
|
|
colorKey: colorKey ?? this.colorKey,
|
|
defaultPriority: defaultPriority ?? this.defaultPriority,
|
|
defaultReward: defaultReward ?? this.defaultReward,
|
|
defaultDifficulty: defaultDifficulty ?? this.defaultDifficulty,
|
|
defaultReminderProfile:
|
|
defaultReminderProfile ?? this.defaultReminderProfile,
|
|
defaultDurationMinutes:
|
|
defaultDurationMinutes ?? this.defaultDurationMinutes,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Starter time range value used by scheduling helpers.
|
|
///
|
|
/// [TimeInterval] is the scheduler's neutral representation of a time span. It
|
|
/// is used for scheduled task slots, locked blocks, required visible blocks, and
|
|
/// candidate placements. The interval convention is start-inclusive and
|
|
/// end-exclusive, which avoids treating two back-to-back blocks as overlapping.
|
|
class TimeInterval {
|
|
const TimeInterval({
|
|
required this.start,
|
|
required this.end,
|
|
this.label,
|
|
});
|
|
|
|
/// Inclusive beginning of the interval.
|
|
final DateTime start;
|
|
|
|
/// Exclusive ending of the interval.
|
|
final DateTime end;
|
|
|
|
/// Optional debug/UI label, often a task id or locked block name.
|
|
final String? label;
|
|
|
|
/// Raw duration between [start] and [end]. Callers are responsible for only
|
|
/// constructing meaningful positive intervals when required by a rule.
|
|
Duration get duration => end.difference(start);
|
|
|
|
/// Whether this interval shares any actual time with [other].
|
|
///
|
|
/// Adjacent intervals do not overlap: `9:00-10:00` and `10:00-11:00` are safe
|
|
/// to place back-to-back because the first interval's end is the second
|
|
/// interval's start.
|
|
bool overlaps(TimeInterval other) {
|
|
return start.isBefore(other.end) && end.isAfter(other.start);
|
|
}
|
|
}
|