diff --git a/Modules/Helpdesk/Database/Migrations/2026_06_12_124726_helpdesk_performance_indexes.php b/Modules/Helpdesk/Database/Migrations/2026_06_12_124726_helpdesk_performance_indexes.php new file mode 100644 index 00000000..3acfcccc --- /dev/null +++ b/Modules/Helpdesk/Database/Migrations/2026_06_12_124726_helpdesk_performance_indexes.php @@ -0,0 +1,65 @@ +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() + { + // + } +} diff --git a/Modules/Helpdesk/Livewire/TicketShow.php b/Modules/Helpdesk/Livewire/TicketShow.php index a238bed4..e210ba71 100644 --- a/Modules/Helpdesk/Livewire/TicketShow.php +++ b/Modules/Helpdesk/Livewire/TicketShow.php @@ -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; + + }); + } } diff --git a/Modules/Helpdesk/Models/Comment.php b/Modules/Helpdesk/Models/Comment.php index 581fbe96..7c2a73a8 100644 --- a/Modules/Helpdesk/Models/Comment.php +++ b/Modules/Helpdesk/Models/Comment.php @@ -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 */ diff --git a/Modules/Helpdesk/Models/Ticket.php b/Modules/Helpdesk/Models/Ticket.php index 3635d3ac..b4fe81da 100644 --- a/Modules/Helpdesk/Models/Ticket.php +++ b/Modules/Helpdesk/Models/Ticket.php @@ -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 */ 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 */ diff --git a/Modules/Helpdesk/Resources/assets/js/app.js b/Modules/Helpdesk/Resources/assets/js/app.js index 919f48df..7ba07a91 100644 --- a/Modules/Helpdesk/Resources/assets/js/app.js +++ b/Modules/Helpdesk/Resources/assets/js/app.js @@ -1,3 +1,5 @@ import './bootstrap'; +import Sortable from 'sortablejs'; +window.Sortable = Sortable; console.info('Helpdesk module assets loaded'); diff --git a/Modules/Helpdesk/Resources/views/partials/comment-thread.blade.php b/Modules/Helpdesk/Resources/views/partials/comment-thread.blade.php index e5fee42d..40700cc0 100644 --- a/Modules/Helpdesk/Resources/views/partials/comment-thread.blade.php +++ b/Modules/Helpdesk/Resources/views/partials/comment-thread.blade.php @@ -2,12 +2,23 @@
@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 +
-
{{ $comment->user?->name ?? 'Unknown user' }}
-
{{ $comment->created_at?->format('d M Y H:i') }}
+
+ {{ $commentUser?->name ?? 'Unknown user' }} +
+
+ {{ $comment->created_at?->format('d M Y H:i') }} +
+ @if ($comment->is_internal) Internal @endif @@ -17,9 +28,9 @@ {!! $comment->body !!}
- @if ($comment->attachments->isNotEmpty()) + @if ($commentAttachments->isNotEmpty())
- @foreach ($comment->attachments as $attachment) + @foreach ($commentAttachments as $attachment) {{ $attachment->original_name }} @@ -34,6 +45,7 @@ @if (($reply_parent_id ?? null) === $comment->id)
+
Cancel Post reply @@ -41,9 +53,12 @@ @endif - @if ($comment->replies->isNotEmpty()) + @if ($commentReplies->isNotEmpty())
- @include('helpdesk::partials.comment-thread', ['comments' => $comment->replies, 'level' => ((int) $level) + 1]) + @include('helpdesk::partials.comment-thread', [ + 'comments' => $commentReplies, + 'level' => ((int) $level) + 1, + ])
@endif
diff --git a/package.json b/package.json index 1b0e62db..b92faa93 100644 --- a/package.json +++ b/package.json @@ -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" },