phases one of time logged now with log display

This commit is contained in:
Mark Gregory
2026-06-22 14:22:58 +01:00
parent bc213e0c6e
commit 0159a638f6
6 changed files with 195 additions and 175 deletions
+38 -67
View File
@@ -5,8 +5,8 @@ namespace Modules\Helpdesk\Livewire;
use Carbon\Carbon;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\QueryException;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Gate;
use Illuminate\Validation\ValidationException;
use Livewire\Component;
use Modules\Helpdesk\Models\Ticket;
@@ -25,7 +25,7 @@ class TicketTimer extends Component
public int $elapsedSeconds = 0;
/**
* Mount the ticket timer component.
* Mount the ticket timer and load any active database-backed timer.
*/
public function mount(Ticket $ticket): void
{
@@ -34,7 +34,7 @@ class TicketTimer extends Component
}
/**
* Start a user-specific timer for this ticket.
* Start a database-backed timer for the current authenticated user.
*
* @throws \Illuminate\Validation\ValidationException
*/
@@ -42,51 +42,47 @@ class TicketTimer extends Component
{
$user = $this->authenticatedUser();
Gate::authorize('update', $this->ticket);
if ($this->activeUserTimerQuery($user)->exists()) {
throw ValidationException::withMessages([
'timer' => 'You already have a running timer. End it before starting another one.',
]);
}
if ($this->activeAnyTicketTimerQuery($user)->exists()) {
throw ValidationException::withMessages([
'timer' => 'You already have a running timer on another ticket. End it before starting another one.',
'timer' => 'You already have a running timer.',
]);
}
$startedAt = Carbon::now();
TicketTimeLog::query()->create([
'ticket_id' => $this->ticket->getKey(),
'user_id' => $user->getAuthIdentifier(),
'minutes' => 0,
'seconds' => 0,
'description' => $this->normalisedDescription(),
'started_at' => $startedAt,
'ended_at' => null,
'logged_at' => $startedAt,
]);
try {
$timer = TicketTimeLog::query()->create([
'ticket_id' => $this->ticket->getKey(),
'user_id' => $user->getAuthIdentifier(),
'minutes' => 0,
'seconds' => 0,
'description' => $this->normalisedDescription(),
'started_at' => $startedAt,
'ended_at' => null,
'logged_at' => $startedAt,
]);
} catch (QueryException $exception) {
throw ValidationException::withMessages([
'timer' => 'You already have a running timer.',
]);
}
$this->activeTimer = $timer->fresh();
$this->elapsedSeconds = 0;
$this->description = '';
$this->message = 'Timer started.';
$this->refreshActiveTimer();
$this->dispatch('timer-started');
}
/**
* End the active user timer and add the elapsed time to the ticket.
* End the current user's active timer and persist seconds/minutes.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function endTimer(): void
{
$user = $this->authenticatedUser();
Gate::authorize('update', $this->ticket);
$timer = $this->activeUserTimerQuery($user)->first();
if (! $timer instanceof TicketTimeLog) {
@@ -108,7 +104,14 @@ class TicketTimer extends Component
'logged_at' => $endedAt,
])->save();
$this->updateTicketTimeSpentFromSeconds();
$totalSeconds = (int) TicketTimeLog::query()
->where('ticket_id', $this->ticket->getKey())
->whereNotNull('ended_at')
->sum('seconds');
$this->ticket->forceFill([
'time_spent_minutes' => (int) ceil($totalSeconds / 60),
])->save();
$this->activeTimer = null;
$this->elapsedSeconds = 0;
@@ -119,15 +122,15 @@ class TicketTimer extends Component
}
/**
* Render the ticket timer view.
* Render the timer view.
*/
public function render(): mixed
public function render()
{
return view('helpdesk::livewire.ticket-timer');
}
/**
* Refresh the active timer from the database.
* Refresh the active timer state from the database.
*/
private function refreshActiveTimer(): void
{
@@ -142,7 +145,7 @@ class TicketTimer extends Component
$this->activeTimer = $this->activeUserTimerQuery($user)->first();
$this->elapsedSeconds = $this->activeTimer instanceof TicketTimeLog
? (int) $this->activeTimer->started_at->diffInSeconds(Carbon::now())
? $this->activeTimer->started_at->diffInSeconds(Carbon::now())
: 0;
}
@@ -157,38 +160,6 @@ class TicketTimer extends Component
->whereNull('ended_at');
}
/**
* Build the active timer query for the authenticated user across all tickets.
*/
private function activeAnyTicketTimerQuery(Authenticatable $user): Builder
{
return TicketTimeLog::query()
->where('user_id', $user->getAuthIdentifier())
->whereNull('ended_at');
}
/**
* Update the ticket's rounded minute total using stored seconds.
*/
private function updateTicketTimeSpentFromSeconds(): void
{
$totalSeconds = (int) TicketTimeLog::query()
->where('ticket_id', $this->ticket->getKey())
->whereNotNull('ended_at')
->sum('seconds');
if ($totalSeconds === 0) {
$totalSeconds = (int) TicketTimeLog::query()
->where('ticket_id', $this->ticket->getKey())
->whereNotNull('ended_at')
->sum('minutes') * 60;
}
$this->ticket->forceFill([
'time_spent_minutes' => (int) ceil($totalSeconds / 60),
])->save();
}
/**
* Return the authenticated user or fail validation cleanly.
*
@@ -200,7 +171,7 @@ class TicketTimer extends Component
if (! $user instanceof Authenticatable) {
throw ValidationException::withMessages([
'timer' => 'You must be signed in to use the ticket timer.',
'timer' => 'You must be logged in to use the timer.',
]);
}
@@ -208,12 +179,12 @@ class TicketTimer extends Component
}
/**
* Return a safe note for the timer.
* Return the safe timer description.
*/
private function normalisedDescription(): string
{
$description = trim($this->description);
return $description !== '' ? $description : 'Work timer';
return $description !== '' ? $description : 'Timer entry';
}
}