forked from eva/focus-flow
feat(tasks): apply push destinations
This commit is contained in:
parent
f0f8553042
commit
f7f239c3f7
3 changed files with 267 additions and 1 deletions
|
|
@ -46,6 +46,8 @@ Execution notes:
|
|||
|
||||
Recommended Codex level: high
|
||||
|
||||
Status: Completed
|
||||
|
||||
Tasks:
|
||||
|
||||
Implement push destinations:
|
||||
|
|
@ -64,6 +66,17 @@ Acceptance criteria:
|
|||
- Test: tomorrow sets tomorrow/top-of-queue metadata.
|
||||
- Test: backlog clears schedule placement.
|
||||
|
||||
Execution notes:
|
||||
|
||||
- Added `PushDestinationResult` to carry selected destination metadata with the scheduling result.
|
||||
- Added `FlexibleTaskActionService.applyPushDestination`.
|
||||
- Next available delegates to `SchedulingEngine.pushFlexibleTaskToNextAvailableSlot`.
|
||||
- Tomorrow/top-of-queue delegates to `SchedulingEngine.pushFlexibleTaskToTomorrowTopOfQueue` and reports `placesAtTomorrowTopOfQueue`.
|
||||
- Backlog destination updates the task list with `SchedulingEngine.moveToBacklog`, clears placement, and records a scheduling change.
|
||||
- `PushDestination` remains limited to next available slot, tomorrow/top of queue, and backlog; later today is not represented.
|
||||
- Added tests for next available, tomorrow/top-of-queue metadata, backlog clearing schedule placement, and the excluded later-today destination.
|
||||
- `dart format lib test`, `dart analyze`, `dart test`, and `git diff --check` passed.
|
||||
|
||||
BREAKPOINT: Stop here. Confirm `high` mode before integrating push behavior.
|
||||
|
||||
## Chunk 6.3 — Required task states
|
||||
|
|
|
|||
|
|
@ -33,6 +33,21 @@ class FlexibleTaskActionResult {
|
|||
bool get changedTask => !startsChildTaskFlow && pushDestinations.isEmpty;
|
||||
}
|
||||
|
||||
/// Result from applying a selected push destination.
|
||||
class PushDestinationResult {
|
||||
const PushDestinationResult({
|
||||
required this.destination,
|
||||
required this.schedulingResult,
|
||||
});
|
||||
|
||||
final PushDestination destination;
|
||||
final SchedulingResult schedulingResult;
|
||||
|
||||
bool get placesAtTomorrowTopOfQueue {
|
||||
return destination == PushDestination.tomorrowTopOfQueue;
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies low-friction quick actions for flexible task cards.
|
||||
class FlexibleTaskActionService {
|
||||
const FlexibleTaskActionService({
|
||||
|
|
@ -83,4 +98,112 @@ class FlexibleTaskActionService {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Task? _taskById(List<Task> tasks, String taskId) {
|
||||
for (final task in tasks) {
|
||||
if (task.id == taskId) {
|
||||
return task;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -864,7 +864,11 @@ void main() {
|
|||
PushDestination.tomorrowTopOfQueue,
|
||||
PushDestination.backlog,
|
||||
]);
|
||||
expect(result.pushDestinations, isNot(contains('laterToday')));
|
||||
expect(PushDestination.values, const [
|
||||
PushDestination.nextAvailableSlot,
|
||||
PushDestination.tomorrowTopOfQueue,
|
||||
PushDestination.backlog,
|
||||
]);
|
||||
expect(result.changedTask, isFalse);
|
||||
});
|
||||
|
||||
|
|
@ -886,6 +890,132 @@ void main() {
|
|||
expect(result.startsChildTaskFlow, isTrue);
|
||||
expect(result.changedTask, isFalse);
|
||||
});
|
||||
|
||||
test('push destination next available uses scheduling engine', () {
|
||||
const service = FlexibleTaskActionService();
|
||||
final task = Task.quickCapture(
|
||||
id: 'task-1',
|
||||
title: 'Handle paperwork',
|
||||
createdAt: now,
|
||||
).copyWith(
|
||||
status: TaskStatus.planned,
|
||||
durationMinutes: 15,
|
||||
scheduledStart: DateTime(2026, 6, 19, 13),
|
||||
scheduledEnd: DateTime(2026, 6, 19, 13, 15),
|
||||
);
|
||||
|
||||
final result = service.applyPushDestination(
|
||||
destination: PushDestination.nextAvailableSlot,
|
||||
input: SchedulingInput(
|
||||
tasks: [task],
|
||||
window: SchedulingWindow(
|
||||
start: DateTime(2026, 6, 19, 13),
|
||||
end: DateTime(2026, 6, 19, 14),
|
||||
),
|
||||
lockedIntervals: [
|
||||
TimeInterval(
|
||||
start: DateTime(2026, 6, 19, 13, 15),
|
||||
end: DateTime(2026, 6, 19, 13, 30),
|
||||
label: 'locked-block',
|
||||
),
|
||||
],
|
||||
),
|
||||
taskId: 'task-1',
|
||||
updatedAt: now,
|
||||
);
|
||||
final pushed = result.schedulingResult.tasks.single;
|
||||
|
||||
expect(result.destination, PushDestination.nextAvailableSlot);
|
||||
expect(pushed.scheduledStart, DateTime(2026, 6, 19, 13, 30));
|
||||
expect(pushed.scheduledEnd, DateTime(2026, 6, 19, 13, 45));
|
||||
expect(pushed.stats.manuallyPushedCount, 1);
|
||||
expect(result.schedulingResult.notices.single.type,
|
||||
SchedulingNoticeType.moved);
|
||||
});
|
||||
|
||||
test('push destination tomorrow sets tomorrow top-of-queue metadata', () {
|
||||
const service = FlexibleTaskActionService();
|
||||
final task = Task.quickCapture(
|
||||
id: 'task-1',
|
||||
title: 'Handle paperwork',
|
||||
createdAt: now,
|
||||
).copyWith(
|
||||
status: TaskStatus.planned,
|
||||
durationMinutes: 15,
|
||||
scheduledStart: DateTime(2026, 6, 19, 13),
|
||||
scheduledEnd: DateTime(2026, 6, 19, 13, 15),
|
||||
);
|
||||
final tomorrowTask = Task.quickCapture(
|
||||
id: 'tomorrow-1',
|
||||
title: 'Fold laundry',
|
||||
createdAt: now,
|
||||
).copyWith(
|
||||
status: TaskStatus.planned,
|
||||
durationMinutes: 30,
|
||||
scheduledStart: DateTime(2026, 6, 20, 9),
|
||||
scheduledEnd: DateTime(2026, 6, 20, 9, 30),
|
||||
);
|
||||
|
||||
final result = service.applyPushDestination(
|
||||
destination: PushDestination.tomorrowTopOfQueue,
|
||||
input: SchedulingInput(
|
||||
tasks: [task, tomorrowTask],
|
||||
window: SchedulingWindow(
|
||||
start: DateTime(2026, 6, 20, 9),
|
||||
end: DateTime(2026, 6, 20, 10),
|
||||
),
|
||||
),
|
||||
taskId: 'task-1',
|
||||
updatedAt: now,
|
||||
);
|
||||
final pushed = result.schedulingResult.tasks.singleWhere(
|
||||
(current) => current.id == 'task-1',
|
||||
);
|
||||
final shiftedTomorrow = result.schedulingResult.tasks.singleWhere(
|
||||
(current) => current.id == 'tomorrow-1',
|
||||
);
|
||||
|
||||
expect(result.destination, PushDestination.tomorrowTopOfQueue);
|
||||
expect(result.placesAtTomorrowTopOfQueue, isTrue);
|
||||
expect(pushed.scheduledStart, DateTime(2026, 6, 20, 9));
|
||||
expect(pushed.scheduledEnd, DateTime(2026, 6, 20, 9, 15));
|
||||
expect(shiftedTomorrow.scheduledStart, DateTime(2026, 6, 20, 9, 15));
|
||||
expect(shiftedTomorrow.scheduledEnd, DateTime(2026, 6, 20, 9, 45));
|
||||
});
|
||||
|
||||
test('push destination backlog clears schedule placement', () {
|
||||
const service = FlexibleTaskActionService();
|
||||
final task = Task.quickCapture(
|
||||
id: 'task-1',
|
||||
title: 'Handle paperwork',
|
||||
createdAt: now,
|
||||
).copyWith(
|
||||
status: TaskStatus.planned,
|
||||
scheduledStart: DateTime(2026, 6, 19, 13),
|
||||
scheduledEnd: DateTime(2026, 6, 19, 13, 30),
|
||||
);
|
||||
|
||||
final result = service.applyPushDestination(
|
||||
destination: PushDestination.backlog,
|
||||
input: SchedulingInput(
|
||||
tasks: [task],
|
||||
window: SchedulingWindow(
|
||||
start: DateTime(2026, 6, 19, 13),
|
||||
end: DateTime(2026, 6, 19, 14),
|
||||
),
|
||||
),
|
||||
taskId: 'task-1',
|
||||
updatedAt: now,
|
||||
);
|
||||
final moved = result.schedulingResult.tasks.single;
|
||||
|
||||
expect(result.destination, PushDestination.backlog);
|
||||
expect(moved.status, TaskStatus.backlog);
|
||||
expect(moved.scheduledStart, isNull);
|
||||
expect(moved.scheduledEnd, isNull);
|
||||
expect(moved.stats.movedToBacklogCount, 1);
|
||||
expect(result.schedulingResult.changes.single.nextStart, isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('SchedulingEngine starter behavior', () {
|
||||
|
|
|
|||
Loading…
Reference in a new issue