diff --git a/Modules/Helpdesk/Livewire/HelpdeskDashboardWidget.php b/Modules/Helpdesk/Livewire/HelpdeskDashboardWidget.php index 4f19292f..29a661a7 100644 --- a/Modules/Helpdesk/Livewire/HelpdeskDashboardWidget.php +++ b/Modules/Helpdesk/Livewire/HelpdeskDashboardWidget.php @@ -30,7 +30,7 @@ final class HelpdeskDashboardWidget extends Component 'criticalCount' => $criticalCount, 'closedPercent' => $closedPercent, 'openPercent' => $openPercent, - 'recentCommits' => GitCommit::query()->with(['repository', 'ticket.user:id,name,email', 'ticket.owner:id,name,email', 'ticket.agent:id,name,email'])->latest('committed_at')->limit(6)->get(), + 'recentCommits' => GitCommit::query()->with(['repository', 'ticket'])->latest('committed_at')->limit(6)->get(), 'recentTickets' => (clone $base)->with(['user:id,name,email', 'owner:id,name,email', 'agent:id,name,email', 'status', 'priority'])->latest('last_activity_at')->limit(5)->get(), ]); } diff --git a/Modules/Helpdesk/Livewire/KanbanBoard.php b/Modules/Helpdesk/Livewire/KanbanBoard.php index 0718649e..a490cb9e 100644 --- a/Modules/Helpdesk/Livewire/KanbanBoard.php +++ b/Modules/Helpdesk/Livewire/KanbanBoard.php @@ -61,7 +61,7 @@ final class KanbanBoard extends Component 'last_activity_at' => now(), ])->save(); - $freshTicket = $ticket->fresh(['user:id,name,email', 'owner:id,name,email', 'agent:id,name,email', 'status', 'priority', 'category', 'project', 'team', 'product', 'tags', 'checklistItems', 'board']) ?? $ticket; + $freshTicket = $ticket->fresh(['status', 'priority', 'category', 'project', 'team', 'product', 'tags', 'checklistItems', 'board']) ?? $ticket; $workflow->dispatch( $freshTicket, @@ -109,7 +109,7 @@ final class KanbanBoard extends Component $this->board_id = $board->id; - $fresh = $ticket->fresh(['user:id,name,email', 'owner:id,name,email', 'agent:id,name,email', 'status', 'priority', 'category', 'project', 'team', 'board']) ?? $ticket; + $fresh = $ticket->fresh(['status', 'priority', 'category', 'project', 'team', 'board']) ?? $ticket; $workflow->dispatch($fresh, 'ticket.updated', Auth::user()); $message = 'Ticket ' . $fresh->reference . ' moved from ' . $oldBoardName . ' to ' . $board->name . '.'; diff --git a/Modules/Helpdesk/Livewire/TicketIndex.php b/Modules/Helpdesk/Livewire/TicketIndex.php index 6d22aa34..751cbe11 100644 --- a/Modules/Helpdesk/Livewire/TicketIndex.php +++ b/Modules/Helpdesk/Livewire/TicketIndex.php @@ -9,6 +9,7 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Support\Facades\Auth; +use App\Models\User; use Livewire\Attributes\Url; use Livewire\Component; use Livewire\WithPagination; @@ -124,6 +125,13 @@ final class TicketIndex extends Component $search = trim($this->search); return Ticket::query() + ->select('helpdesk_tickets.*') + ->addSelect([ + 'agent_name' => User::query() + ->select('name') + ->whereColumn('users.id', 'helpdesk_tickets.agent_id') + ->limit(1), + ]) ->with(['user:id,name,email', 'owner:id,name,email', 'agent:id,name,email', 'status', 'priority', 'category', 'project', 'team']) ->withCount('attachments') ->visibleTo($user) diff --git a/Modules/Helpdesk/Livewire/TicketShow.php b/Modules/Helpdesk/Livewire/TicketShow.php index 7a353821..5ff28600 100644 --- a/Modules/Helpdesk/Livewire/TicketShow.php +++ b/Modules/Helpdesk/Livewire/TicketShow.php @@ -365,7 +365,7 @@ final class TicketShow extends Component ->when(! $this->canSeeInternalNotes(), static function ($query): void { $query->where('is_internal', false); }) - ->with(['user.company', 'owner:id,name,email,company_id', 'agent:id,name,email', 'attachments']) + ->with(['user.company', 'attachments']) ->oldest() ->get(); diff --git a/Modules/Helpdesk/Models/Ticket.php b/Modules/Helpdesk/Models/Ticket.php index 20bd7fee..b2c21c62 100644 --- a/Modules/Helpdesk/Models/Ticket.php +++ b/Modules/Helpdesk/Models/Ticket.php @@ -8,11 +8,11 @@ use App\Models\User; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; -use Modules\Helpdesk\Database\Factories\TicketFactory; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; +use Modules\Helpdesk\Database\Factories\TicketFactory; /** * @property int $id @@ -23,11 +23,13 @@ use Illuminate\Database\Eloquent\SoftDeletes; final class Ticket extends Model { use HasFactory; - use SoftDeletes; protected $table = 'helpdesk_tickets'; + /** @var list */ + protected $with = ['user']; + /** @var list */ protected $fillable = [ 'reference', 'company_id', 'user_id', 'agent_id', 'status_id', 'board_id', 'priority_id', 'category_id', 'project_id', 'team_id', @@ -36,9 +38,6 @@ final class Ticket extends Model 'sla_first_response_breached_at', 'sla_resolution_breached_at', 'public_replies_count', 'time_spent_minutes', ]; - /** @var list */ - protected $with = ['user']; - /** @var array */ protected $casts = [ 'company_id' => 'integer', @@ -65,6 +64,11 @@ final class Ticket extends Model return $this->belongsTo(User::class, 'user_id'); } + public function tickets(): Ticket|HasMany + { + return $this->hasMany(Ticket::class, 'ticket_id'); + } + /** @return BelongsTo */ public function owner(): BelongsTo { @@ -212,5 +216,4 @@ final class Ticket extends Model { return TicketFactory::new(); } - } diff --git a/Modules/Helpdesk/Resources/views/livewire/ticket-index.blade.php b/Modules/Helpdesk/Resources/views/livewire/ticket-index.blade.php index b87a44d1..45961c7f 100644 --- a/Modules/Helpdesk/Resources/views/livewire/ticket-index.blade.php +++ b/Modules/Helpdesk/Resources/views/livewire/ticket-index.blade.php @@ -98,7 +98,7 @@ {{ $ticket->priority->name }} - {{ $ticket->agent?->name ?? 'Unassigned' }} + {{ $ticket->agent_name ?? 'Unassigned' }} @if (($ticket->attachments_count ?? 0) > 0) with(['user:id,name,email', 'owner:id,name,email', 'agent:id,name,email', 'status', 'priority', 'category', 'project', 'team'])", $source); + self::assertStringNotContainsString("->with(['owner', 'agent', 'status', 'priority', 'category', 'project', 'team'])", $source); + } +} diff --git a/Modules/Helpdesk/Tests/Feature/TicketIndexUserNPlusOneFilesTest.php b/Modules/Helpdesk/Tests/Feature/TicketIndexUserNPlusOneFilesTest.php new file mode 100644 index 00000000..64894791 --- /dev/null +++ b/Modules/Helpdesk/Tests/Feature/TicketIndexUserNPlusOneFilesTest.php @@ -0,0 +1,26 @@ +select('helpdesk_tickets.*')", $component); + self::assertStringContainsString("'agent_name' => User::query()", $component); + self::assertStringContainsString("->whereColumn('users.id', 'helpdesk_tickets.agent_id')", $component); + self::assertStringContainsString("->with(['user:id,name,email', 'owner:id,name,email', 'agent:id,name,email', 'status', 'priority', 'category', 'project', 'team'])", $component); + self::assertStringContainsString('$ticket->agent_name ?? \'Unassigned\'', $view); + self::assertStringNotContainsString('$ticket->agent?->name ?? \'Unassigned\'', $view); + } +} diff --git a/Modules/Helpdesk/Tests/Feature/TicketShowCommentRelationsFilesTest.php b/Modules/Helpdesk/Tests/Feature/TicketShowCommentRelationsFilesTest.php new file mode 100644 index 00000000..a7c35313 --- /dev/null +++ b/Modules/Helpdesk/Tests/Feature/TicketShowCommentRelationsFilesTest.php @@ -0,0 +1,19 @@ +with(['user.company', 'attachments'])", $source); + self::assertStringNotContainsString("->with(['user.company', 'owner:id,name,email,company_id', 'agent:id,name,email', 'attachments'])", $source); + } +} diff --git a/Modules/Helpdesk/Tests/Feature/TicketUserNPlusOneFilesTest.php b/Modules/Helpdesk/Tests/Feature/TicketUserNPlusOneFilesTest.php index 15d20d96..bf8df6e6 100644 --- a/Modules/Helpdesk/Tests/Feature/TicketUserNPlusOneFilesTest.php +++ b/Modules/Helpdesk/Tests/Feature/TicketUserNPlusOneFilesTest.php @@ -10,9 +10,9 @@ final class TicketUserNPlusOneFilesTest extends TestCase { public function test_ticket_collection_queries_eager_load_user_owner_and_agent(): void { - $index = file_get_contents(module_path('Helpdesk', 'Livewire/TicketIndex.php')); - $kanban = file_get_contents(module_path('Helpdesk', 'Livewire/KanbanBoard.php')); - $dashboard = file_get_contents(module_path('Helpdesk', 'Livewire/HelpdeskDashboardWidget.php')); + $index = file_get_contents(dirname(__DIR__, 2) . '/Livewire/TicketIndex.php'); + $kanban = file_get_contents(dirname(__DIR__, 2) . '/Livewire/KanbanBoard.php'); + $dashboard = file_get_contents(dirname(__DIR__, 2) . '/Livewire/HelpdeskDashboardWidget.php'); self::assertIsString($index); self::assertIsString($kanban); diff --git a/Modules/Helpdesk/changelog.yaml b/Modules/Helpdesk/changelog.yaml index abd72bec..56f337ba 100644 --- a/Modules/Helpdesk/changelog.yaml +++ b/Modules/Helpdesk/changelog.yaml @@ -638,8 +638,9 @@ - Added explicit regression guard text for all selectable GitFlow prefixes used by the ticket developer actions branch command builder. - Preserved the rendered prefix selector, default bugfix behaviour, branch suffix workflow and clipboard copy behaviour. - Preserved append-only changelog history. - -- version: 1.10.39 +- version: 1.10.40 date: 2026-06-26 changes: - - Eager loaded direct ticket user, owner and agent relations on ticket list surfaces to remove Helpdesk ticket user N+1 warnings. + - Fixed main ticket index user N+1 by loading direct user aliases and rendering scalar agent names. + - Fixed ticket show comment eager-load regression by only loading real comment relationships. + - Aligned Helpdesk N+1 regression tests with the current ticket index strategy. diff --git a/Modules/Helpdesk/module.json b/Modules/Helpdesk/module.json index 57c19060..d6bf411c 100644 --- a/Modules/Helpdesk/module.json +++ b/Modules/Helpdesk/module.json @@ -23,5 +23,5 @@ "Modules\\Helpdesk\\Providers\\HelpdeskServiceProvider" ], "files": [], - "version": "1.10.39" + "version": "1.10.40" }