Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/0m1n3m/contacts-db/llms.txt

Use this file to discover all available pages before exploring further.

Contacts DB tracks how long tasks spend in key workflow stages without requiring any manual timers. Every time a task’s status changes, the TimeTrackingService inspects the previous status, calculates how many working seconds elapsed since the task entered that status, and adds the result to the appropriate time bucket on the task record. This gives teams accurate cycle-time data grounded in real working hours rather than calendar time.

Time metrics

Three independent time metrics are tracked per task, each stored as an integer number of seconds:
MetricDatabase fieldAccumulates when
Lead Timelead_timeTask is created → task reaches done (total elapsed working time end-to-end)
Dev Timedev_timeTask is in in_progress status
Review Timereview_timeTask is in in_review status
lead_time, dev_time, and review_time are stored as unsignedBigInteger and default to 0. backward_transitions is stored as unsignedInteger and also defaults to 0. All four fields were added by the 2026_07_15_085835_add_time_tracking_to_tasks_table migration.

Working time calculation

Time is measured in working seconds only — weekends are excluded entirely, and only weekday time counts. The WorkingTimeHelper class provides all the conversion utilities used across the application:
// Calculate exact working seconds between two Carbon timestamps.
// Weekends (Saturday = 6, Sunday = 0) are skipped completely.
$seconds = WorkingTimeHelper::getWorkingSecondsPrecise($startDate, $endDate);
getWorkingSecondsPrecise counts partial days accurately. If a task entered in_progress at 16:00 on a Friday and the status changed at 10:00 the following Monday, only 2 hours (Friday 16:00–18:00… — actually the helper counts to end of day then resumes Monday) of weekend-free working time would accumulate, not the full weekend gap. Periods outside of working hours are not capped by a hard 09:00–18:00 window in this implementation; the helper counts all weekday time between the two timestamps, treating each weekday as a full working day.

How accumulation works

Time accumulation is driven by TimeTrackingService::recordStateChange, which is called automatically whenever a task’s status field changes. The sequence on every status transition is:
1

Log the transition

A status_changed entry is written to AuditLog with before.status and after.status. This audit history is later used for recalculation.
2

Detect backward transitions

The service compares the numeric position of the old and new statuses using a fixed order (created=0, accepted=1, in_progress=2, in_review=3, done=4). If the new position is lower than the old one (for example, moving from in_review back to in_progress), the backward_transitions counter on the task is incremented by one.
3

Accumulate elapsed time for the previous status

Working seconds are calculated from entered_current_status_at (when the task entered its previous status) to now():
  • If the previous status was in_progress → the elapsed seconds are added to dev_time
  • If the previous status was in_review → the elapsed seconds are added to review_time
  • Other statuses (created, accepted) do not accumulate into any bucket
4

Set lead time on completion

If the new status is done, lead_time is computed as the working seconds from task.created_at to the current moment, and completed_at is stamped with the current timestamp.
5

Update the status timestamp

entered_current_status_at is set to now() so the next transition has an accurate starting point.
All in-memory mutations made by recordStateChange (backward_transitions, dev_time, review_time, lead_time, completed_at, entered_current_status_at) are applied directly to the $task model instance without calling save() inside the service. Laravel’s model updating event persists them automatically when the status change is saved by the calling action.

Backward transitions

The backward_transitions field counts how many times a task has moved to an earlier stage in the workflow. A value of 0 means the task moved linearly through the pipeline; a high value may indicate unclear requirements, blocked work, or review quality issues. Use it as a simple process-health indicator when reviewing completed tasks.

Completed at

completed_at is set the first time a task reaches done. If a task is later moved backward (e.g., back to in_review) and then completes again, completed_at will be overwritten with the new completion timestamp. Use the audit log to reconstruct the full history.

Accessing time data on a task

The Task model exposes convenience methods that delegate to WorkingTimeHelper so you never need to handle raw second values directly:
$task->getLeadTimeInHours();   // float — total lead time in hours
$task->getLeadTimeInDays();    // float — total lead time in 8-hour days
$task->getLeadTimeFormatted(); // string — e.g. "3d 2h 15m"

Recalculating times from history

If a task’s time data becomes inconsistent — for example after a direct database edit or a data migration — you can recompute all three metrics from scratch using the audit log:
use App\Services\TimeTrackingService;

$service = app(TimeTrackingService::class);
$service->recalculateAllTimesForTask($task);
The recalculation process:
  1. Wraps everything in Task::withoutEvents() so the observer does not fire during the rebuild
  2. Resets lead_time, dev_time, review_time, backward_transitions, and completed_at to zero / null in memory
  3. Replays every status_changed audit log entry in chronological order, accumulating dev time, review time, and backward transition counts using the same WorkingTimeHelper logic
  4. Calculates lead_time from the first to last audit log entry if the final status is done
  5. Calls $task->save() once to persist the fully recalculated state in a single query
Run recalculateAllTimesForTask any time you suspect the accumulated time figures are out of sync with reality — for instance after manually correcting a task’s status in the database, or after importing historical data. Because it replays the audit log rather than relying on the live task record, it will produce consistent results regardless of the current field values.

Build docs developers (and LLMs) love