249 lines
7.4 KiB
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();
|
|
}
|