Add Programmer Tasks module (work-log + time tracking)
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>
This commit is contained in:
@@ -1,17 +1,25 @@
|
||||
import '../models/task_activity_log.model.dart';
|
||||
|
||||
/// Finds the canonical execution start time for a task.
|
||||
/// 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].
|
||||
///
|
||||
/// [logs] is expected in DESCENDING order (newest first), matching the
|
||||
/// `order('created_at', ascending: false)` used by Supabase queries.
|
||||
/// The reversed iteration therefore walks chronologically forward and stops
|
||||
/// at the earliest 'started' event.
|
||||
DateTime? resolveTaskExecutionStart(
|
||||
List<TaskActivityLog> logs,
|
||||
/// [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 logs.reversed) {
|
||||
for (final entry in events.reversed) {
|
||||
if (entry.actionType == 'started') {
|
||||
started = entry.createdAt;
|
||||
break;
|
||||
@@ -20,31 +28,30 @@ DateTime? resolveTaskExecutionStart(
|
||||
return started ?? fallbackStartedAt;
|
||||
}
|
||||
|
||||
/// Computes the effective worked duration for a task, subtracting all
|
||||
/// paused intervals between [fallbackStartedAt] (or the first 'started'
|
||||
/// log event) and [endAt].
|
||||
/// Computes the effective worked duration, subtracting all paused intervals
|
||||
/// between the resolved start and [endAt].
|
||||
///
|
||||
/// [logs] must be in DESCENDING order (newest first) — the same order
|
||||
/// returned by `task_activity_logs` queries with `ascending: false`.
|
||||
Duration computeEffectiveTaskDuration({
|
||||
/// [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<TaskActivityLog> logs,
|
||||
required List<DurationEvent> events,
|
||||
}) {
|
||||
final start = resolveTaskExecutionStart(logs, fallbackStartedAt);
|
||||
final start = resolveExecutionStart(events, fallbackStartedAt);
|
||||
if (start == null || !endAt.isAfter(start)) return Duration.zero;
|
||||
|
||||
Duration pausedTotal = Duration.zero;
|
||||
DateTime? pausedSince;
|
||||
|
||||
// logs.reversed → ascending (chronological) order within [start, endAt]
|
||||
final events = logs.reversed.where((e) {
|
||||
// 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 events) {
|
||||
for (final event in relevant) {
|
||||
if (event.actionType == 'paused') {
|
||||
pausedSince ??= event.createdAt;
|
||||
} else if (event.actionType == 'resumed' && pausedSince != null) {
|
||||
@@ -55,7 +62,7 @@ Duration computeEffectiveTaskDuration({
|
||||
}
|
||||
}
|
||||
|
||||
// Task is still paused at endAt — count the open pause interval.
|
||||
// Still paused at endAt — count the open pause interval.
|
||||
if (pausedSince != null && endAt.isAfter(pausedSince)) {
|
||||
pausedTotal += endAt.difference(pausedSince);
|
||||
}
|
||||
@@ -63,3 +70,29 @@ Duration computeEffectiveTaskDuration({
|
||||
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),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user