// Low-friction capture flow. // // Quick capture is the "dump the thought before it disappears" path. The goal // is to accept minimal data, preserve the user's input, and only ask for more // structure when the user wants immediate scheduling. import 'models.dart'; import 'scheduling_engine.dart'; /// Outcome of a quick-capture request. /// /// The UI can use this status to decide whether to show a passive success, draw /// a scheduled card on the timeline, or display validation messages. It is not /// an exception-based flow because quick capture should fail gently and keep the /// user's typed task available. enum QuickCaptureStatus { /// Capture succeeded and the task remains unscheduled in backlog. addedToBacklog, /// Capture succeeded and the task was placed on the timeline. scheduled, /// Capture could not complete the requested flow; see result messages. validationError, } /// Input for low-friction task capture. /// /// This object represents what the UI knows at the moment of capture. It mirrors /// the product goal: adding a thought should require as little structure as /// possible, but the user can optionally provide enough detail to immediately /// schedule it into the next open flexible slot. class QuickCaptureRequest { const QuickCaptureRequest({ required this.id, required this.title, required this.createdAt, this.addToNextAvailableSlot = false, this.projectId = 'inbox', this.priority = PriorityLevel.medium, this.reward = RewardLevel.notSet, this.difficulty = DifficultyLevel.notSet, this.type = TaskType.flexible, this.durationMinutes, this.backlogTags = const {}, }); /// Caller-generated id. Keeping id generation outside this service makes the /// domain layer independent from persistence/database choices. final String id; /// Raw user-entered title. The [Task.quickCapture] factory trims it. final String title; /// Capture timestamp supplied by the caller for testability. final DateTime createdAt; /// Whether capture should attempt immediate timeline placement. final bool addToNextAvailableSlot; /// Project id to assign; defaults to the inbox for uncategorized thoughts. final String projectId; /// Initial priority used by backlog/scheduler heuristics. final PriorityLevel priority; /// Initial reward estimate. final RewardLevel reward; /// Initial difficulty estimate. final DifficultyLevel difficulty; /// Captured task type. Immediate scheduling currently requires flexible tasks. final TaskType type; /// Optional duration estimate. Required only when scheduling immediately. final int? durationMinutes; /// Optional backlog flags such as wishlist/someday. final Set backlogTags; } /// Result of a quick-capture request. /// /// The result always carries a [task], even on validation failure, so the UI can /// preserve the user's input and show what needs to be fixed. When scheduling /// was attempted, [schedulingResult] exposes the lower-level engine notices and /// changes for debugging or timeline updates. class QuickCaptureResult { const QuickCaptureResult({ required this.task, required this.status, this.schedulingResult, this.messages = const [], }); /// Captured task, scheduled or unscheduled depending on [status]. final Task task; /// High-level outcome of the capture attempt. final QuickCaptureStatus status; /// Detailed scheduling output when immediate placement was attempted. final SchedulingResult? schedulingResult; /// Human-readable validation or scheduling messages. final List messages; /// Convenience check for UI branches that only care whether capture succeeded. bool get isValid => status != QuickCaptureStatus.validationError; } /// Coordinates quick capture defaults and optional scheduling. /// /// This service is intentionally thin: it builds a [Task], validates the extra /// requirements for immediate scheduling, and delegates placement to /// [SchedulingEngine]. It keeps quick-capture UI code from needing to understand /// every scheduler precondition. class QuickCaptureService { const QuickCaptureService({this.engine = const SchedulingEngine()}); /// Scheduling dependency. Defaults to the starter engine but can be swapped in /// tests or future implementations. final SchedulingEngine engine; /// Capture a task and optionally place it into the next available slot. /// /// Flow: /// 1. Build the task using lightweight defaults. /// 2. If immediate scheduling was not requested, return a backlog success. /// 3. Validate the requirements for immediate scheduling. /// 4. Call [SchedulingEngine.insertBacklogTaskIntoNextAvailableSlot]. /// 5. Convert the lower-level scheduling result into a capture result. QuickCaptureResult capture( QuickCaptureRequest request, { SchedulingInput? schedulingInput, DateTime? updatedAt, }) { final capturedTask = Task.quickCapture( id: request.id, title: request.title, createdAt: request.createdAt, projectId: request.projectId, type: request.type, priority: request.priority, reward: request.reward, difficulty: request.difficulty, backlogTags: request.backlogTags, updatedAt: updatedAt, ).copyWith(durationMinutes: request.durationMinutes); if (!request.addToNextAvailableSlot) { return QuickCaptureResult( task: capturedTask, status: QuickCaptureStatus.addedToBacklog, ); } if (request.durationMinutes == null || request.durationMinutes! <= 0) { return QuickCaptureResult( task: capturedTask, status: QuickCaptureStatus.validationError, messages: const ['Duration is required to schedule quick capture.'], ); } if (request.type != TaskType.flexible) { return QuickCaptureResult( task: capturedTask, status: QuickCaptureStatus.validationError, messages: const ['Scheduled quick capture must be flexible.'], ); } if (schedulingInput == null) { return QuickCaptureResult( task: capturedTask, status: QuickCaptureStatus.validationError, messages: const ['Scheduling input is required.'], ); } final result = engine.insertBacklogTaskIntoNextAvailableSlot( input: SchedulingInput( tasks: [capturedTask, ...schedulingInput.tasks], window: schedulingInput.window, lockedIntervals: schedulingInput.lockedIntervals, requiredVisibleIntervals: schedulingInput.requiredVisibleIntervals, ), taskId: capturedTask.id, updatedAt: updatedAt, ); final scheduledTask = _taskById(result.tasks, capturedTask.id); if (scheduledTask == null || scheduledTask.isBacklog) { return QuickCaptureResult( task: capturedTask, status: QuickCaptureStatus.validationError, schedulingResult: result, messages: result.notices.map((notice) => notice.message).toList(), ); } return QuickCaptureResult( task: scheduledTask, status: QuickCaptureStatus.scheduled, schedulingResult: result, ); } } /// Find a task in a returned scheduling result. /// /// This duplicates a small helper rather than exposing scheduler internals. The /// capture service only needs to retrieve the newly created task after placement. Task? _taskById(List tasks, String id) { for (final task in tasks) { if (task.id == id) { return task; } } return null; }