diff --git a/Modules/Helpdesk/Database/Migrations/2026_06_17_111500_add_subject_to_helpdesk_comments.php b/Modules/Helpdesk/Database/Migrations/2026_06_17_111500_add_subject_to_helpdesk_comments.php new file mode 100644 index 00000000..4e37817d --- /dev/null +++ b/Modules/Helpdesk/Database/Migrations/2026_06_17_111500_add_subject_to_helpdesk_comments.php @@ -0,0 +1,53 @@ +string('subject', 180)->nullable()->after('user_id'); + } + }); + + DB::table('helpdesk_comments') + ->whereNull('subject') + ->orWhere('subject', '') + ->orderBy('id') + ->select(['id', 'parent_id', 'body', 'body_text', 'is_internal']) + ->chunkById(100, static function ($comments): void { + foreach ($comments as $comment) { + $plain = trim(preg_replace('/\s+/', ' ', strip_tags((string) ($comment->body_text ?: $comment->body))) ?: ''); + $subject = (bool) $comment->is_internal + ? 'Internal note' + : ((int) ($comment->parent_id ?? 0) > 0 ? 'RE: ' : '').Str::limit($plain !== '' ? $plain : 'Response', 120, ''); + + DB::table('helpdesk_comments') + ->where('id', $comment->id) + ->update(['subject' => $subject]); + } + }); + + Schema::table('helpdesk_comments', static function (Blueprint $table): void { + $table->index(['ticket_id', 'subject'], 'helpdesk_comments_ticket_subject_index'); + }); + } + + public function down(): void + { + Schema::table('helpdesk_comments', static function (Blueprint $table): void { + if (Schema::hasColumn('helpdesk_comments', 'subject')) { + $table->dropIndex('helpdesk_comments_ticket_subject_index'); + $table->dropColumn('subject'); + } + }); + } +}; diff --git a/Modules/Helpdesk/Http/Controllers/TicketCommentStoreController.php b/Modules/Helpdesk/Http/Controllers/TicketCommentStoreController.php index 90a16fa1..75556d36 100644 --- a/Modules/Helpdesk/Http/Controllers/TicketCommentStoreController.php +++ b/Modules/Helpdesk/Http/Controllers/TicketCommentStoreController.php @@ -32,6 +32,7 @@ final class TicketCommentStoreController extends Controller $allowedMimes = (array) config('helpdesk.allowed_mimes', ['jpg', 'jpeg', 'png', 'pdf', 'txt', 'log', 'zip']); $validated = $request->validate([ + 'subject' => ['nullable', 'string', 'max:180'], 'commentBody' => ['nullable', 'string'], 'internal' => ['nullable', 'boolean'], 'parent_id' => [ @@ -48,16 +49,24 @@ final class TicketCommentStoreController extends Controller abort_if($user === null, 403); $body = trim((string) ($validated['commentBody'] ?? '')); + $subject = trim((string) ($validated['subject'] ?? '')); - if ($body === '' && filled($validated['canned_response_id'] ?? null)) { + if (filled($validated['canned_response_id'] ?? null)) { $response = CannedResponse::query() ->where('is_active', true) ->findOrFail((int) $validated['canned_response_id']); - $body = trim($renderer->render($response->body, $ticket, $user)); + if ($body === '') { + $body = trim($renderer->render($response->body, $ticket, $user)); + } + + if ($subject === '') { + $subject = (string) $response->name; + } } $body = $this->normaliseHtmlBody($body); + $subject = $this->normaliseSubject($subject, $body, filled($validated['parent_id'] ?? null)); validator(['commentBody' => $body], [ 'commentBody' => ['required', 'string', 'min:2'], @@ -73,12 +82,34 @@ final class TicketCommentStoreController extends Controller internal: (bool) ($validated['internal'] ?? false) && $this->userCanCreateInternalNote($user), files: $uploadedFiles, parentId: filled($validated['parent_id'] ?? null) ? (int) $validated['parent_id'] : null, + subject: $subject, ); return redirect() ->route('helpdesk.tickets.show', $ticket) ->with('ticket-comment-added', filled($validated['parent_id'] ?? null) ? 'Reply posted.' : 'Comment posted.'); } + + /** + * Derive a user-friendly Lotus Notes style response subject. + */ + private function normaliseSubject(string $subject, string $body, bool $isReply): string + { + $subject = trim(strip_tags(html_entity_decode($subject, ENT_QUOTES | ENT_HTML5, 'UTF-8'))); + + if ($subject !== '') { + return Str::limit($subject, 180, ''); + } + + $plain = trim(preg_replace('/\s+/', ' ', strip_tags(html_entity_decode($body, ENT_QUOTES | ENT_HTML5, 'UTF-8'))) ?: ''); + + if ($plain === '') { + return $isReply ? 'RE: Response' : 'Response'; + } + + return ($isReply ? 'RE: ' : '').Str::limit($plain, 120, ''); + } + /** * Keep supported formatting tags but strip scripts and unsafe inline handlers. */ diff --git a/Modules/Helpdesk/Models/Comment.php b/Modules/Helpdesk/Models/Comment.php index 20a1d4c5..6d33816a 100644 --- a/Modules/Helpdesk/Models/Comment.php +++ b/Modules/Helpdesk/Models/Comment.php @@ -18,6 +18,7 @@ final class Comment extends Model 'ticket_id', 'parent_id', 'user_id', + 'subject', 'body', 'body_text', 'is_internal', diff --git a/Modules/Helpdesk/Resources/views/livewire/ticket-show.blade.php b/Modules/Helpdesk/Resources/views/livewire/ticket-show.blade.php index 837e1ae3..1d3c1810 100644 --- a/Modules/Helpdesk/Resources/views/livewire/ticket-show.blade.php +++ b/Modules/Helpdesk/Resources/views/livewire/ticket-show.blade.php @@ -61,7 +61,7 @@
Lotus Notes-style document responses and responses to responses.
+Conversation
+Lotus Notes-style document responses and replies.
+