b307f66f2c
Projects: Brick-backed model with list/detail screens and nav entry. Work Logs: per-contributor time ledger, assignee running-clock + helper logged minutes with Hours/Minutes input; pause-after-save and helper-deduct prompts. Comments: threaded comments with file attachments and Quill composer. Detail screen: tabbed layout (Work Log / Comments / Activity), editable title + description via edit dialog, Quill-rendered descriptions with legacy plain-text fallback. Create dialog: fixed-width (480px max), Quill description editor. Shared QuillFieldEditor + QuillReadOnly widgets extracted for reuse. RLS: hardened activity-log actor binding, immutable creator_id/ created_at triggers, SECURITY INVOKER visibility helper, append-only work-log and scoped comment policies. Brick migration for project_id FK on programmer_tasks. Co-Authored-By: claude-flow <ruv@ruv.net>
102 lines
3.8 KiB
Dart
102 lines
3.8 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], 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<DurationEvent> 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<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),
|
|
);
|