627645056b
Separate feature for the `programmer` role to record dev work by category, distinct from the IT help-desk `tasks` feature. Modeled on the IT Service Requests pattern (Brick offline-first model -> Riverpod stream/controller -> list/detail screens); seed of a future Project Management workflow. - programmer_tasks + programmer_task_activity_logs (category enum, RLS, realtime) via supabase migration - single assignee, self-logged or lead-assigned; assign/reassign logged - pause-on-switch: starting a task while another runs prompts to pause the running one, keeping one active task per user for accurate time attribution - worked-duration math shared with help-desk tasks via a generic core in task_duration.dart (existing callers unchanged) - list distinguishes running vs paused with a per-task Resume shortcut - accessible dialog labels; route /programmer-tasks + nav gated to admin/programmer - unit/widget tests: controller lifecycle, duration math, pause-switch dialog Co-Authored-By: claude-flow <ruv@ruv.net>
99 lines
3.6 KiB
Dart
99 lines
3.6 KiB
Dart
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<DurationEvent> 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].
|
|
///
|
|
/// [events] must be in DESCENDING order (newest first) — the same order
|
|
/// returned by activity-log queries with `ascending: false`.
|
|
Duration computeEffectiveDuration({
|
|
required DateTime? fallbackStartedAt,
|
|
required DateTime endAt,
|
|
required List<DurationEvent> events,
|
|
}) {
|
|
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;
|
|
return total.isNegative ? Duration.zero : total;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// TaskActivityLog wrappers (unchanged public API — existing call sites intact)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
List<DurationEvent> _toEvents(List<TaskActivityLog> 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<TaskActivityLog> 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<TaskActivityLog> logs,
|
|
}) => computeEffectiveDuration(
|
|
fallbackStartedAt: fallbackStartedAt,
|
|
endAt: endAt,
|
|
events: _toEvents(logs),
|
|
);
|