// 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 {}, 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 backlogTags = const {}, 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 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? 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 backlogTags = const {}, 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); } }