332 lines
9.4 KiB
Dart
332 lines
9.4 KiB
Dart
/// Composes the FocusFlow Flutter application around Demo Scheduler Composition.
|
|
library;
|
|
|
|
import 'package:scheduler_core/scheduler_core.dart';
|
|
|
|
import '../controllers/scheduler_command_controller.dart';
|
|
import '../controllers/scheduler_read_controller.dart';
|
|
import '../controllers/today_screen_controller.dart';
|
|
|
|
/// Wires the Flutter demo UI to in-memory scheduler application use cases.
|
|
class DemoSchedulerComposition {
|
|
DemoSchedulerComposition._({
|
|
required this.store,
|
|
required this.todayQuery,
|
|
required this.managementUseCases,
|
|
required this.commandUseCases,
|
|
required this.date,
|
|
required this.readAt,
|
|
});
|
|
|
|
/// Fixed owner used by the seeded demo data.
|
|
static const ownerId = 'owner-1';
|
|
|
|
/// Fixed owner time zone used by the seeded demo data.
|
|
static const timeZoneId = 'UTC';
|
|
|
|
/// Fixed project id used by the seeded demo data.
|
|
static const projectId = 'home';
|
|
|
|
/// In-memory application store backing the demo.
|
|
final InMemoryApplicationUnitOfWork store;
|
|
|
|
/// Query object used to read the Today state.
|
|
final GetTodayStateQuery todayQuery;
|
|
|
|
/// Management use cases used by backlog-facing UI surfaces.
|
|
final V1ApplicationManagementUseCases managementUseCases;
|
|
|
|
/// Command use cases used by interactive UI controls.
|
|
final V1ApplicationCommandUseCases commandUseCases;
|
|
|
|
/// Local date represented by the seeded demo.
|
|
final CivilDate date;
|
|
|
|
/// Stable read clock used for deterministic demo operations.
|
|
final DateTime readAt;
|
|
|
|
/// Human-readable label for [date].
|
|
String get dateLabel => formatDateLabel(date);
|
|
|
|
/// Creates a deterministic seeded composition for the demo app and tests.
|
|
factory DemoSchedulerComposition.seeded({
|
|
Clock? clock,
|
|
CivilDate? selectedDate,
|
|
bool includeTodayItems = true,
|
|
bool includeBacklogItems = true,
|
|
}) {
|
|
final resolvedDate =
|
|
selectedDate ??
|
|
CivilDate.fromDateTime((clock ?? const SystemClock()).now());
|
|
final readAt = _instant(resolvedDate, 15, 55);
|
|
final createdAt = _instant(resolvedDate, 8);
|
|
final project = ProjectProfile(
|
|
id: projectId,
|
|
name: 'Home',
|
|
colorKey: 'project-home',
|
|
);
|
|
final tasks = <Task>[
|
|
if (includeTodayItems) ..._todaySeedTasks(resolvedDate, createdAt),
|
|
if (includeBacklogItems)
|
|
Task.quickCapture(
|
|
id: 'later-backlog-note',
|
|
title: 'Review reusable grocery list',
|
|
projectId: projectId,
|
|
createdAt: createdAt,
|
|
updatedAt: createdAt,
|
|
),
|
|
];
|
|
final store = InMemoryApplicationUnitOfWork(
|
|
initialProjects: [project],
|
|
initialOwnerSettings: [
|
|
OwnerSettings(
|
|
ownerId: ownerId,
|
|
timeZoneId: timeZoneId,
|
|
compactModeEnabled: true,
|
|
),
|
|
],
|
|
initialTasks: tasks,
|
|
);
|
|
|
|
return DemoSchedulerComposition._(
|
|
store: store,
|
|
todayQuery: GetTodayStateQuery(
|
|
applicationStore: store,
|
|
timeZoneResolver: const FixedOffsetTimeZoneResolver.utc(),
|
|
),
|
|
managementUseCases: V1ApplicationManagementUseCases(
|
|
applicationStore: store,
|
|
),
|
|
commandUseCases: V1ApplicationCommandUseCases(
|
|
applicationStore: store,
|
|
timeZoneResolver: const FixedOffsetTimeZoneResolver.utc(),
|
|
),
|
|
date: resolvedDate,
|
|
readAt: readAt,
|
|
);
|
|
}
|
|
|
|
/// Creates the controller used by the compact Today screen mockup.
|
|
TodayScreenController createTodayScreenController() {
|
|
return TodayScreenController(
|
|
read: () {
|
|
return todayQuery.execute(
|
|
GetTodayStateRequest(
|
|
context: context('ui-plan-1-read-today'),
|
|
date: date,
|
|
includeNextFlexibleItem: true,
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Creates a generic read controller for Today-state panes.
|
|
UiReadController<TodayState> createTodayController() {
|
|
return ApplicationReadController<TodayState>(
|
|
read: () {
|
|
return todayQuery.execute(
|
|
GetTodayStateRequest(
|
|
context: context('ui-read-today'),
|
|
date: date,
|
|
includeNextFlexibleItem: true,
|
|
),
|
|
);
|
|
},
|
|
isEmpty: (state) {
|
|
return state.timelineItems.isEmpty && state.pendingNotices.isEmpty;
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Creates a generic read controller for backlog panes.
|
|
UiReadController<BacklogQueryResult> createBacklogController() {
|
|
return ApplicationReadController<BacklogQueryResult>(
|
|
read: () {
|
|
return managementUseCases.getBacklog(
|
|
GetBacklogRequest(
|
|
context: context('ui-read-backlog'),
|
|
sortKey: BacklogSortKey.age,
|
|
),
|
|
);
|
|
},
|
|
isEmpty: (state) => state.items.isEmpty,
|
|
);
|
|
}
|
|
|
|
/// Creates the command controller used by interactive scheduler controls.
|
|
SchedulerCommandController createCommandController({
|
|
required ReadRefresh refreshReads,
|
|
}) {
|
|
return SchedulerCommandController(
|
|
commands: commandUseCases,
|
|
contextFor: context,
|
|
localDate: date,
|
|
refreshReads: refreshReads,
|
|
);
|
|
}
|
|
|
|
/// Creates an application operation context for the supplied [operationId].
|
|
ApplicationOperationContext context(String operationId) {
|
|
return ApplicationOperationContext.start(
|
|
operationId: operationId,
|
|
ownerTimeZone: OwnerTimeZoneContext(
|
|
ownerId: ownerId,
|
|
timeZoneId: timeZoneId,
|
|
),
|
|
clock: FixedClock(readAt),
|
|
idGenerator: SequentialIdGenerator(prefix: operationId),
|
|
);
|
|
}
|
|
|
|
/// Formats a civil date as a long month/day/year label.
|
|
static String formatDateLabel(CivilDate date) {
|
|
const months = [
|
|
'January',
|
|
'February',
|
|
'March',
|
|
'April',
|
|
'May',
|
|
'June',
|
|
'July',
|
|
'August',
|
|
'September',
|
|
'October',
|
|
'November',
|
|
'December',
|
|
];
|
|
return '${months[date.month - 1]} ${date.day}, ${date.year}';
|
|
}
|
|
|
|
static List<Task> _todaySeedTasks(CivilDate date, DateTime createdAt) {
|
|
return [
|
|
_task(
|
|
id: 'clean-coffee-maker',
|
|
title: 'Clean coffee maker',
|
|
type: TaskType.flexible,
|
|
status: TaskStatus.planned,
|
|
date: date,
|
|
startHour: 16,
|
|
startMinute: 15,
|
|
endHour: 16,
|
|
endMinute: 30,
|
|
reward: RewardLevel.medium,
|
|
difficulty: DifficultyLevel.easy,
|
|
createdAt: createdAt,
|
|
),
|
|
_task(
|
|
id: 'cleaned-kitchen-counter-early',
|
|
title: 'Cleaned kitchen counter',
|
|
type: TaskType.surprise,
|
|
status: TaskStatus.completed,
|
|
date: date,
|
|
startHour: 16,
|
|
startMinute: 45,
|
|
endHour: 17,
|
|
endMinute: 0,
|
|
reward: RewardLevel.medium,
|
|
difficulty: DifficultyLevel.medium,
|
|
completedAt: _instant(date, 16, 20),
|
|
createdAt: createdAt,
|
|
),
|
|
_task(
|
|
id: 'pay-bill',
|
|
title: 'Pay bill',
|
|
type: TaskType.critical,
|
|
status: TaskStatus.planned,
|
|
date: date,
|
|
startHour: 18,
|
|
startMinute: 0,
|
|
endHour: 18,
|
|
endMinute: 10,
|
|
reward: RewardLevel.low,
|
|
difficulty: DifficultyLevel.medium,
|
|
createdAt: createdAt,
|
|
),
|
|
_task(
|
|
id: 'doctor-appointment',
|
|
title: 'Doctor appointment',
|
|
type: TaskType.inflexible,
|
|
status: TaskStatus.planned,
|
|
date: date,
|
|
startHour: 18,
|
|
startMinute: 30,
|
|
endHour: 19,
|
|
endMinute: 15,
|
|
reward: RewardLevel.medium,
|
|
difficulty: DifficultyLevel.hard,
|
|
createdAt: createdAt,
|
|
),
|
|
_task(
|
|
id: 'free-slot',
|
|
title: 'Free Slot',
|
|
type: TaskType.freeSlot,
|
|
status: TaskStatus.planned,
|
|
date: date,
|
|
startHour: 19,
|
|
startMinute: 15,
|
|
endHour: 20,
|
|
endMinute: 15,
|
|
reward: RewardLevel.notSet,
|
|
difficulty: DifficultyLevel.notSet,
|
|
createdAt: createdAt,
|
|
),
|
|
_task(
|
|
id: 'cleaned-kitchen-counter-late',
|
|
title: 'Cleaned kitchen counter',
|
|
type: TaskType.surprise,
|
|
status: TaskStatus.completed,
|
|
date: date,
|
|
startHour: 20,
|
|
startMinute: 15,
|
|
endHour: 20,
|
|
endMinute: 30,
|
|
reward: RewardLevel.medium,
|
|
difficulty: DifficultyLevel.medium,
|
|
completedAt: _instant(date, 20, 20),
|
|
createdAt: createdAt,
|
|
),
|
|
];
|
|
}
|
|
|
|
static Task _task({
|
|
required String id,
|
|
required String title,
|
|
required TaskType type,
|
|
required TaskStatus status,
|
|
required CivilDate date,
|
|
required int startHour,
|
|
required int startMinute,
|
|
required int endHour,
|
|
required int endMinute,
|
|
required RewardLevel reward,
|
|
required DifficultyLevel difficulty,
|
|
required DateTime createdAt,
|
|
DateTime? completedAt,
|
|
}) {
|
|
final start = _instant(date, startHour, startMinute);
|
|
final end = _instant(date, endHour, endMinute);
|
|
return Task(
|
|
id: id,
|
|
title: title,
|
|
projectId: projectId,
|
|
type: type,
|
|
status: status,
|
|
priority: PriorityLevel.medium,
|
|
reward: reward,
|
|
difficulty: difficulty,
|
|
durationMinutes: end.difference(start).inMinutes,
|
|
scheduledStart: start,
|
|
scheduledEnd: end,
|
|
actualStart: status == TaskStatus.completed ? start : null,
|
|
actualEnd: status == TaskStatus.completed ? end : null,
|
|
completedAt: completedAt,
|
|
createdAt: createdAt,
|
|
updatedAt: completedAt ?? createdAt,
|
|
);
|
|
}
|
|
|
|
static DateTime _instant(CivilDate date, int hour, [int minute = 0]) {
|
|
return DateTime.utc(date.year, date.month, date.day, hour, minute);
|
|
}
|
|
}
|