import 'package:scheduler_core/scheduler_core.dart'; class RequiredBannerModel { const RequiredBannerModel({required this.title, required this.timeText}); final String title; final String timeText; } class TimelineRangeModel { const TimelineRangeModel({ required this.startMinutes, required this.endMinutes, }); final int startMinutes; final int endMinutes; } enum TaskVisualKind { flexible, required, appointment, freeSlot, completedSurprise, } class TodayScreenData { const TodayScreenData({ required this.dateLabel, required this.timelineRange, required this.cards, this.requiredBanner, }); factory TodayScreenData.empty({required String dateLabel}) { return TodayScreenData( dateLabel: dateLabel, timelineRange: defaultRange, cards: const [], ); } 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( title: nextRequired.item.displayTitle, timeText: _formatTime(nextRequired.start), ), cards: List.unmodifiable(cards), ); } static const defaultRange = TimelineRangeModel( startMinutes: 16 * 60, endMinutes: 20 * 60 + 45, ); final String dateLabel; final RequiredBannerModel? requiredBanner; final TimelineRangeModel timelineRange; final List cards; } class TimelineCardModel { 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.visualKind, required this.rewardIconToken, required this.difficultyIconToken, required this.isCompleted, required this.isSelectable, required this.showsQuickActions, this.durationMinutes, }); 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)}'; return TimelineCardModel( id: item.id, title: title, typeLabel: _typeLabelFor(visualKind), subtitle: _subtitleFor(item, visualKind, timeText), timeText: timeText, startMinutes: _minutesSinceMidnight(start), endMinutes: _minutesSinceMidnight(end), visualKind: visualKind, rewardIconToken: item.item.rewardIconToken, difficultyIconToken: item.item.difficultyIconToken, durationMinutes: duration, isCompleted: isCompleted, isSelectable: true, showsQuickActions: !isCompleted && visualKind != TaskVisualKind.freeSlot, ); } final String id; final String title; final String typeLabel; final String subtitle; final String timeText; final int startMinutes; final int endMinutes; final int? durationMinutes; final TaskVisualKind visualKind; final TimelineRewardIconToken rewardIconToken; final TimelineDifficultyIconToken difficultyIconToken; final bool isCompleted; final bool isSelectable; final bool showsQuickActions; 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', }; } 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(TaskVisualKind visualKind) { return switch (visualKind) { TaskVisualKind.flexible => 'Flexible', TaskVisualKind.required => 'Required', TaskVisualKind.appointment => 'Required', TaskVisualKind.freeSlot => 'Free Slot', TaskVisualKind.completedSurprise => 'Surprise task', }; } String _subtitleFor( TodayTimelineItem item, TaskVisualKind visualKind, String timeText, ) { if (visualKind == TaskVisualKind.freeSlot) { return 'Intentional rest'; } if (visualKind == TaskVisualKind.completedSurprise) { final completedAt = _formatTime(item.item.end ?? item.end); return 'Surprise task - Completed at $completedAt'; } 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'; }