import '../models/task_activity_log.model.dart'; /// A minimal activity event shape: just the fields the worked-duration math /// needs. Both [TaskActivityLog] and ProgrammerTaskActivityLog map onto this, /// so the pause-aware duration logic is shared across task modules. typedef DurationEvent = ({DateTime createdAt, String actionType}); // --------------------------------------------------------------------------- // Generic core (works on any DurationEvent list) // --------------------------------------------------------------------------- /// Finds the canonical execution start time from [events]. /// /// [events] is expected in DESCENDING order (newest first). The reversed /// iteration walks chronologically forward and stops at the earliest 'started' /// event, falling back to [fallbackStartedAt]. DateTime? resolveExecutionStart( List events, DateTime? fallbackStartedAt, ) { DateTime? started; for (final entry in events.reversed) { if (entry.actionType == 'started') { started = entry.createdAt; break; } } return started ?? fallbackStartedAt; } /// Computes the effective worked duration, subtracting all paused intervals /// between the resolved start and [endAt], then subtracting [adjustment] /// (e.g. time reassigned to another task when the user helped elsewhere). /// /// [events] must be in DESCENDING order (newest first) — the same order /// returned by activity-log queries with `ascending: false`. The result never /// goes below zero. Duration computeEffectiveDuration({ required DateTime? fallbackStartedAt, required DateTime endAt, required List events, Duration adjustment = Duration.zero, }) { final start = resolveExecutionStart(events, fallbackStartedAt); if (start == null || !endAt.isAfter(start)) return Duration.zero; Duration pausedTotal = Duration.zero; DateTime? pausedSince; // events.reversed → ascending (chronological) order within [start, endAt] final relevant = events.reversed.where((e) { if (e.createdAt.isBefore(start)) return false; if (e.createdAt.isAfter(endAt)) return false; return e.actionType == 'paused' || e.actionType == 'resumed'; }); for (final event in relevant) { if (event.actionType == 'paused') { pausedSince ??= event.createdAt; } else if (event.actionType == 'resumed' && pausedSince != null) { if (event.createdAt.isAfter(pausedSince)) { pausedTotal += event.createdAt.difference(pausedSince); } pausedSince = null; } } // Still paused at endAt — count the open pause interval. if (pausedSince != null && endAt.isAfter(pausedSince)) { pausedTotal += endAt.difference(pausedSince); } final total = endAt.difference(start) - pausedTotal - adjustment; return total.isNegative ? Duration.zero : total; } // --------------------------------------------------------------------------- // TaskActivityLog wrappers (unchanged public API — existing call sites intact) // --------------------------------------------------------------------------- List _toEvents(List logs) => logs.map((l) => (createdAt: l.createdAt, actionType: l.actionType)).toList(); /// Finds the canonical execution start time for a task. See /// [resolveExecutionStart]. DateTime? resolveTaskExecutionStart( List logs, DateTime? fallbackStartedAt, ) => resolveExecutionStart(_toEvents(logs), fallbackStartedAt); /// Computes the effective worked duration for a task. See /// [computeEffectiveDuration]. Duration computeEffectiveTaskDuration({ required DateTime? fallbackStartedAt, required DateTime endAt, required List logs, }) => computeEffectiveDuration( fallbackStartedAt: fallbackStartedAt, endAt: endAt, events: _toEvents(logs), );