// Flexible task card actions. // // The main scheduling engine moves tasks through time. This file models the // small user actions that appear on a flexible task card, such as done, push, // backlog, and break-up. Keeping these in a service makes UI button handlers // thin and keeps task-type safety checks in one place. import 'models.dart'; import 'scheduling_engine.dart'; /// Quick actions available from a flexible task card. /// /// These are the low-friction card controls the UI can expose directly on a /// planned flexible task. The service below translates each button into either a /// direct task update, a scheduling operation, or a follow-up flow. enum FlexibleTaskQuickAction { /// Mark the task completed. done, /// Ask the user where the task should be pushed. push, /// Move the task out of today's plan and into backlog. backlog, /// Start a flow that splits the task into child tasks. breakUp, } /// Explicit push destinations shown after choosing the push quick action. /// /// Push starts as a simple quick action, but the actual destination requires one /// more choice. Keeping destinations as a separate enum prevents the initial card /// action list from becoming too crowded. enum PushDestination { /// Move the task later within the current planning window. nextAvailableSlot, /// Move the task to the beginning of the supplied tomorrow/future window. tomorrowTopOfQueue, /// Remove the task from the active timeline and store it for later. backlog, } /// Domain result for a flexible task quick action. /// /// This result deliberately supports three outcomes: the task changed, the user /// must choose a push destination, or the UI should start the child-task flow. /// That keeps card code from guessing how to interpret each action. class FlexibleTaskActionResult { const FlexibleTaskActionResult({ required this.action, required this.task, this.pushDestinations = const [], this.startsChildTaskFlow = false, }); /// Action the user selected. final FlexibleTaskQuickAction action; /// Current or updated task, depending on the action. final Task task; /// Destination choices to show after `push`; empty for direct actions. final List pushDestinations; /// Whether the UI should open a child-task creation flow. final bool startsChildTaskFlow; /// True when the action directly produced an updated [task]. bool get changedTask => !startsChildTaskFlow && pushDestinations.isEmpty; } /// Result from applying a selected push destination. /// /// The selected destination is included next to the [SchedulingResult] so UI and /// tests can distinguish "moved later today" from "moved to tomorrow" even if /// the low-level scheduling change shape is similar. class PushDestinationResult { const PushDestinationResult({ required this.destination, required this.schedulingResult, }); /// Destination that was applied. final PushDestination destination; /// Full scheduler output: updated tasks, notices, changes, and overlaps. final SchedulingResult schedulingResult; /// Convenience flag for UI copy or persistence behavior that cares about the /// tomorrow queue specifically. bool get placesAtTomorrowTopOfQueue { return destination == PushDestination.tomorrowTopOfQueue; } } /// Applies low-friction quick actions for flexible task cards. /// /// This service is the adapter between small UI button presses and domain logic. /// It intentionally only accepts flexible tasks; required/locked/surprise items /// should have their own action rules so the UI cannot accidentally apply a /// flexible-only behavior to a fixed commitment. class FlexibleTaskActionService { const FlexibleTaskActionService({ this.schedulingEngine = const SchedulingEngine(), }); /// Scheduling dependency used for actions that need timeline changes. final SchedulingEngine schedulingEngine; /// Apply the first-stage quick action. /// /// Direct actions (`done`, `backlog`) return a changed task. `push` returns the /// list of destinations the UI should present. `breakUp` signals that the UI /// should start a child-task flow rather than changing the task immediately. FlexibleTaskActionResult apply({ required Task task, required FlexibleTaskQuickAction action, DateTime? updatedAt, }) { if (!task.isFlexible) { throw ArgumentError.value( task.type, 'task.type', 'Task must be flexible.'); } switch (action) { case FlexibleTaskQuickAction.done: return FlexibleTaskActionResult( action: action, task: task.copyWith( status: TaskStatus.completed, updatedAt: updatedAt ?? DateTime.now(), ), ); case FlexibleTaskQuickAction.push: return FlexibleTaskActionResult( action: action, task: task, pushDestinations: const [ PushDestination.nextAvailableSlot, PushDestination.tomorrowTopOfQueue, PushDestination.backlog, ], ); case FlexibleTaskQuickAction.backlog: return FlexibleTaskActionResult( action: action, task: schedulingEngine.moveToBacklog(task, updatedAt: updatedAt), ); case FlexibleTaskQuickAction.breakUp: return FlexibleTaskActionResult( action: action, task: task, startsChildTaskFlow: true, ); } } /// Apply the second-stage destination selected after the `push` action. /// /// This needs the full [SchedulingInput] because pushing can shift other /// flexible tasks and must avoid locked/required intervals. PushDestinationResult applyPushDestination({ required PushDestination destination, required SchedulingInput input, required String taskId, DateTime? updatedAt, }) { final result = switch (destination) { PushDestination.nextAvailableSlot => schedulingEngine.pushFlexibleTaskToNextAvailableSlot( input: input, taskId: taskId, updatedAt: updatedAt, ), PushDestination.tomorrowTopOfQueue => schedulingEngine.pushFlexibleTaskToTomorrowTopOfQueue( input: input, taskId: taskId, updatedAt: updatedAt, ), PushDestination.backlog => _moveTaskToBacklog( input: input, taskId: taskId, updatedAt: updatedAt, ), }; return PushDestinationResult( destination: destination, schedulingResult: result, ); } /// Move one planned flexible task to backlog inside a scheduling result. /// /// This mirrors the shape of other push destination results so callers can /// handle every destination through the same `SchedulingResult` interface. SchedulingResult _moveTaskToBacklog({ required SchedulingInput input, required String taskId, DateTime? updatedAt, }) { final task = _taskById(input.tasks, taskId); if (task == null) { return SchedulingResult( tasks: input.tasks, notices: [ SchedulingNotice( 'Task was not found.', type: SchedulingNoticeType.noFit, taskId: taskId, ), ], ); } if (!task.isFlexible || task.status != TaskStatus.planned) { return SchedulingResult( tasks: input.tasks, notices: [ SchedulingNotice( 'Only planned flexible tasks can be moved to backlog.', type: SchedulingNoticeType.noFit, taskId: task.id, ), ], ); } final movedTask = schedulingEngine.moveToBacklog( task, updatedAt: updatedAt, ); return SchedulingResult( tasks: List.unmodifiable( input.tasks.map((current) { if (current.id == task.id) { return movedTask; } return current; }), ), notices: [ SchedulingNotice( 'Flexible task moved to backlog.', type: SchedulingNoticeType.moved, taskId: task.id, ), ], changes: [ SchedulingChange( taskId: task.id, previousStart: task.scheduledStart, previousEnd: task.scheduledEnd, nextStart: null, nextEnd: null, ), ], ); } } /// Find one task by id in a list. Task? _taskById(List tasks, String taskId) { for (final task in tasks) { if (task.id == taskId) { return task; } } return null; }