277 lines
8.4 KiB
Dart
277 lines
8.4 KiB
Dart
// 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 <PushDestination>[],
|
|
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<PushDestination> 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<Task>.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<Task> tasks, String taskId) {
|
|
for (final task in tasks) {
|
|
if (task.id == taskId) {
|
|
return task;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|