focus-flow/apps/focus_flow_flutter/lib/controllers/scheduler_command_controller.dart

249 lines
7.4 KiB
Dart

/// Coordinates Scheduler Command Controller state for the FocusFlow Flutter UI.
library;
import 'package:flutter/foundation.dart';
import 'package:scheduler_core/scheduler_core.dart';
/// Builds an application operation context for a UI command operation.
typedef OperationContextFactory =
ApplicationOperationContext Function(String operationId, {DateTime? now});
/// Refreshes any reads that should reflect a completed command.
typedef ReadRefresh = Future<void> Function();
/// Base state for scheduler command execution in the UI.
sealed class SchedulerCommandState {
/// Creates a scheduler command state.
const SchedulerCommandState();
}
/// Indicates that no scheduler command is currently running.
class SchedulerCommandIdle extends SchedulerCommandState {
/// Creates an idle command state.
const SchedulerCommandIdle();
}
/// Indicates that a scheduler command is currently running.
class SchedulerCommandRunning extends SchedulerCommandState {
/// Creates a running command state with a user-facing [label].
const SchedulerCommandRunning(this.label);
/// User-facing description of the command in progress.
final String label;
}
/// Indicates that a scheduler command completed successfully.
class SchedulerCommandSuccess extends SchedulerCommandState {
/// Creates a success command state with a user-facing [message].
const SchedulerCommandSuccess(this.message);
/// User-facing success message.
final String message;
}
/// Indicates that a scheduler command failed.
class SchedulerCommandFailure extends SchedulerCommandState {
/// Creates a failure state from an application [failure] or unexpected [error].
const SchedulerCommandFailure({this.failure, this.error});
/// Typed application failure returned by the scheduler core.
final ApplicationFailure? failure;
/// Unexpected error thrown while running the command.
final Object? error;
/// Stable label for display and diagnostics.
String get codeLabel {
final typed = failure;
if (typed != null) {
return typed.detailCode ?? typed.code.name;
}
return 'unexpected';
}
}
/// Runs scheduler write commands and exposes command state to widgets.
class SchedulerCommandController extends ChangeNotifier {
/// Creates a command controller from scheduler use cases and UI callbacks.
SchedulerCommandController({
required this.commands,
required this.contextFor,
required this.localDate,
required this.refreshReads,
DateTime Function()? now,
}) : now = now ?? _utcNow;
/// Scheduler write use cases invoked by this controller.
final V1ApplicationCommandUseCases commands;
/// Factory for command-scoped application operation contexts.
final OperationContextFactory contextFor;
/// Local date commands should operate against.
final CivilDate localDate;
/// Refresh callback invoked after commands that mutate read state.
final ReadRefresh refreshReads;
/// Clock used by direct UI commands such as marking a task complete.
final DateTime Function() now;
var _sequence = 0;
SchedulerCommandState _state = const SchedulerCommandIdle();
/// Current command execution state.
SchedulerCommandState get state => _state;
/// Captures [title] as a backlog task.
Future<void> quickCaptureToBacklog(String title) async {
await _run(
label: 'Capturing task',
successMessage: 'Task captured',
refreshAfterSuccess: true,
action: (operationId) {
return commands.quickCaptureToBacklog(
context: contextFor(operationId),
title: title,
projectId: 'home',
);
},
);
}
/// Schedules a backlog task into the next available slot.
Future<void> scheduleBacklogItem({
required String taskId,
required int durationMinutes,
required DateTime expectedUpdatedAt,
}) async {
await _run(
label: 'Scheduling task',
successMessage: 'Task scheduled',
refreshAfterSuccess: true,
action: (operationId) {
return commands.scheduleBacklogItemToNextAvailableSlot(
context: contextFor(operationId),
localDate: localDate,
taskId: taskId,
durationMinutes: durationMinutes,
expectedUpdatedAt: expectedUpdatedAt,
);
},
);
}
/// Completes a planned flexible task from the Today view.
Future<void> completeFlexibleTask({
required String taskId,
DateTime? expectedUpdatedAt,
}) async {
await completeTask(
taskId: taskId,
taskType: TaskType.flexible,
expectedUpdatedAt: expectedUpdatedAt,
);
}
/// Completes a planned task from the Today timeline.
Future<void> completeTask({
required String taskId,
required TaskType taskType,
DateTime? expectedUpdatedAt,
}) async {
final completedAt = now();
await _run(
label: 'Completing task',
successMessage: 'Task completed',
refreshAfterSuccess: true,
action: (operationId) {
final context = contextFor(operationId, now: completedAt);
return switch (taskType) {
TaskType.flexible => commands.completeFlexibleTask(
context: context,
localDate: localDate,
taskId: taskId,
expectedUpdatedAt: expectedUpdatedAt,
),
TaskType.critical ||
TaskType.inflexible => commands.applyRequiredTaskAction(
context: context,
localDate: localDate,
taskId: taskId,
action: RequiredTaskAction.done,
expectedUpdatedAt: expectedUpdatedAt,
),
TaskType.locked ||
TaskType.surprise ||
TaskType.freeSlot => Future.value(
ApplicationResult.failure(
ApplicationFailure(
code: ApplicationFailureCode.validation,
entityId: taskId,
detailCode: 'unsupportedTaskType',
),
),
),
};
},
);
}
/// Returns a completed task to its planned state.
Future<void> uncompleteTask({
required String taskId,
DateTime? expectedUpdatedAt,
}) async {
final occurredAt = now();
await _run(
label: 'Reopening task',
successMessage: 'Task marked as not done',
refreshAfterSuccess: true,
action: (operationId) {
return commands.uncompleteTask(
context: contextFor(operationId, now: occurredAt),
localDate: localDate,
taskId: taskId,
expectedUpdatedAt: expectedUpdatedAt,
);
},
);
}
Future<void> _run({
required String label,
required String successMessage,
required bool refreshAfterSuccess,
required Future<ApplicationResult<ApplicationCommandResult>> Function(
String operationId,
)
action,
}) async {
final operationId = _nextOperationId();
_setState(SchedulerCommandRunning(label));
try {
final result = await action(operationId);
final failure = result.failure;
if (failure != null) {
_setState(SchedulerCommandFailure(failure: failure));
return;
}
if (refreshAfterSuccess) {
await refreshReads();
}
_setState(SchedulerCommandSuccess(successMessage));
} on Object catch (error) {
_setState(SchedulerCommandFailure(error: error));
}
}
String _nextOperationId() {
_sequence += 1;
return 'ui-command-$_sequence';
}
void _setState(SchedulerCommandState next) {
_state = next;
notifyListeners();
}
static DateTime _utcNow() => DateTime.now().toUtc();
}