import 'package:timezone/timezone.dart' as tz; import 'package:tasq/models/programmer_task.model.dart'; import 'package:tasq/models/programmer_task_activity_log.model.dart'; import 'app_time.dart'; /// Get the Manila calendar date of an instant as DateTime.utc(y, m, d) DateTime manilaDay(DateTime instant) { final utc = instant.isUtc ? instant : instant.toUtc(); final manila = tz.TZDateTime.from(utc, tz.local); return DateTime.utc(manila.year, manila.month, manila.day); } /// Get the Manila midnight of a day key as a TZDateTime DateTime manilaDayStart(DateTime dayKey) { return AppTime.fromComponents( year: dayKey.year, month: dayKey.month, day: dayKey.day, hour: 0, minute: 0, ); } /// Parse a day key string (e.g., '2026-09-27') to DateTime.utc DateTime parseDayKey(String yyyyMmDd) { final parts = yyyyMmDd.split('-'); return DateTime.utc(int.parse(parts[0]), int.parse(parts[1]), int.parse(parts[2])); } /// Format a day key to string (e.g., DateTime.utc(2026, 9, 27) -> '2026-09-27') String formatDayKey(DateTime dayKey) { final y = dayKey.year.toString(); final m = dayKey.month.toString().padLeft(2, '0'); final d = dayKey.day.toString().padLeft(2, '0'); return '$y-$m-$d'; } /// Represents a worked interval for an assignee typedef WorkedInterval = ({String assigneeId, DateTime start, DateTime end}); /// Calculate worked intervals from task and activity logs List workedIntervals({ required ProgrammerTask task, required List logs, required DateTime now, }) { final sorted = List.from(logs) ..sort((a, b) => a.createdAt.compareTo(b.createdAt)); final intervals = []; DateTime? segmentStart; String currentAssignee = _initialAssignee(task, sorted); // Check if there's a synthetic started event needed if (!sorted.any((l) => l.actionType == 'started') && task.startedAt != null) { // Inject synthetic started sorted.insert( 0, ProgrammerTaskActivityLog( id: 'synthetic-start', taskId: task.id, actionType: 'started', createdAt: task.startedAt!, ), ); } for (final log in sorted) { switch (log.actionType) { case 'started': case 'resumed': if (segmentStart == null) { segmentStart = log.createdAt; } break; case 'paused': case 'completed': case 'cancelled': if (segmentStart != null) { intervals.add(( assigneeId: currentAssignee, start: segmentStart, end: log.createdAt, )); segmentStart = null; } break; case 'reassigned': final wasRunning = segmentStart != null; if (segmentStart != null) { // Close segment for old assignee intervals.add(( assigneeId: currentAssignee, start: segmentStart, end: log.createdAt, )); segmentStart = null; } // Update assignee and reopen segment only if it was running if (log.meta?['assignee_id'] != null) { currentAssignee = log.meta!['assignee_id'] as String; if (wasRunning) { segmentStart = log.createdAt; } } break; } } // Close any remaining open segment if (segmentStart != null) { final endTime = task.status == ProgrammerTaskStatus.inProgress ? now : (task.completedAt ?? task.cancelledAt ?? now); intervals.add(( assigneeId: currentAssignee, start: segmentStart, end: endTime, )); } return intervals; } /// Calculate daily seconds by assignee Map> dailySecondsByAssignee({ required ProgrammerTask task, required List logs, required DateTime now, }) { final intervals = workedIntervals(task: task, logs: logs, now: now); final result = >{}; // Process intervals and split at Manila midnights for (final interval in intervals) { var current = interval.start; final end = interval.end; while (current.isBefore(end)) { final currentDay = manilaDay(current); final dayStart = manilaDayStart(currentDay); final nextDayStart = dayStart.add(Duration(days: 1)); final segmentEnd = end.isBefore(nextDayStart) ? end : nextDayStart; final seconds = segmentEnd.difference(current).inSeconds; if (!result.containsKey(interval.assigneeId)) { result[interval.assigneeId] = {}; } result[interval.assigneeId]![currentDay] = (result[interval.assigneeId]![currentDay] ?? 0) + seconds; current = segmentEnd; } } // Apply adjustments for (final log in logs) { if (log.actionType == 'adjustment' && log.meta?['seconds'] != null) { final adjustSeconds = (log.meta!['seconds'] as num).toInt(); final adjustDay = manilaDay(log.createdAt); // Find the assignee at this time final assigneeAtTime = _assigneeAtTime(task, logs, log.createdAt); if (assigneeAtTime != null) { if (!result.containsKey(assigneeAtTime)) { result[assigneeAtTime] = {}; } final current = result[assigneeAtTime]![adjustDay] ?? 0; result[assigneeAtTime]![adjustDay] = (current - adjustSeconds).clamp(0, 999999999); } } } return result; } /// Find the initial assignee based on task and logs String _initialAssignee( ProgrammerTask task, List logs, ) { // First: check for 'assigned' event for (final log in logs) { if (log.actionType == 'assigned' && log.meta?['assignee_id'] != null) { return log.meta!['assignee_id'] as String; } } // Second: if any 'reassigned' event exists, use task.creatorId if (logs.any((l) => l.actionType == 'reassigned') && task.creatorId != null) { return task.creatorId!; } // Third: use task.assigneeId return task.assigneeId ?? 'unknown'; } /// Find the assignee at a specific time String? _assigneeAtTime( ProgrammerTask task, List logs, DateTime time, ) { final sorted = List.from(logs) ..sort((a, b) => a.createdAt.compareTo(b.createdAt)); var currentAssignee = _initialAssignee(task, sorted); for (final log in sorted) { if (log.createdAt.isAfter(time)) break; if (log.actionType == 'reassigned' && log.meta?['assignee_id'] != null) { currentAssignee = log.meta!['assignee_id'] as String; } } return currentAssignee; }