diff --git a/Modules/Helpdesk/Http/Controllers/TicketControlsController.php b/Modules/Helpdesk/Http/Controllers/TicketControlsController.php index 502e691a..67ab7114 100644 --- a/Modules/Helpdesk/Http/Controllers/TicketControlsController.php +++ b/Modules/Helpdesk/Http/Controllers/TicketControlsController.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Modules\Helpdesk\Http\Controllers; use App\Http\Controllers\Controller; +use App\Models\User; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; @@ -50,8 +51,8 @@ final class TicketControlsController extends Controller 'last_activity_at' => now(), ]); - $fresh = $ticket->fresh(['agent', 'owner', 'status', 'priority', 'category', 'project', 'team', 'product']) ?? $ticket; - $workflow->dispatch($fresh, 'ticket.updated', Auth::user()); + $fresh = $ticket->fresh(['user.company', 'owner.company', 'agent.company', 'status', 'priority', 'category', 'project', 'team', 'product']) ?? $ticket; + $workflow->dispatch($fresh, 'ticket.updated', $this->actor()); return redirect() ->route('helpdesk.tickets.show', $fresh) @@ -83,13 +84,13 @@ final class TicketControlsController extends Controller $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) { $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() ->route('helpdesk.tickets.show', $fresh) @@ -114,8 +115,8 @@ final class TicketControlsController extends Controller 'last_activity_at' => now(), ]); - $fresh = $ticket->fresh(['agent', 'owner', 'status', 'priority', 'category', 'project', 'team', 'product']) ?? $ticket; - $workflow->dispatch($fresh, 'ticket.updated', Auth::user()); + $fresh = $ticket->fresh(['user.company', 'owner.company', 'agent.company', 'status', 'priority', 'category', 'project', 'team', 'product']) ?? $ticket; + $workflow->dispatch($fresh, 'ticket.updated', $this->actor()); return redirect() ->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. */ diff --git a/Modules/Helpdesk/Livewire/TicketEdit.php b/Modules/Helpdesk/Livewire/TicketEdit.php index f5f8502c..ac8e73af 100644 --- a/Modules/Helpdesk/Livewire/TicketEdit.php +++ b/Modules/Helpdesk/Livewire/TicketEdit.php @@ -248,8 +248,8 @@ final class TicketEdit extends Component private function freshTicket(Ticket $ticket): Ticket { return $ticket->fresh([ - 'owner', - 'agent', + 'owner.company', + 'agent.company', 'status', 'priority', 'category', diff --git a/Modules/Helpdesk/Livewire/TicketShow.php b/Modules/Helpdesk/Livewire/TicketShow.php index 76bdb048..d83c3032 100644 --- a/Modules/Helpdesk/Livewire/TicketShow.php +++ b/Modules/Helpdesk/Livewire/TicketShow.php @@ -55,6 +55,7 @@ final class TicketShow extends Component public function mount(Ticket $ticket): void { + $this->actor()->loadMissing('company'); $ticket->loadMissing($this->ticketDetailRelations()); $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'])), ]); - $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->ticket = $this->freshTicket($this->ticket); @@ -102,7 +103,7 @@ final class TicketShow extends Component '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->ticket = $this->freshTicket($this->ticket); @@ -122,7 +123,7 @@ final class TicketShow extends Component ->where('is_active', true) ->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->dispatch('helpdesk-canned-response-applied', body: $this->commentBody); } @@ -135,7 +136,7 @@ final class TicketShow extends Component ->where('ticket_id', $this->ticket->id) ->findOrFail($itemId); - $checklists->toggle($item, Auth::user()); + $checklists->toggle($item, $this->actor()); $this->ticket = $this->freshTicket($this->ticket); } @@ -176,7 +177,7 @@ final class TicketShow extends Component $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.'.'); $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->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.'.'); $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->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 ? 'Ticket is now unassigned.' @@ -261,6 +262,7 @@ final class TicketShow extends Component public function render(): View { + $this->actor()->loadMissing('company'); $this->ticket = $this->freshTicket($this->ticket); return view('helpdesk::livewire.ticket-show', [ @@ -274,6 +276,19 @@ final class TicketShow extends Component /** * 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 { if ($value === null || $value === '' || $value === '0' || $value === 0) { @@ -291,6 +306,7 @@ final class TicketShow extends Component private function ticketDetailRelations(): array { return [ + 'user.company', 'owner.company', 'agent.company', 'status', diff --git a/Modules/Helpdesk/Models/Ticket.php b/Modules/Helpdesk/Models/Ticket.php index fcf0a290..f69d1c7e 100644 --- a/Modules/Helpdesk/Models/Ticket.php +++ b/Modules/Helpdesk/Models/Ticket.php @@ -35,6 +35,7 @@ final class Ticket extends Model * @var list */ protected $with = [ + 'user.company', 'owner.company', 'agent.company', ]; @@ -67,6 +68,13 @@ final class Ticket extends Model 'time_spent_minutes' => 'integer', ]; + + /** @return BelongsTo */ + public function user(): BelongsTo + { + return $this->belongsTo(User::class, 'user_id'); + } + /** @return BelongsTo */ public function owner(): BelongsTo { diff --git a/Modules/Helpdesk/Services/CannedResponseRenderer.php b/Modules/Helpdesk/Services/CannedResponseRenderer.php index 99bd8f20..4be7ff81 100644 --- a/Modules/Helpdesk/Services/CannedResponseRenderer.php +++ b/Modules/Helpdesk/Services/CannedResponseRenderer.php @@ -11,7 +11,8 @@ final class CannedResponseRenderer { 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, [ '{{ ticket.reference }}' => $ticket->reference, diff --git a/Modules/Helpdesk/Services/WorkflowNotificationService.php b/Modules/Helpdesk/Services/WorkflowNotificationService.php index ee5aa265..4fb624fb 100644 --- a/Modules/Helpdesk/Services/WorkflowNotificationService.php +++ b/Modules/Helpdesk/Services/WorkflowNotificationService.php @@ -22,7 +22,8 @@ final class WorkflowNotificationService */ 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() ->where('event', $event) diff --git a/Modules/Helpdesk/changelog.yaml b/Modules/Helpdesk/changelog.yaml index 8cdc58e7..60a5ec18 100644 --- a/Modules/Helpdesk/changelog.yaml +++ b/Modules/Helpdesk/changelog.yaml @@ -198,3 +198,10 @@ versions: - 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 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. diff --git a/Modules/Helpdesk/module.json b/Modules/Helpdesk/module.json index aaafa75a..1e443c6b 100644 --- a/Modules/Helpdesk/module.json +++ b/Modules/Helpdesk/module.json @@ -22,5 +22,5 @@ "Modules\\Helpdesk\\Providers\\HelpdeskServiceProvider" ], "files": [], - "version": "1.3.10" + "version": "1.3.11" } diff --git a/config/mcp.php b/config/mcp.php deleted file mode 100644 index 34bb81e9..00000000 --- a/config/mcp.php +++ /dev/null @@ -1,54 +0,0 @@ - [ - '*', - // '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, - -];