From a6148f9bfd25fa9f9e6685a71dbb24c8023fbfef Mon Sep 17 00:00:00 2001 From: Mark Gregory Date: Tue, 23 Jun 2026 11:01:55 +0100 Subject: [PATCH] proper closer status --- .../Controllers/TicketControlsController.php | 28 ++++- Modules/Helpdesk/Livewire/TicketShow.php | 32 ++++- Modules/Helpdesk/README.md | 34 ++++++ .../views/livewire/ticket-create.blade.php | 109 +++++++++++------- .../views/partials/ticket-controls.blade.php | 91 ++++++++++++++- .../Tests/Feature/TicketConversationTest.php | 60 ++++++++++ Modules/Helpdesk/changelog.yaml | 23 ++++ Modules/Helpdesk/module.json | 2 +- 8 files changed, 329 insertions(+), 50 deletions(-) diff --git a/Modules/Helpdesk/Http/Controllers/TicketControlsController.php b/Modules/Helpdesk/Http/Controllers/TicketControlsController.php index b3c6e60f..607b4100 100644 --- a/Modules/Helpdesk/Http/Controllers/TicketControlsController.php +++ b/Modules/Helpdesk/Http/Controllers/TicketControlsController.php @@ -16,6 +16,7 @@ use Illuminate\Validation\Rule; use Modules\Helpdesk\Models\Status; use Modules\Helpdesk\Models\Ticket; use Modules\Helpdesk\Services\SlaService; +use Modules\Helpdesk\Services\TicketService; use Modules\Helpdesk\Services\WorkflowNotificationService; final class TicketControlsController extends Controller @@ -64,7 +65,7 @@ final class TicketControlsController extends Controller /** * Persist the selected ticket status from the ticket detail controls. */ - public function updateStatus(Request $request, Ticket $ticket, WorkflowNotificationService $workflow, SlaService $sla): RedirectResponse + public function updateStatus(Request $request, Ticket $ticket, WorkflowNotificationService $workflow, SlaService $sla, TicketService $tickets): RedirectResponse { $this->hydrateUserRelations($ticket); @@ -72,10 +73,21 @@ final class TicketControlsController extends Controller $validated = $request->validate([ 'status_id' => ['required', 'integer', Rule::exists('helpdesk_statuses', 'id')], + 'closure_comment' => ['nullable', 'string', 'min:10', 'max:5000'], + 'closure_internal' => ['nullable', 'boolean'], ]); $status = Status::query()->findOrFail((int) $validated['status_id']); + if ($status->is_closed) { + validator($validated, [ + 'closure_comment' => ['required', 'string', 'min:10', 'max:5000'], + ], [ + 'closure_comment.required' => 'A closure comment is required before closing the ticket.', + 'closure_comment.min' => 'The closure comment must explain why the ticket is being closed.', + ])->validate(); + } + $this->updateTicket($ticket, [ 'status_id' => $status->id, 'completed_at' => $status->is_closed ? now() : null, @@ -85,12 +97,24 @@ final class TicketControlsController extends Controller $this->ensureBoardContainsStatus($ticket, (int) $status->id); $fresh = $this->freshTicketForAction($ticket); + $actor = $this->actor(); if ($status->is_closed) { + $tickets->comment( + $fresh, + $actor, + trim((string) $validated['closure_comment']), + (bool) ($validated['closure_internal'] ?? false), + [], + null, + 'Closure comment' + ); + + $fresh = $this->freshTicketForAction($ticket); $sla->recordClosure($fresh); } - $workflow->dispatch($fresh, $status->is_closed ? 'ticket.closed' : 'ticket.updated', $this->actor()); + $workflow->dispatch($fresh, $status->is_closed ? 'ticket.closed' : 'ticket.updated', $actor); return redirect() ->route('helpdesk.tickets.show', $fresh) diff --git a/Modules/Helpdesk/Livewire/TicketShow.php b/Modules/Helpdesk/Livewire/TicketShow.php index ea731326..abed51e6 100644 --- a/Modules/Helpdesk/Livewire/TicketShow.php +++ b/Modules/Helpdesk/Livewire/TicketShow.php @@ -54,6 +54,10 @@ final class TicketShow extends Component public ?int $agent_id = null; + public string $closureComment = ''; + + public bool $closureInternal = false; + public ?int $reply_parent_id = null; public string $replyBody = ''; @@ -144,7 +148,7 @@ final class TicketShow extends Component $this->ticket = $this->freshTicket($this->ticket); } - public function updateStatus(WorkflowNotificationService $workflow, SlaService $sla): void + public function updateStatus(WorkflowNotificationService $workflow, SlaService $sla, TicketService $tickets): void { $this->authorize('update', $this->ticket); @@ -157,6 +161,17 @@ final class TicketShow extends Component $status = Status::query()->findOrFail($statusId); + if ($status->is_closed) { + validator([ + 'closureComment' => $this->closureComment, + ], [ + 'closureComment' => ['required', 'string', 'min:10', 'max:5000'], + ], [ + 'closureComment.required' => 'A closure comment is required before closing the ticket.', + 'closureComment.min' => 'The closure comment must explain why the ticket is being closed.', + ])->validate(); + } + $updates = [ 'status_id' => $status->id, 'completed_at' => $status->is_closed ? now() : null, @@ -175,6 +190,21 @@ final class TicketShow extends Component $this->status_id = (int) $this->ticket->status_id; if ($status->is_closed) { + $user = Auth::user(); + abort_if($user === null, 403); + + $tickets->comment( + $this->ticket, + $user, + trim($this->closureComment), + $this->closureInternal, + [], + null, + 'Closure comment' + ); + + $this->reset(['closureComment', 'closureInternal']); + $this->ticket = $this->freshTicket($this->ticket); $sla->recordClosure($this->ticket); } diff --git a/Modules/Helpdesk/README.md b/Modules/Helpdesk/README.md index 6eea33ec..0a5f5350 100644 --- a/Modules/Helpdesk/README.md +++ b/Modules/Helpdesk/README.md @@ -167,3 +167,37 @@ Helpdesk changes must preserve the established WMS/Laravel module baseline: - `changelog.yaml` must be append-only. - `module.json` version must match the latest `changelog.yaml` version. - Tests must be added or updated for workflow changes. + +## Closing a Ticket With a Mandatory Comment + +The ticket status control now enforces an auditable closure workflow. + +### How to close a ticket + +1. Open the Helpdesk ticket. +2. Open **Ticket controls**. +3. Change **Status** to a closed status, for example **Closed**. +4. Click **Update status**. +5. The **Close ticket** dialog opens if no closure comment has been entered. +6. Enter a clear closure comment explaining what was resolved or why the ticket is being closed. +7. Leave **Internal closure note** unticked when the customer/requester should see the closure response. +8. Tick **Internal closure note** only when the closure reason is staff-only. +9. Click **Close ticket**. + +### Closure comment rules + +- A closed status cannot be saved without a closure comment. +- The closure comment must be at least 10 characters. +- Public closure comments are saved as normal Helpdesk responses. +- Internal closure comments are saved as internal notes and are only visible to users allowed to see internal notes. +- The ticket `completed_at` timestamp is set when a closed status is saved successfully. +- The resolution SLA is recorded after the closure is saved. + +### Recommended closure wording + +```text +Issue resolved and verified. + +The requested work has been completed and no further action is currently required. This ticket is now closed. +``` + diff --git a/Modules/Helpdesk/Resources/views/livewire/ticket-create.blade.php b/Modules/Helpdesk/Resources/views/livewire/ticket-create.blade.php index 248b3b30..a0df77d5 100644 --- a/Modules/Helpdesk/Resources/views/livewire/ticket-create.blade.php +++ b/Modules/Helpdesk/Resources/views/livewire/ticket-create.blade.php @@ -274,7 +274,7 @@ data-dusk="ticket-body-editor" dusk="ticket-body-editor">{!! $body ?? '' !!} - + @error('body')

{{ $message }}

@@ -361,57 +361,78 @@ } diff --git a/Modules/Helpdesk/Resources/views/partials/ticket-controls.blade.php b/Modules/Helpdesk/Resources/views/partials/ticket-controls.blade.php index b157108f..27cebcdc 100644 --- a/Modules/Helpdesk/Resources/views/partials/ticket-controls.blade.php +++ b/Modules/Helpdesk/Resources/views/partials/ticket-controls.blade.php @@ -101,7 +101,8 @@
+ class="mb-4 space-y-2" + data-helpdesk-status-form> @csrf