/// Defines Today Screen Models models for the FocusFlow Flutter UI. library; import 'package:scheduler_core/scheduler_core.dart'; /// Presentation model for the next-required-task banner. class RequiredBannerModel { /// Creates a required banner model. const RequiredBannerModel({ required this.timelineCardId, required this.title, required this.timeText, }); /// Timeline card id linked to the highlighted required task. final String timelineCardId; /// Title of the next required task. final String title; /// Display-ready time for the next required task. final String timeText; } /// Visible timeline range expressed as minutes since midnight. class TimelineRangeModel { /// Creates a timeline range model. const TimelineRangeModel({ required this.startMinutes, required this.endMinutes, }); /// First visible minute from midnight. final int startMinutes; /// Last visible minute from midnight. final int endMinutes; } /// Visual category used to style timeline cards. enum TaskVisualKind { /// Flexible planned work. flexible, /// Critical required task. required, /// Inflexible appointment-style task. appointment, /// Intentional free time. freeSlot, /// Completed surprise task. completedSurprise, } /// Presentation model for the compact Today screen. class TodayScreenData { /// Creates Today screen data. const TodayScreenData({ required this.dateLabel, required this.timelineRange, required this.cards, this.requiredBanner, }); /// Creates an empty Today screen model for [dateLabel]. factory TodayScreenData.empty({required String dateLabel}) { return TodayScreenData( dateLabel: dateLabel, timelineRange: defaultRange, cards: const [], ); } /// Maps scheduler core [TodayState] into UI presentation data. factory TodayScreenData.fromTodayState(TodayState state) { final cards = [ for (final item in state.timelineItems) TimelineCardModel.fromItem(item), ]; final nextRequired = state.nextRequiredItem; return TodayScreenData( dateLabel: _dateLabel(state.date), timelineRange: defaultRange, requiredBanner: nextRequired == null ? null : RequiredBannerModel( timelineCardId: nextRequired.id, title: nextRequired.item.displayTitle, timeText: _formatTime(nextRequired.start), ), cards: List.unmodifiable(cards), ); } /// Default visible range for the compact timeline. static const defaultRange = TimelineRangeModel( startMinutes: 0, endMinutes: 24 * 60, ); /// Display-ready date label. final String dateLabel; /// Optional next-required-task banner model. final RequiredBannerModel? requiredBanner; /// Visible timeline range. final TimelineRangeModel timelineRange; /// Timeline cards to render. final List cards; } /// Presentation model for a single compact timeline card. class TimelineCardModel { /// Creates a timeline card model. const TimelineCardModel({ required this.id, required this.title, required this.typeLabel, required this.subtitle, required this.timeText, required this.startMinutes, required this.endMinutes, required this.taskType, required this.visualKind, required this.rewardIconToken, required this.difficultyIconToken, required this.isCompleted, required this.isSelectable, required this.showsQuickActions, this.completedTimeText, this.durationMinutes, }); /// Maps a scheduler timeline item into card presentation data. factory TimelineCardModel.fromItem(TodayTimelineItem item) { final start = item.start; final end = item.end; final visualKind = _visualKindFor(item); final isCompleted = item.taskStatus == TaskStatus.completed; final title = item.item.displayTitle; final duration = item.item.durationMinutes; final timeText = start == null || end == null ? duration == null ? '' : '$duration min' : '${_formatTime(start)} - ${_formatTime(end)}'; final completedAt = item.item.completedAt ?? item.item.end ?? item.end; return TimelineCardModel( id: item.id, title: title, typeLabel: _typeLabelFor(item.taskType, visualKind), subtitle: _subtitleFor(item, visualKind, timeText), timeText: timeText, startMinutes: _minutesSinceMidnight(start), endMinutes: _minutesSinceMidnight(end), taskType: item.taskType, visualKind: visualKind, rewardIconToken: item.item.rewardIconToken, difficultyIconToken: item.item.difficultyIconToken, durationMinutes: duration, completedTimeText: isCompleted ? _formatTime(completedAt) : null, isCompleted: isCompleted, isSelectable: true, showsQuickActions: !isCompleted && visualKind != TaskVisualKind.freeSlot, ); } /// Stable card identifier. final String id; /// Primary task title shown on the card. final String title; /// Short task type label. final String typeLabel; /// Secondary line shown under the title. final String subtitle; /// Display-ready time or duration text. final String timeText; /// Display-ready completion time, if the task has been completed. final String? completedTimeText; /// Card start position in minutes since midnight. final int startMinutes; /// Card end position in minutes since midnight. final int endMinutes; /// Optional task duration in minutes. final int? durationMinutes; /// Source task scheduling behavior type. final TaskType taskType; /// Visual category used for styling. final TaskVisualKind visualKind; /// Reward icon token from the scheduler core. final TimelineRewardIconToken rewardIconToken; /// Difficulty icon token from the scheduler core. final TimelineDifficultyIconToken difficultyIconToken; /// Whether the card represents completed work. final bool isCompleted; /// Whether tapping the card should open its selection modal. final bool isSelectable; /// Whether the card should show compact quick-action controls. final bool showsQuickActions; /// Whether the left status control can toggle this task's completion. bool get canToggleCompletion { return switch (taskType) { TaskType.flexible || TaskType.critical || TaskType.inflexible => true, TaskType.locked || TaskType.surprise || TaskType.freeSlot => false, }; } /// Whether the left status control can complete this task. bool get canComplete => canToggleCompletion && !isCompleted; /// Whether the left status control can return this task to planned. bool get canUncomplete => canToggleCompletion && isCompleted; /// Accessibility and modal label for the reward icon. String get rewardLabel { return switch (rewardIconToken) { TimelineRewardIconToken.notSet => 'Reward not set', TimelineRewardIconToken.veryLow => 'Reward level 1', TimelineRewardIconToken.low => 'Reward level 2', TimelineRewardIconToken.medium => 'Reward level 3', TimelineRewardIconToken.high => 'Reward level 4', TimelineRewardIconToken.veryHigh => 'Reward level 5', }; } /// Accessibility and modal label for the effort icon. String get effortLabel { return switch (difficultyIconToken) { TimelineDifficultyIconToken.notSet => 'Effort not set', TimelineDifficultyIconToken.veryEasy => 'Very easy effort', TimelineDifficultyIconToken.easy => 'Easy effort', TimelineDifficultyIconToken.medium => 'Medium effort', TimelineDifficultyIconToken.hard => 'Hard effort', TimelineDifficultyIconToken.veryHard => 'Very hard effort', }; } } TaskVisualKind _visualKindFor(TodayTimelineItem item) { if (item.taskStatus == TaskStatus.completed || (item.taskType == TaskType.surprise && item.taskStatus == TaskStatus.completed)) { return TaskVisualKind.completedSurprise; } return switch (item.taskType) { TaskType.flexible => TaskVisualKind.flexible, TaskType.critical => TaskVisualKind.required, TaskType.inflexible => TaskVisualKind.appointment, TaskType.freeSlot => TaskVisualKind.freeSlot, TaskType.surprise => TaskVisualKind.completedSurprise, TaskType.locked => TaskVisualKind.completedSurprise, }; } String _typeLabelFor(TaskType taskType, TaskVisualKind visualKind) { if (visualKind == TaskVisualKind.completedSurprise) { return 'Completed'; } return switch (taskType) { TaskType.flexible => 'Flexible', TaskType.critical => 'Required', TaskType.inflexible => 'Required', TaskType.freeSlot => 'Free Slot', TaskType.surprise => 'Surprise task', TaskType.locked => 'Locked', }; } String _subtitleFor( TodayTimelineItem item, TaskVisualKind visualKind, String timeText, ) { if (visualKind == TaskVisualKind.freeSlot) { return 'Intentional rest'; } if (item.taskStatus == TaskStatus.completed || visualKind == TaskVisualKind.completedSurprise) { return 'Completed at: ' '${_formatTime(item.item.completedAt ?? item.item.end ?? item.end)}'; } if (item.taskType == TaskType.flexible && item.item.durationMinutes != null) { return '${item.item.durationMinutes} min'; } return timeText; } String _dateLabel(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}'; } int _minutesSinceMidnight(DateTime? value) { if (value == null) { return 0; } return value.hour * 60 + value.minute; } String _formatTime(DateTime? value) { if (value == null) { return ''; } final period = value.hour >= 12 ? 'PM' : 'AM'; final rawHour = value.hour % 12; final hour = rawHour == 0 ? 12 : rawHour; final minute = value.minute.toString().padLeft(2, '0'); return '$hour:$minute $period'; }