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;
|
namespace Modules\Helpdesk\Livewire;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
use Illuminate\Contracts\View\View;
|
use Illuminate\Contracts\View\View;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Livewire\Attributes\Validate;
|
use Livewire\Attributes\Validate;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||||
use Livewire\WithFileUploads;
|
use Livewire\WithFileUploads;
|
||||||
|
use Modules\Helpdesk\Models\Comment;
|
||||||
use Modules\Helpdesk\Models\Priority;
|
use Modules\Helpdesk\Models\Priority;
|
||||||
use Modules\Helpdesk\Models\Status;
|
use Modules\Helpdesk\Models\Status;
|
||||||
use Modules\Helpdesk\Models\Ticket;
|
use Modules\Helpdesk\Models\Ticket;
|
||||||
@@ -38,7 +41,6 @@ final class TicketShow extends Component
|
|||||||
|
|
||||||
public string $replyBody = '';
|
public string $replyBody = '';
|
||||||
|
|
||||||
/** @return void */
|
|
||||||
public function mount(Ticket $ticket): void
|
public function mount(Ticket $ticket): void
|
||||||
{
|
{
|
||||||
$this->authorize('view', $ticket);
|
$this->authorize('view', $ticket);
|
||||||
@@ -47,28 +49,25 @@ final class TicketShow extends Component
|
|||||||
$this->priority_id = $this->ticket->priority_id;
|
$this->priority_id = $this->ticket->priority_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return void */
|
|
||||||
public function startReply(int $commentId): void
|
public function startReply(int $commentId): void
|
||||||
{
|
{
|
||||||
$this->reply_parent_id = $commentId;
|
$this->reply_parent_id = $commentId;
|
||||||
$this->replyBody = '';
|
$this->replyBody = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return void */
|
|
||||||
public function cancelReply(): void
|
public function cancelReply(): void
|
||||||
{
|
{
|
||||||
$this->reply_parent_id = null;
|
$this->reply_parent_id = null;
|
||||||
$this->replyBody = '';
|
$this->replyBody = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return void */
|
|
||||||
public function addComment(TicketService $tickets): void
|
public function addComment(TicketService $tickets): void
|
||||||
{
|
{
|
||||||
$this->authorize('comment', $this->ticket);
|
$this->authorize('comment', $this->ticket);
|
||||||
|
|
||||||
$this->validate([
|
$this->validate([
|
||||||
'commentBody' => ['required', 'string', 'min:2'],
|
'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);
|
$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');
|
$this->dispatch('helpdesk-comment-added');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return void */
|
|
||||||
public function addReply(TicketService $tickets): void
|
public function addReply(TicketService $tickets): void
|
||||||
{
|
{
|
||||||
$this->authorize('comment', $this->ticket);
|
$this->authorize('comment', $this->ticket);
|
||||||
@@ -95,7 +93,6 @@ final class TicketShow extends Component
|
|||||||
$this->dispatch('helpdesk-reply-added');
|
$this->dispatch('helpdesk-reply-added');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return void */
|
|
||||||
public function updateStatus(WorkflowNotificationService $workflow): void
|
public function updateStatus(WorkflowNotificationService $workflow): void
|
||||||
{
|
{
|
||||||
$this->authorize('update', $this->ticket);
|
$this->authorize('update', $this->ticket);
|
||||||
@@ -113,7 +110,6 @@ final class TicketShow extends Component
|
|||||||
$this->dispatch('helpdesk-status-updated');
|
$this->dispatch('helpdesk-status-updated');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return void */
|
|
||||||
public function updatePriority(WorkflowNotificationService $workflow): void
|
public function updatePriority(WorkflowNotificationService $workflow): void
|
||||||
{
|
{
|
||||||
$this->authorize('update', $this->ticket);
|
$this->authorize('update', $this->ticket);
|
||||||
@@ -129,7 +125,6 @@ final class TicketShow extends Component
|
|||||||
$this->dispatch('helpdesk-priority-updated');
|
$this->dispatch('helpdesk-priority-updated');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return View */
|
|
||||||
public function render(): View
|
public function render(): View
|
||||||
{
|
{
|
||||||
return view('helpdesk::livewire.ticket-show', [
|
return view('helpdesk::livewire.ticket-show', [
|
||||||
@@ -138,13 +133,48 @@ final class TicketShow extends Component
|
|||||||
])->layout('layouts.app');
|
])->layout('layouts.app');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return Ticket */
|
|
||||||
private function freshTicket(Ticket $ticket): Ticket
|
private function freshTicket(Ticket $ticket): Ticket
|
||||||
{
|
{
|
||||||
return $ticket->fresh([
|
$ticket = $ticket->fresh([
|
||||||
'owner', 'agent', 'status', 'priority', 'category', 'project', 'team',
|
'owner',
|
||||||
'comments.user', 'comments.attachments', 'comments.replies.user', 'comments.replies.attachments', 'comments.replies.replies.user',
|
'agent',
|
||||||
'attachments.user', 'gitCommits.repository',
|
'status',
|
||||||
|
'priority',
|
||||||
|
'category',
|
||||||
|
'project',
|
||||||
|
'team',
|
||||||
|
'attachments.user',
|
||||||
|
'gitCommits.repository',
|
||||||
]) ?? $ticket;
|
]) ?? $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;
|
namespace Modules\Helpdesk\Models;
|
||||||
|
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
final class Comment extends Model
|
final class Comment extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
|
||||||
|
|
||||||
protected $table = 'helpdesk_comments';
|
protected $table = 'helpdesk_comments';
|
||||||
|
|
||||||
/** @var list<string> */
|
/** @var list<string> */
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
|||||||
*/
|
*/
|
||||||
final class Ticket extends Model
|
final class Ticket extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
|
||||||
use SoftDeletes;
|
use SoftDeletes;
|
||||||
|
|
||||||
protected $table = 'helpdesk_tickets';
|
protected $table = 'helpdesk_tickets';
|
||||||
@@ -86,7 +85,9 @@ final class Ticket extends Model
|
|||||||
/** @return HasMany<Comment> */
|
/** @return HasMany<Comment> */
|
||||||
public function comments(): HasMany
|
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> */
|
/** @return HasMany<GitCommit> */
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
import './bootstrap';
|
import './bootstrap';
|
||||||
|
import Sortable from 'sortablejs';
|
||||||
|
|
||||||
|
window.Sortable = Sortable;
|
||||||
console.info('Helpdesk module assets loaded');
|
console.info('Helpdesk module assets loaded');
|
||||||
|
|||||||
@@ -2,12 +2,23 @@
|
|||||||
|
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
@foreach ($comments as $comment)
|
@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="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 class="flex items-start justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<div class="font-medium text-zinc-900 dark:text-zinc-100">{{ $comment->user?->name ?? 'Unknown user' }}</div>
|
<div class="font-medium text-zinc-900 dark:text-zinc-100">
|
||||||
<div class="text-xs text-zinc-500">{{ $comment->created_at?->format('d M Y H:i') }}</div>
|
{{ $commentUser?->name ?? 'Unknown user' }}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-zinc-500">
|
||||||
|
{{ $comment->created_at?->format('d M Y H:i') }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if ($comment->is_internal)
|
@if ($comment->is_internal)
|
||||||
<flux:badge color="amber">Internal</flux:badge>
|
<flux:badge color="amber">Internal</flux:badge>
|
||||||
@endif
|
@endif
|
||||||
@@ -17,9 +28,9 @@
|
|||||||
{!! $comment->body !!}
|
{!! $comment->body !!}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if ($comment->attachments->isNotEmpty())
|
@if ($commentAttachments->isNotEmpty())
|
||||||
<div class="mt-3 flex flex-wrap gap-2">
|
<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">
|
<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 }}
|
{{ $attachment->original_name }}
|
||||||
</a>
|
</a>
|
||||||
@@ -34,6 +45,7 @@
|
|||||||
@if (($reply_parent_id ?? null) === $comment->id)
|
@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">
|
<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..." />
|
<x-helpdesk::html-editor wire:model="replyBody" label="Reply" rows="4" placeholder="Write a reply..." />
|
||||||
|
|
||||||
<div class="mt-4 flex justify-end gap-2">
|
<div class="mt-4 flex justify-end gap-2">
|
||||||
<flux:button type="button" wire:click="cancelReply" variant="ghost">Cancel</flux:button>
|
<flux:button type="button" wire:click="cancelReply" variant="ghost">Cancel</flux:button>
|
||||||
<flux:button type="submit" variant="primary">Post reply</flux:button>
|
<flux:button type="submit" variant="primary">Post reply</flux:button>
|
||||||
@@ -41,9 +53,12 @@
|
|||||||
</form>
|
</form>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
@if ($comment->replies->isNotEmpty())
|
@if ($commentReplies->isNotEmpty())
|
||||||
<div class="mt-4">
|
<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>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
"flatpickr": "^4.6.13",
|
"flatpickr": "^4.6.13",
|
||||||
"laravel-vite-plugin": "^3.1",
|
"laravel-vite-plugin": "^3.1",
|
||||||
"sass-embedded": "^1.100.0",
|
"sass-embedded": "^1.100.0",
|
||||||
|
"sortablejs": "^1.15.7",
|
||||||
"tailwindcss": "^4.0.7",
|
"tailwindcss": "^4.0.7",
|
||||||
"vite": "^8.0.0"
|
"vite": "^8.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user