Attempt to fix N+1 issues still there and more now
This commit is contained in:
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace Modules\Helpdesk\Http\Controllers;
|
namespace Modules\Helpdesk\Http\Controllers;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\User;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
@@ -50,8 +51,8 @@ final class TicketControlsController extends Controller
|
|||||||
'last_activity_at' => now(),
|
'last_activity_at' => now(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$fresh = $ticket->fresh(['agent', 'owner', 'status', 'priority', 'category', 'project', 'team', 'product']) ?? $ticket;
|
$fresh = $ticket->fresh(['user.company', 'owner.company', 'agent.company', 'status', 'priority', 'category', 'project', 'team', 'product']) ?? $ticket;
|
||||||
$workflow->dispatch($fresh, 'ticket.updated', Auth::user());
|
$workflow->dispatch($fresh, 'ticket.updated', $this->actor());
|
||||||
|
|
||||||
return redirect()
|
return redirect()
|
||||||
->route('helpdesk.tickets.show', $fresh)
|
->route('helpdesk.tickets.show', $fresh)
|
||||||
@@ -83,13 +84,13 @@ final class TicketControlsController extends Controller
|
|||||||
|
|
||||||
$this->ensureBoardContainsStatus($ticket, (int) $status->id);
|
$this->ensureBoardContainsStatus($ticket, (int) $status->id);
|
||||||
|
|
||||||
$fresh = $ticket->fresh(['agent', 'owner', 'status', 'priority', 'category', 'project', 'team', 'product']) ?? $ticket;
|
$fresh = $ticket->fresh(['user.company', 'owner.company', 'agent.company', 'status', 'priority', 'category', 'project', 'team', 'product']) ?? $ticket;
|
||||||
|
|
||||||
if ($status->is_closed) {
|
if ($status->is_closed) {
|
||||||
$sla->recordClosure($fresh);
|
$sla->recordClosure($fresh);
|
||||||
}
|
}
|
||||||
|
|
||||||
$workflow->dispatch($fresh, $status->is_closed ? 'ticket.closed' : 'ticket.updated', Auth::user());
|
$workflow->dispatch($fresh, $status->is_closed ? 'ticket.closed' : 'ticket.updated', $this->actor());
|
||||||
|
|
||||||
return redirect()
|
return redirect()
|
||||||
->route('helpdesk.tickets.show', $fresh)
|
->route('helpdesk.tickets.show', $fresh)
|
||||||
@@ -114,8 +115,8 @@ final class TicketControlsController extends Controller
|
|||||||
'last_activity_at' => now(),
|
'last_activity_at' => now(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$fresh = $ticket->fresh(['agent', 'owner', 'status', 'priority', 'category', 'project', 'team', 'product']) ?? $ticket;
|
$fresh = $ticket->fresh(['user.company', 'owner.company', 'agent.company', 'status', 'priority', 'category', 'project', 'team', 'product']) ?? $ticket;
|
||||||
$workflow->dispatch($fresh, 'ticket.updated', Auth::user());
|
$workflow->dispatch($fresh, 'ticket.updated', $this->actor());
|
||||||
|
|
||||||
return redirect()
|
return redirect()
|
||||||
->route('helpdesk.tickets.show', $fresh)
|
->route('helpdesk.tickets.show', $fresh)
|
||||||
@@ -172,6 +173,19 @@ final class TicketControlsController extends Controller
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the authenticated user with company eager-loaded for workflow rendering/N+1 safety.
|
||||||
|
*/
|
||||||
|
private function actor(): User
|
||||||
|
{
|
||||||
|
/** @var User $user */
|
||||||
|
$user = Auth::user();
|
||||||
|
$user->loadMissing('company');
|
||||||
|
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert form select values to nullable integers.
|
* Convert form select values to nullable integers.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -248,8 +248,8 @@ final class TicketEdit extends Component
|
|||||||
private function freshTicket(Ticket $ticket): Ticket
|
private function freshTicket(Ticket $ticket): Ticket
|
||||||
{
|
{
|
||||||
return $ticket->fresh([
|
return $ticket->fresh([
|
||||||
'owner',
|
'owner.company',
|
||||||
'agent',
|
'agent.company',
|
||||||
'status',
|
'status',
|
||||||
'priority',
|
'priority',
|
||||||
'category',
|
'category',
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ final class TicketShow extends Component
|
|||||||
|
|
||||||
public function mount(Ticket $ticket): void
|
public function mount(Ticket $ticket): void
|
||||||
{
|
{
|
||||||
|
$this->actor()->loadMissing('company');
|
||||||
$ticket->loadMissing($this->ticketDetailRelations());
|
$ticket->loadMissing($this->ticketDetailRelations());
|
||||||
|
|
||||||
$this->authorize('view', $ticket);
|
$this->authorize('view', $ticket);
|
||||||
@@ -85,7 +86,7 @@ final class TicketShow extends Component
|
|||||||
'attachments.*' => 'file|max:'.config('helpdesk.max_upload_kb', 10240).'|mimes:'.implode(',', config('helpdesk.allowed_mimes', ['jpg', 'jpeg', 'png', 'pdf', 'txt', 'log', 'zip'])),
|
'attachments.*' => 'file|max:'.config('helpdesk.max_upload_kb', 10240).'|mimes:'.implode(',', config('helpdesk.allowed_mimes', ['jpg', 'jpeg', 'png', 'pdf', 'txt', 'log', 'zip'])),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$tickets->comment($this->ticket, Auth::user(), $this->commentBody, $this->internal, $this->attachments);
|
$tickets->comment($this->ticket, $this->actor(), $this->commentBody, $this->internal, $this->attachments);
|
||||||
|
|
||||||
$this->reset(['commentBody', 'internal', 'attachments']);
|
$this->reset(['commentBody', 'internal', 'attachments']);
|
||||||
$this->ticket = $this->freshTicket($this->ticket);
|
$this->ticket = $this->freshTicket($this->ticket);
|
||||||
@@ -102,7 +103,7 @@ final class TicketShow extends Component
|
|||||||
'replyBody' => ['required', 'string', 'min:2'],
|
'replyBody' => ['required', 'string', 'min:2'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$tickets->comment($this->ticket, Auth::user(), $this->replyBody, $this->internal, [], $this->reply_parent_id);
|
$tickets->comment($this->ticket, $this->actor(), $this->replyBody, $this->internal, [], $this->reply_parent_id);
|
||||||
|
|
||||||
$this->reset(['reply_parent_id', 'replyBody']);
|
$this->reset(['reply_parent_id', 'replyBody']);
|
||||||
$this->ticket = $this->freshTicket($this->ticket);
|
$this->ticket = $this->freshTicket($this->ticket);
|
||||||
@@ -122,7 +123,7 @@ final class TicketShow extends Component
|
|||||||
->where('is_active', true)
|
->where('is_active', true)
|
||||||
->findOrFail($this->canned_response_id);
|
->findOrFail($this->canned_response_id);
|
||||||
|
|
||||||
$rendered = $renderer->render($response->body, $this->ticket, Auth::user());
|
$rendered = $renderer->render($response->body, $this->ticket, $this->actor());
|
||||||
$this->commentBody = trim($this->commentBody."\n\n".$rendered);
|
$this->commentBody = trim($this->commentBody."\n\n".$rendered);
|
||||||
$this->dispatch('helpdesk-canned-response-applied', body: $this->commentBody);
|
$this->dispatch('helpdesk-canned-response-applied', body: $this->commentBody);
|
||||||
}
|
}
|
||||||
@@ -135,7 +136,7 @@ final class TicketShow extends Component
|
|||||||
->where('ticket_id', $this->ticket->id)
|
->where('ticket_id', $this->ticket->id)
|
||||||
->findOrFail($itemId);
|
->findOrFail($itemId);
|
||||||
|
|
||||||
$checklists->toggle($item, Auth::user());
|
$checklists->toggle($item, $this->actor());
|
||||||
$this->ticket = $this->freshTicket($this->ticket);
|
$this->ticket = $this->freshTicket($this->ticket);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,7 +177,7 @@ final class TicketShow extends Component
|
|||||||
$sla->recordClosure($this->ticket);
|
$sla->recordClosure($this->ticket);
|
||||||
}
|
}
|
||||||
|
|
||||||
$workflow->dispatch($this->ticket, $status->is_closed ? 'ticket.closed' : 'ticket.updated', Auth::user());
|
$workflow->dispatch($this->ticket, $status->is_closed ? 'ticket.closed' : 'ticket.updated', $this->actor());
|
||||||
|
|
||||||
session()->flash('ticket-action-updated', 'Status updated to '.$this->ticket->status?->name.'.');
|
session()->flash('ticket-action-updated', 'Status updated to '.$this->ticket->status?->name.'.');
|
||||||
$this->dispatch('helpdesk-status-updated', statusId: $this->status_id);
|
$this->dispatch('helpdesk-status-updated', statusId: $this->status_id);
|
||||||
@@ -212,7 +213,7 @@ final class TicketShow extends Component
|
|||||||
$this->ticket = $this->freshTicket($this->ticket);
|
$this->ticket = $this->freshTicket($this->ticket);
|
||||||
$this->priority_id = (int) $this->ticket->priority_id;
|
$this->priority_id = (int) $this->ticket->priority_id;
|
||||||
|
|
||||||
$workflow->dispatch($this->ticket, 'ticket.updated', Auth::user());
|
$workflow->dispatch($this->ticket, 'ticket.updated', $this->actor());
|
||||||
|
|
||||||
session()->flash('ticket-action-updated', 'Priority updated to '.$this->ticket->priority?->name.'.');
|
session()->flash('ticket-action-updated', 'Priority updated to '.$this->ticket->priority?->name.'.');
|
||||||
$this->dispatch('helpdesk-priority-updated', priorityId: $this->priority_id);
|
$this->dispatch('helpdesk-priority-updated', priorityId: $this->priority_id);
|
||||||
@@ -250,7 +251,7 @@ final class TicketShow extends Component
|
|||||||
$this->ticket = $this->freshTicket($this->ticket);
|
$this->ticket = $this->freshTicket($this->ticket);
|
||||||
$this->agent_id = $this->ticket->agent_id === null ? null : (int) $this->ticket->agent_id;
|
$this->agent_id = $this->ticket->agent_id === null ? null : (int) $this->ticket->agent_id;
|
||||||
|
|
||||||
$workflow->dispatch($this->ticket, 'ticket.updated', Auth::user());
|
$workflow->dispatch($this->ticket, 'ticket.updated', $this->actor());
|
||||||
|
|
||||||
session()->flash('ticket-action-updated', $this->agent_id === null
|
session()->flash('ticket-action-updated', $this->agent_id === null
|
||||||
? 'Ticket is now unassigned.'
|
? 'Ticket is now unassigned.'
|
||||||
@@ -261,6 +262,7 @@ final class TicketShow extends Component
|
|||||||
|
|
||||||
public function render(): View
|
public function render(): View
|
||||||
{
|
{
|
||||||
|
$this->actor()->loadMissing('company');
|
||||||
$this->ticket = $this->freshTicket($this->ticket);
|
$this->ticket = $this->freshTicket($this->ticket);
|
||||||
|
|
||||||
return view('helpdesk::livewire.ticket-show', [
|
return view('helpdesk::livewire.ticket-show', [
|
||||||
@@ -274,6 +276,19 @@ final class TicketShow extends Component
|
|||||||
/**
|
/**
|
||||||
* Convert Livewire select values into a nullable integer.
|
* Convert Livewire select values into a nullable integer.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the authenticated user with relations commonly used by policies and workflow templates.
|
||||||
|
*/
|
||||||
|
private function actor(): User
|
||||||
|
{
|
||||||
|
/** @var User $user */
|
||||||
|
$user = Auth::user();
|
||||||
|
$user->loadMissing('company');
|
||||||
|
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
|
||||||
private function normaliseNullableInt(mixed $value): ?int
|
private function normaliseNullableInt(mixed $value): ?int
|
||||||
{
|
{
|
||||||
if ($value === null || $value === '' || $value === '0' || $value === 0) {
|
if ($value === null || $value === '' || $value === '0' || $value === 0) {
|
||||||
@@ -291,6 +306,7 @@ final class TicketShow extends Component
|
|||||||
private function ticketDetailRelations(): array
|
private function ticketDetailRelations(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
'user.company',
|
||||||
'owner.company',
|
'owner.company',
|
||||||
'agent.company',
|
'agent.company',
|
||||||
'status',
|
'status',
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ final class Ticket extends Model
|
|||||||
* @var list<string>
|
* @var list<string>
|
||||||
*/
|
*/
|
||||||
protected $with = [
|
protected $with = [
|
||||||
|
'user.company',
|
||||||
'owner.company',
|
'owner.company',
|
||||||
'agent.company',
|
'agent.company',
|
||||||
];
|
];
|
||||||
@@ -67,6 +68,13 @@ final class Ticket extends Model
|
|||||||
'time_spent_minutes' => 'integer',
|
'time_spent_minutes' => 'integer',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
|
/** @return BelongsTo<User, Ticket> */
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'user_id');
|
||||||
|
}
|
||||||
|
|
||||||
/** @return BelongsTo<User, Ticket> */
|
/** @return BelongsTo<User, Ticket> */
|
||||||
public function owner(): BelongsTo
|
public function owner(): BelongsTo
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ final class CannedResponseRenderer
|
|||||||
{
|
{
|
||||||
public function render(string $template, Ticket $ticket, ?User $actor = null): string
|
public function render(string $template, Ticket $ticket, ?User $actor = null): string
|
||||||
{
|
{
|
||||||
$ticket->loadMissing(['owner', 'agent', 'status', 'priority', 'category', 'project', 'team']);
|
$ticket->loadMissing(['user.company', 'owner.company', 'agent.company', 'status', 'priority', 'category', 'project', 'team']);
|
||||||
|
$actor?->loadMissing('company');
|
||||||
|
|
||||||
return strtr($template, [
|
return strtr($template, [
|
||||||
'{{ ticket.reference }}' => $ticket->reference,
|
'{{ ticket.reference }}' => $ticket->reference,
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ final class WorkflowNotificationService
|
|||||||
*/
|
*/
|
||||||
public function dispatch(Ticket $ticket, string $event, ?User $actor = null): void
|
public function dispatch(Ticket $ticket, string $event, ?User $actor = null): void
|
||||||
{
|
{
|
||||||
$ticket->loadMissing(['owner', 'agent', 'status', 'priority', 'category', 'project', 'team', 'product']);
|
$ticket->loadMissing(['user.company', 'owner.company', 'agent.company', 'status', 'priority', 'category', 'project', 'team', 'product']);
|
||||||
|
$actor?->loadMissing('company');
|
||||||
|
|
||||||
WorkflowTemplate::query()
|
WorkflowTemplate::query()
|
||||||
->where('event', $event)
|
->where('event', $event)
|
||||||
|
|||||||
@@ -198,3 +198,10 @@ versions:
|
|||||||
- Replaced remaining unsupported orWhereKey usage with supported id predicates in ticket assignment option queries.
|
- Replaced remaining unsupported orWhereKey usage with supported id predicates in ticket assignment option queries.
|
||||||
- Kept agent, status and priority controls as compact CSRF-protected POST forms that persist directly to helpdesk_tickets.
|
- Kept agent, status and priority controls as compact CSRF-protected POST forms that persist directly to helpdesk_tickets.
|
||||||
- Kept module.json version in sync with changelog.yaml.
|
- Kept module.json version in sync with changelog.yaml.
|
||||||
|
|
||||||
|
|
||||||
|
- version: 1.3.11
|
||||||
|
date: 2026-06-16
|
||||||
|
changes:
|
||||||
|
- Eager-loaded ticket requester, assigned agent, and related company records across ticket detail and workflow-control paths to remove remaining N+1 warnings on ticket load and after agent/status/priority actions.
|
||||||
|
- Added the user relation alias on helpdesk tickets for compatibility with existing views, policies, detectors, and services that access the requester through either user or owner.
|
||||||
|
|||||||
@@ -22,5 +22,5 @@
|
|||||||
"Modules\\Helpdesk\\Providers\\HelpdeskServiceProvider"
|
"Modules\\Helpdesk\\Providers\\HelpdeskServiceProvider"
|
||||||
],
|
],
|
||||||
"files": [],
|
"files": [],
|
||||||
"version": "1.3.10"
|
"version": "1.3.11"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
return [
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Redirect Domains
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| These domains are the domains that OAuth clients are permitted to use
|
|
||||||
| for redirect URIs. Each domain should be specified with its scheme
|
|
||||||
| and host. Domains not in this list will raise validation errors.
|
|
||||||
|
|
|
||||||
| An "*" may be used to allow all domains.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'redirect_domains' => [
|
|
||||||
'*',
|
|
||||||
// 'https://example.com',
|
|
||||||
// 'http://localhost',
|
|
||||||
],
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Allowed Custom Schemes
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Native desktop OAuth clients like Cursor and VS Code use private-use URI
|
|
||||||
| schemes (RFC 8252) for redirect callbacks instead of standard schemes
|
|
||||||
| like HTTPS. Here, you may list which custom schemes you will allow.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'custom_schemes' => [
|
|
||||||
// 'claude',
|
|
||||||
// 'cursor',
|
|
||||||
// 'vscode',
|
|
||||||
],
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Authorization Server
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Here you may configure the OAuth authorization server issuer identifier
|
|
||||||
| per RFC 8414. This value appears in your protected resource and auth
|
|
||||||
| server metadata endpoints. When null, this defaults to `url('/')`.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'authorization_server' => null,
|
|
||||||
|
|
||||||
];
|
|
||||||
Reference in New Issue
Block a user