started to add indexes and fixed N+1 issue
This commit is contained in:
+65
@@ -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()
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> */
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user