219 lines
6.2 KiB
PHP
219 lines
6.2 KiB
PHP
<?php
|
|
|
|
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\Validation\ValidationException;
|
|
use Livewire\Component;
|
|
use Modules\Helpdesk\Models\Ticket;
|
|
use Modules\Helpdesk\Models\TicketTimeLog;
|
|
|
|
class TicketTimer extends Component
|
|
{
|
|
public Ticket $ticket;
|
|
|
|
public ?TicketTimeLog $activeTimer = null;
|
|
|
|
public string $description = '';
|
|
|
|
public ?string $message = null;
|
|
|
|
public int $elapsedSeconds = 0;
|
|
|
|
/**
|
|
* Mount the timer for the current ticket.
|
|
*/
|
|
public function mount(Ticket $ticket): void
|
|
{
|
|
$this->ticket = $ticket;
|
|
$this->refreshActiveTimer();
|
|
}
|
|
|
|
/**
|
|
* Start a database-backed timer for the authenticated user.
|
|
*
|
|
* @throws \Illuminate\Validation\ValidationException
|
|
*/
|
|
public function startTimer(): void
|
|
{
|
|
$user = $this->authenticatedUser();
|
|
|
|
if ($this->userHasAnyActiveTimer($user)) {
|
|
throw ValidationException::withMessages([
|
|
'timer' => 'You already have a running timer. End the current timer before starting another one.',
|
|
]);
|
|
}
|
|
|
|
$startedAt = Carbon::now();
|
|
|
|
try {
|
|
TicketTimeLog::query()->create([
|
|
'ticket_id' => $this->ticket->getKey(),
|
|
'user_id' => $user->getAuthIdentifier(),
|
|
'minutes' => 0,
|
|
'description' => $this->normalisedDescription(),
|
|
'started_at' => $startedAt,
|
|
'ended_at' => null,
|
|
'logged_at' => $startedAt,
|
|
]);
|
|
} catch (QueryException $exception) {
|
|
if ($this->isUniqueTimerConstraintViolation($exception)) {
|
|
throw ValidationException::withMessages([
|
|
'timer' => 'You already have a running timer. End the current timer before starting another one.',
|
|
]);
|
|
}
|
|
|
|
throw $exception;
|
|
}
|
|
|
|
$this->description = '';
|
|
$this->message = 'Timer started.';
|
|
$this->refreshActiveTimer();
|
|
|
|
$this->dispatch('timer-started');
|
|
}
|
|
|
|
/**
|
|
* End the active timer and add the elapsed minutes to the ticket total.
|
|
*
|
|
* @throws \Illuminate\Validation\ValidationException
|
|
*/
|
|
public function endTimer(): void
|
|
{
|
|
$user = $this->authenticatedUser();
|
|
$timer = $this->activeTimerQuery($user)->first();
|
|
|
|
if (! $timer instanceof TicketTimeLog) {
|
|
throw ValidationException::withMessages([
|
|
'timer' => 'There is no running timer to end.',
|
|
]);
|
|
}
|
|
|
|
$endedAt = Carbon::now();
|
|
$minutes = max(1, (int) ceil($timer->started_at->diffInSeconds($endedAt) / 60));
|
|
$description = trim($this->description) !== '' ? trim($this->description) : $timer->description;
|
|
|
|
$timer->forceFill([
|
|
'ended_at' => $endedAt,
|
|
'minutes' => $minutes,
|
|
'description' => $description,
|
|
'logged_at' => $endedAt,
|
|
])->save();
|
|
|
|
$this->refreshTicketTimeSpentMinutes();
|
|
|
|
$this->description = '';
|
|
$this->message = 'Timer ended and time was added to the ticket.';
|
|
$this->refreshActiveTimer();
|
|
|
|
$this->dispatch('timer-ended');
|
|
}
|
|
|
|
/**
|
|
* Render the ticket timer view.
|
|
*/
|
|
public function render()
|
|
{
|
|
return view('helpdesk::livewire.ticket-timer');
|
|
}
|
|
|
|
/**
|
|
* Refresh the active timer and elapsed seconds from the database.
|
|
*/
|
|
public function refreshActiveTimer(): void
|
|
{
|
|
$user = Auth::user();
|
|
|
|
if (! $user instanceof Authenticatable) {
|
|
$this->activeTimer = null;
|
|
$this->elapsedSeconds = 0;
|
|
|
|
return;
|
|
}
|
|
|
|
$this->activeTimer = $this->activeTimerQuery($user)->first();
|
|
$this->elapsedSeconds = $this->activeTimer instanceof TicketTimeLog
|
|
? $this->activeTimer->started_at->diffInSeconds(Carbon::now())
|
|
: 0;
|
|
}
|
|
|
|
/**
|
|
* Build the active timer query for the authenticated user and current ticket.
|
|
*/
|
|
private function activeTimerQuery(Authenticatable $user): Builder
|
|
{
|
|
return TicketTimeLog::query()
|
|
->where('ticket_id', $this->ticket->getKey())
|
|
->where('user_id', $user->getAuthIdentifier())
|
|
->whereNull('ended_at');
|
|
}
|
|
|
|
/**
|
|
* Check for any active timer for this user across all tickets.
|
|
*/
|
|
private function userHasAnyActiveTimer(Authenticatable $user): bool
|
|
{
|
|
return TicketTimeLog::query()
|
|
->where('user_id', $user->getAuthIdentifier())
|
|
->whereNull('ended_at')
|
|
->exists();
|
|
}
|
|
|
|
/**
|
|
* Refresh the ticket's denormalised total without requiring a Ticket::timeLogs() relationship.
|
|
*/
|
|
private function refreshTicketTimeSpentMinutes(): void
|
|
{
|
|
$totalMinutes = (int) TicketTimeLog::query()
|
|
->where('ticket_id', $this->ticket->getKey())
|
|
->whereNotNull('ended_at')
|
|
->sum('minutes');
|
|
|
|
$this->ticket->forceFill([
|
|
'time_spent_minutes' => $totalMinutes,
|
|
])->save();
|
|
}
|
|
|
|
/**
|
|
* Return the authenticated user or fail validation cleanly.
|
|
*
|
|
* @throws \Illuminate\Validation\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 description to store against the timer.
|
|
*/
|
|
private function normalisedDescription(): string
|
|
{
|
|
$description = trim($this->description);
|
|
|
|
return $description !== '' ? $description : 'Timer work log';
|
|
}
|
|
|
|
/**
|
|
* Detect PostgreSQL/MySQL unique constraint collisions for active timers.
|
|
*/
|
|
private function isUniqueTimerConstraintViolation(QueryException $exception): bool
|
|
{
|
|
$sqlState = (string) ($exception->errorInfo[0] ?? '');
|
|
|
|
return in_array($sqlState, ['23000', '23505'], true);
|
|
}
|
|
}
|