ticket = $ticket; $this->refreshActiveTimer(); } /** * Refresh the timer every poll cycle while the timer is running. */ public function refreshTimer(): void { $this->refreshActiveTimer(); } /** * Start a database-backed timer for the current authenticated user. * * @throws ValidationException */ public function startTimer(): void { $user = $this->authenticatedUser(); if ($this->activeUserTimerQuery($user)->exists()) { throw ValidationException::withMessages([ 'timer' => 'You already have a running timer.', ]); } $startedAt = Carbon::now(); 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) { throw ValidationException::withMessages([ 'timer' => 'You already have a running timer.', ]); } $this->activeTimer = $timer->fresh(); $this->elapsedSeconds = 0; $this->description = ''; $this->message = 'Timer started.'; $this->dispatch('timer-started'); $this->dispatch('helpdesk-ticket-time-log-updated'); } /** * End the current user's active timer and persist seconds/minutes. * * @throws ValidationException */ public function endTimer(): void { $user = $this->authenticatedUser(); $timer = $this->activeUserTimerQuery($user)->first(); if (! $timer instanceof TicketTimeLog) { throw ValidationException::withMessages([ 'timer' => 'There is no running timer to end.', ]); } $endedAt = Carbon::now(); $description = trim($this->description) !== '' ? trim($this->description) : $timer->description; $seconds = max(1, (int) $timer->started_at->diffInSeconds($endedAt)); $minutes = (int) ceil($seconds / 60); $timer->forceFill([ 'ended_at' => $endedAt, 'seconds' => $seconds, 'minutes' => $minutes, 'description' => $description, 'logged_at' => $endedAt, ])->save(); $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; $this->description = ''; $this->message = 'Timer ended and time was added to the ticket.'; $this->dispatch('timer-ended'); $this->dispatch('helpdesk-ticket-time-log-updated'); } /** * Get the currently elapsed timer duration. */ public function getElapsedTimeProperty(): string { return gmdate('H:i:s', $this->elapsedSeconds); } /** * Determine whether the current user has a running timer. */ public function getIsRunningProperty(): bool { return $this->activeTimer instanceof TicketTimeLog; } /** * Render the timer view. */ public function render() { return view('modules.helpdesk.livewire.ticket-timer'); } /** * Refresh the active timer state from the database. */ private function refreshActiveTimer(): void { $user = Auth::user(); if (! $user instanceof Authenticatable) { $this->activeTimer = null; $this->elapsedSeconds = 0; return; } $this->activeTimer = $this->activeUserTimerQuery($user)->first(); $this->elapsedSeconds = $this->activeTimer instanceof TicketTimeLog ? max(0, (int) $this->activeTimer->started_at->diffInSeconds(Carbon::now())) : 0; } /** * Build the active timer query for the authenticated user and current ticket. */ private function activeUserTimerQuery(Authenticatable $user): Builder { return TicketTimeLog::query() ->where('ticket_id', $this->ticket->getKey()) ->where('user_id', $user->getAuthIdentifier()) ->whereNull('ended_at'); } /** * Return the authenticated user or fail validation cleanly. * * @throws ValidationException */ private function authenticatedUser(): Authenticatable { $user = Auth::user(); if (! $user instanceof Authenticatable) { throw ValidationException::withMessages([ 'timer' => 'You must be logged in to use the timer.', ]); } return $user; } /** * Return the safe timer description. */ private function normalisedDescription(): string { $description = trim($this->description); return $description !== '' ? $description : 'Timer entry'; } }