// UI/platform-independent reminder policy. // // This file decides whether the core would deliver, suppress, defer, or require // acknowledgement for a reminder at a specific instant. It does not schedule OS // notifications, run background timers, or move tasks. import 'models.dart'; import 'occupancy_policy.dart'; /// High-level reminder directive for application/platform layers. enum ReminderDirectiveAction { deliver, suppress, defer, requireAcknowledgement, } /// Stable reason codes for reminder directives. enum ReminderDirectiveReason { deliverNormal, requireRequiredAcknowledgement, deferUntilTaskWindow, deferRequiredDuringProtectedRest, suppressSilentProfile, suppressProtectedRest, suppressNoScheduledWindow, suppressUnsupportedTaskType, suppressInactiveStatus, suppressHiddenLockedTime, suppressFreeSlotRecord, } /// Result of resolving the effective reminder profile for one task. class EffectiveReminderProfile { const EffectiveReminderProfile({ required this.profile, required this.source, }); /// Resolved reminder profile. final ReminderProfile profile; /// Where [profile] came from. final EffectiveReminderProfileSource source; } /// Source for an effective reminder profile. enum EffectiveReminderProfileSource { taskOverride, projectDefault, fallback, } /// UI/platform-independent directive for one task reminder check. class ReminderDirective { const ReminderDirective({ required this.taskId, required this.action, required this.reason, required this.effectiveProfile, required this.effectiveProfileSource, required this.evaluatedAt, this.scheduledStart, this.scheduledEnd, this.protectedFreeSlotTaskId, this.nextEligibleAt, }); /// Task evaluated for reminder behavior. final String taskId; /// What an application layer may do. final ReminderDirectiveAction action; /// Stable reason code for tests, telemetry, and later UI copy. final ReminderDirectiveReason reason; /// Effective reminder profile used for the decision. final ReminderProfile effectiveProfile; /// Source of [effectiveProfile]. final EffectiveReminderProfileSource effectiveProfileSource; /// Instant used to evaluate the directive. final DateTime evaluatedAt; /// Task scheduled start, when present. final DateTime? scheduledStart; /// Task scheduled end, when present. final DateTime? scheduledEnd; /// Active protected Free Slot that affected the decision, when any. final String? protectedFreeSlotTaskId; /// Next known time the application may re-check, when deterministic. final DateTime? nextEligibleAt; } /// Resolves reminder profiles and reminder directives. class ReminderPolicyService { const ReminderPolicyService({ this.fallbackProfile = ReminderProfile.gentle, this.occupancyPolicy = const OccupancyPolicy(), }); /// Fallback used when no task override or project profile is available. final ReminderProfile fallbackProfile; /// Occupancy classifier used to detect active protected Free Slots. final OccupancyPolicy occupancyPolicy; /// Resolve effective profile without considering learned suggestions. EffectiveReminderProfile resolveEffectiveProfile({ required Task task, ProjectProfile? project, }) { final override = task.reminderOverride; if (override != null) { return EffectiveReminderProfile( profile: override, source: EffectiveReminderProfileSource.taskOverride, ); } if (project != null) { return EffectiveReminderProfile( profile: project.defaultReminderProfile, source: EffectiveReminderProfileSource.projectDefault, ); } return EffectiveReminderProfile( profile: fallbackProfile, source: EffectiveReminderProfileSource.fallback, ); } /// Resolve one reminder directive at [now]. ReminderDirective directiveForTask({ required Task task, required DateTime now, ProjectProfile? project, Iterable contextTasks = const [], }) { final effective = resolveEffectiveProfile(task: task, project: project); final activeFreeSlot = _activeProtectedFreeSlot( now: now, contextTasks: contextTasks, ); return _directiveForTask( task: task, now: now, effective: effective, activeFreeSlot: activeFreeSlot, ); } ReminderDirective _directiveForTask({ required Task task, required DateTime now, required EffectiveReminderProfile effective, required OccupancyEntry? activeFreeSlot, }) { final start = task.scheduledStart; final end = task.scheduledEnd; ReminderDirective build({ required ReminderDirectiveAction action, required ReminderDirectiveReason reason, DateTime? nextEligibleAt, }) { return ReminderDirective( taskId: task.id, action: action, reason: reason, effectiveProfile: effective.profile, effectiveProfileSource: effective.source, evaluatedAt: now, scheduledStart: start, scheduledEnd: end, protectedFreeSlotTaskId: activeFreeSlot?.taskId, nextEligibleAt: nextEligibleAt, ); } if (effective.profile == ReminderProfile.silent) { return build( action: ReminderDirectiveAction.suppress, reason: ReminderDirectiveReason.suppressSilentProfile, ); } if (task.type == TaskType.locked) { return build( action: ReminderDirectiveAction.suppress, reason: ReminderDirectiveReason.suppressHiddenLockedTime, ); } if (task.type == TaskType.freeSlot) { return build( action: ReminderDirectiveAction.suppress, reason: ReminderDirectiveReason.suppressFreeSlotRecord, ); } if (!_isReminderEligibleStatus(task.status)) { return build( action: ReminderDirectiveAction.suppress, reason: ReminderDirectiveReason.suppressInactiveStatus, ); } if (!_isReminderEligibleType(task.type)) { return build( action: ReminderDirectiveAction.suppress, reason: ReminderDirectiveReason.suppressUnsupportedTaskType, ); } if (start == null || end == null || !start.isBefore(end)) { return build( action: ReminderDirectiveAction.suppress, reason: ReminderDirectiveReason.suppressNoScheduledWindow, ); } if (now.isBefore(start)) { return build( action: ReminderDirectiveAction.defer, reason: ReminderDirectiveReason.deferUntilTaskWindow, nextEligibleAt: start, ); } final protectedRestActive = activeFreeSlot != null; if (protectedRestActive && task.isFlexible) { return build( action: ReminderDirectiveAction.suppress, reason: ReminderDirectiveReason.suppressProtectedRest, nextEligibleAt: activeFreeSlot.interval?.end, ); } if (protectedRestActive && task.isRequiredVisible) { return _requiredDuringProtectedRest( task: task, effective: effective, build: build, freeSlotEnd: activeFreeSlot.interval?.end, ); } if (task.type == TaskType.critical && effective.profile == ReminderProfile.strict) { return build( action: ReminderDirectiveAction.requireAcknowledgement, reason: ReminderDirectiveReason.requireRequiredAcknowledgement, ); } return build( action: ReminderDirectiveAction.deliver, reason: ReminderDirectiveReason.deliverNormal, ); } ReminderDirective _requiredDuringProtectedRest({ required Task task, required EffectiveReminderProfile effective, required ReminderDirective Function({ required ReminderDirectiveAction action, required ReminderDirectiveReason reason, DateTime? nextEligibleAt, }) build, required DateTime? freeSlotEnd, }) { if (task.type == TaskType.critical && effective.profile == ReminderProfile.strict) { return build( action: ReminderDirectiveAction.requireAcknowledgement, reason: ReminderDirectiveReason.requireRequiredAcknowledgement, ); } if (task.type == TaskType.inflexible && effective.profile == ReminderProfile.gentle) { return build( action: ReminderDirectiveAction.defer, reason: ReminderDirectiveReason.deferRequiredDuringProtectedRest, nextEligibleAt: freeSlotEnd, ); } return build( action: ReminderDirectiveAction.deliver, reason: ReminderDirectiveReason.deliverNormal, ); } OccupancyEntry? _activeProtectedFreeSlot({ required DateTime now, required Iterable contextTasks, }) { for (final entry in occupancyPolicy.classify(tasks: contextTasks)) { final interval = entry.interval; if (entry.category != OccupancyCategory.protectedFreeSlot || interval == null) { continue; } final startsAtOrBeforeNow = interval.start.isBefore(now) || interval.start.isAtSameMomentAs(now); final endsAfterNow = interval.end.isAfter(now); if (startsAtOrBeforeNow && endsAfterNow) { return entry; } } return null; } } bool _isReminderEligibleStatus(TaskStatus status) { return status == TaskStatus.planned || status == TaskStatus.active; } bool _isReminderEligibleType(TaskType type) { return type == TaskType.flexible || type == TaskType.critical || type == TaskType.inflexible; }