started to add indexes and fixed N+1 issue

This commit is contained in:
Mark Gregory
2026-06-12 13:52:45 +01:00
parent 8ec2c33c47
commit 4dcee08d71
7 changed files with 136 additions and 25 deletions
@@ -0,0 +1,65 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class HelpdeskPerformanceIndexes extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
Schema::table('helpdesk_tickets', function (Blueprint $table): void {
$table->index(['company_id', 'status_id']);
$table->index(['company_id', 'priority_id']);
$table->index(['company_id', 'category_id']);
$table->index(['company_id', 'project_id']);
$table->index(['company_id', 'team_id']);
$table->index(['agent_id', 'completed_at']);
$table->index(['user_id', 'created_at']);
$table->index(['status_id', 'created_at']);
$table->index(['last_activity_at']);
$table->index(['due_at']);
$table->index(['reference']);
});
Schema::table('helpdesk_comments', function (Blueprint $table): void {
$table->index(['ticket_id', 'parent_id']);
$table->index(['ticket_id', 'created_at']);
$table->index(['parent_id', 'created_at']);
$table->index(['user_id', 'created_at']);
});
Schema::table('helpdesk_attachments', function (Blueprint $table): void {
$table->index(['ticket_id']);
$table->index(['comment_id']);
$table->index(['user_id']);
});
Schema::table('helpdesk_projects', function (Blueprint $table): void {
$table->index(['company_id', 'archived_at']);
});
Schema::table('helpdesk_teams', function (Blueprint $table): void {
$table->index(['company_id', 'archived_at']);
});
Schema::table('helpdesk_category_user', function (Blueprint $table): void {
$table->index(['category_id', 'user_id']);
$table->index(['user_id', 'category_id']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}
+44 -14
View File
@@ -4,12 +4,15 @@ declare(strict_types=1);
namespace Modules\Helpdesk\Livewire;
use App\Models\User;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Validate;
use Livewire\Component;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
use Livewire\WithFileUploads;
use Modules\Helpdesk\Models\Comment;
use Modules\Helpdesk\Models\Priority;
use Modules\Helpdesk\Models\Status;
use Modules\Helpdesk\Models\Ticket;
@@ -38,7 +41,6 @@ final class TicketShow extends Component
public string $replyBody = '';
/** @return void */
public function mount(Ticket $ticket): void
{
$this->authorize('view', $ticket);
@@ -47,28 +49,25 @@ final class TicketShow extends Component
$this->priority_id = $this->ticket->priority_id;
}
/** @return void */
public function startReply(int $commentId): void
{
$this->reply_parent_id = $commentId;
$this->replyBody = '';
}
/** @return void */
public function cancelReply(): void
{
$this->reply_parent_id = null;
$this->replyBody = '';
}
/** @return void */
public function addComment(TicketService $tickets): void
{
$this->authorize('comment', $this->ticket);
$this->validate([
'commentBody' => ['required', 'string', 'min:2'],
'attachments.*' => 'file|max:' . config('helpdesk.max_upload_kb') . '|mimes:' . implode(',', config('helpdesk.allowed_mimes')),
'attachments.*' => 'file|max:'.config('helpdesk.max_upload_kb').'|mimes:'.implode(',', config('helpdesk.allowed_mimes')),
]);
$tickets->comment($this->ticket, Auth::user(), $this->commentBody, $this->internal, $this->attachments);
@@ -78,7 +77,6 @@ final class TicketShow extends Component
$this->dispatch('helpdesk-comment-added');
}
/** @return void */
public function addReply(TicketService $tickets): void
{
$this->authorize('comment', $this->ticket);
@@ -95,7 +93,6 @@ final class TicketShow extends Component
$this->dispatch('helpdesk-reply-added');
}
/** @return void */
public function updateStatus(WorkflowNotificationService $workflow): void
{
$this->authorize('update', $this->ticket);
@@ -113,7 +110,6 @@ final class TicketShow extends Component
$this->dispatch('helpdesk-status-updated');
}
/** @return void */
public function updatePriority(WorkflowNotificationService $workflow): void
{
$this->authorize('update', $this->ticket);
@@ -129,7 +125,6 @@ final class TicketShow extends Component
$this->dispatch('helpdesk-priority-updated');
}
/** @return View */
public function render(): View
{
return view('helpdesk::livewire.ticket-show', [
@@ -138,13 +133,48 @@ final class TicketShow extends Component
])->layout('layouts.app');
}
/** @return Ticket */
private function freshTicket(Ticket $ticket): Ticket
{
return $ticket->fresh([
'owner', 'agent', 'status', 'priority', 'category', 'project', 'team',
'comments.user', 'comments.attachments', 'comments.replies.user', 'comments.replies.attachments', 'comments.replies.replies.user',
'attachments.user', 'gitCommits.repository',
$ticket = $ticket->fresh([
'owner',
'agent',
'status',
'priority',
'category',
'project',
'team',
'attachments.user',
'gitCommits.repository',
]) ?? $ticket;
$comments = Comment::query()
->where('ticket_id', $ticket->id)
->with(['user', 'attachments'])
->oldest()
->get();
$grouped = $comments->groupBy('parent_id');
$comments->each(function ($comment) use ($grouped): void {
$comment->setRelation('replies', $grouped->get($comment->id, collect()));
});
$ticket->setRelation('comments', $this->buildCommentTree($grouped));
return $ticket;
}
private function buildCommentTree($grouped, ?int $parentId = null)
{
return $grouped->get($parentId, collect())->map(function ($comment) use ($grouped) {
$comment->setRelation('replies', $this->buildCommentTree($grouped, $comment->id));
return $comment;
});
}
}
-3
View File
@@ -5,15 +5,12 @@ declare(strict_types=1);
namespace Modules\Helpdesk\Models;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
final class Comment extends Model
{
use HasFactory;
protected $table = 'helpdesk_comments';
/** @var list<string> */
+3 -2
View File
@@ -20,7 +20,6 @@ use Illuminate\Database\Eloquent\SoftDeletes;
*/
final class Ticket extends Model
{
use HasFactory;
use SoftDeletes;
protected $table = 'helpdesk_tickets';
@@ -86,7 +85,9 @@ final class Ticket extends Model
/** @return HasMany<Comment> */
public function comments(): HasMany
{
return $this->hasMany(Comment::class, 'ticket_id')->whereNull('parent_id')->with(['user', 'attachments', 'replies.user', 'replies.attachments', 'replies.replies.user'])->oldest();
return $this->hasMany(Comment::class, 'ticket_id')
->whereNull('parent_id')
->oldest();
}
/** @return HasMany<GitCommit> */
@@ -1,3 +1,5 @@
import './bootstrap';
import Sortable from 'sortablejs';
window.Sortable = Sortable;
console.info('Helpdesk module assets loaded');
@@ -2,12 +2,23 @@
<div class="space-y-3">
@foreach ($comments as $comment)
@php
$commentUser = $comment->relationLoaded('user') ? $comment->getRelation('user') : null;
$commentAttachments = $comment->relationLoaded('attachments') ? $comment->getRelation('attachments') : collect();
$commentReplies = $comment->relationLoaded('replies') ? $comment->getRelation('replies') : collect();
@endphp
<div class="rounded-2xl border border-zinc-200 bg-white p-4 shadow-sm dark:border-zinc-700 dark:bg-zinc-900" style="margin-left: {{ min((int) $level, 4) * 1.25 }}rem">
<div class="flex items-start justify-between gap-3">
<div>
<div class="font-medium text-zinc-900 dark:text-zinc-100">{{ $comment->user?->name ?? 'Unknown user' }}</div>
<div class="text-xs text-zinc-500">{{ $comment->created_at?->format('d M Y H:i') }}</div>
<div class="font-medium text-zinc-900 dark:text-zinc-100">
{{ $commentUser?->name ?? 'Unknown user' }}
</div>
<div class="text-xs text-zinc-500">
{{ $comment->created_at?->format('d M Y H:i') }}
</div>
</div>
@if ($comment->is_internal)
<flux:badge color="amber">Internal</flux:badge>
@endif
@@ -17,9 +28,9 @@
{!! $comment->body !!}
</div>
@if ($comment->attachments->isNotEmpty())
@if ($commentAttachments->isNotEmpty())
<div class="mt-3 flex flex-wrap gap-2">
@foreach ($comment->attachments as $attachment)
@foreach ($commentAttachments as $attachment)
<a href="{{ $attachment->url() }}" target="_blank" class="rounded-lg border border-zinc-200 px-3 py-1 text-xs text-zinc-600 hover:bg-zinc-50 dark:border-zinc-700 dark:text-zinc-300 dark:hover:bg-zinc-800">
{{ $attachment->original_name }}
</a>
@@ -34,6 +45,7 @@
@if (($reply_parent_id ?? null) === $comment->id)
<form wire:submit="addReply" class="mt-4 rounded-xl border border-blue-200 bg-blue-50 p-4 dark:border-blue-900 dark:bg-blue-950/40">
<x-helpdesk::html-editor wire:model="replyBody" label="Reply" rows="4" placeholder="Write a reply..." />
<div class="mt-4 flex justify-end gap-2">
<flux:button type="button" wire:click="cancelReply" variant="ghost">Cancel</flux:button>
<flux:button type="submit" variant="primary">Post reply</flux:button>
@@ -41,9 +53,12 @@
</form>
@endif
@if ($comment->replies->isNotEmpty())
@if ($commentReplies->isNotEmpty())
<div class="mt-4">
@include('helpdesk::partials.comment-thread', ['comments' => $comment->replies, 'level' => ((int) $level) + 1])
@include('helpdesk::partials.comment-thread', [
'comments' => $commentReplies,
'level' => ((int) $level) + 1,
])
</div>
@endif
</div>
+1
View File
@@ -20,6 +20,7 @@
"flatpickr": "^4.6.13",
"laravel-vite-plugin": "^3.1",
"sass-embedded": "^1.100.0",
"sortablejs": "^1.15.7",
"tailwindcss": "^4.0.7",
"vite": "^8.0.0"
},