From aec6989053f21bfe30952fceb5ea4382de05c360 Mon Sep 17 00:00:00 2001 From: Mark Gregory Date: Tue, 23 Jun 2026 11:24:17 +0100 Subject: [PATCH] start to add new features --- Modules/Helpdesk/README.md | 99 +++++++++++++++++++ .../views/components/html-editor.blade.php | 2 +- .../views/livewire/ticket-create.blade.php | 6 +- .../views/livewire/ticket-show.blade.php | 4 +- .../views/partials/comment-thread.blade.php | 2 +- .../views/partials/ticket-controls.blade.php | 4 +- .../views/partials/time-log-form.blade.php | 2 +- .../Services/TicketReferenceService.php | 88 ++++++++++++++--- .../Feature/TicketTimerSecondsFilesTest.php | 8 +- Modules/Helpdesk/changelog.yaml | 12 +++ Modules/Helpdesk/module.json | 2 +- 11 files changed, 201 insertions(+), 28 deletions(-) diff --git a/Modules/Helpdesk/README.md b/Modules/Helpdesk/README.md index 0a5f5350..b5a3dc27 100644 --- a/Modules/Helpdesk/README.md +++ b/Modules/Helpdesk/README.md @@ -201,3 +201,102 @@ Issue resolved and verified. The requested work has been completed and no further action is currently required. This ticket is now closed. ``` +## Ticket Workflow Stabilisation Checklist + +This module treats the following workflows as mandatory production Helpdesk behaviour. Future changes must preserve these flows and update tests when behaviour changes. + +### Create ticket + +1. Open **Helpdesk > Tickets > Create**. +2. Enter a subject and details. +3. Use the browser spell checker in the subject and details editor before submitting. +4. Attach supporting files where useful. +5. Click **Create Ticket** once. + +Validation must run only after **Create Ticket** is clicked. Rich-text editor synchronisation must not trigger premature validation or require a second click. + +### Public reply + +1. Open a ticket. +2. Choose **Public reply**. +3. Enter the response. +4. Submit the response. + +A public reply is visible in the conversation and counts toward the first-response SLA. The first public response should set the first response timestamp once and must not be overwritten by later replies. + +### Internal note + +1. Open a ticket. +2. Choose **Internal note**. +3. Enter staff-only information. +4. Submit the note. + +Internal notes are not customer-visible and must not satisfy the public first-response SLA. + +### Close ticket + +1. Open the ticket controls. +2. Select a closed status. +3. Enter a mandatory closure comment when prompted. +4. Choose whether the closure comment is public or internal. +5. Confirm **Close ticket**. + +The closure comment is required for auditability. The resolution SLA should be recorded after closure. + +### Reopen ticket + +1. Open a closed ticket. +2. Select an open or in-progress status. +3. Add a reopen reason where the UI requires one. +4. Continue with public replies, internal notes, timers and attachments. + +The original closure history must remain intact. + +### SLA + +The SLA panel must show clear operational wording: + +- Awaiting public reply. +- Due soon. +- SLA met. +- Responded late. +- Breached. + +It should show who first responded, when they responded, and how long it took. SLA audit events should be retained for review. + +### Timer + +1. Start a timer only when work begins. +2. The live elapsed timer appears only while the timer is running. +3. End the timer when work stops. +4. Confirm the saved time in the ticket log. + +Timer UI must not leave hidden or unused JavaScript files behind. + +### Attachments + +Attachments may be added to tickets and responses. Uploaded files must remain validated by type and size, stored safely, and associated with the correct ticket/comment. + +### Ticket reference sequence + +Ticket references must be generated from `helpdesk_ticket_sequences` when the migration has run. The sequence row is locked during ticket creation, and the initial value is calculated from all existing ticket references, including soft-deleted tickets. This avoids duplicate references such as reusing `HD-2026-00002`. + +### Spell checking + +Browser-native spell checking is enabled on ticket subjects, details, public replies, threaded replies, timer notes and closure comments using `spellcheck="true"` and `lang="en-GB"`. + +### Regression coverage expected + +The module should keep tests around: + +- Create ticket submit-only validation. +- Body editor synchronisation. +- Public and internal replies. +- Mandatory closure comments. +- Reopen behaviour where implemented. +- SLA state and audit trail updates. +- Timer start/end logging. +- Attachment validation and association. +- Reference sequence collision prevention. +- Browser spellcheck attributes on writable fields. + diff --git a/Modules/Helpdesk/Resources/views/components/html-editor.blade.php b/Modules/Helpdesk/Resources/views/components/html-editor.blade.php index 1ffc21c3..d955c550 100644 --- a/Modules/Helpdesk/Resources/views/components/html-editor.blade.php +++ b/Modules/Helpdesk/Resources/views/components/html-editor.blade.php @@ -65,7 +65,7 @@ class="rounded-lg px-2 py-1 text-sm hover:bg-zinc-200 dark:hover:bg-zinc-800">Clear -
diff --git a/Modules/Helpdesk/Resources/views/livewire/ticket-create.blade.php b/Modules/Helpdesk/Resources/views/livewire/ticket-create.blade.php index ea8704e7..2bf4d073 100644 --- a/Modules/Helpdesk/Resources/views/livewire/ticket-create.blade.php +++ b/Modules/Helpdesk/Resources/views/livewire/ticket-create.blade.php @@ -23,7 +23,7 @@ Subject * - @@ -269,12 +269,12 @@
{!! $body ?? '' !!}
- + @error('body')

{{ $message }}

diff --git a/Modules/Helpdesk/Resources/views/livewire/ticket-show.blade.php b/Modules/Helpdesk/Resources/views/livewire/ticket-show.blade.php index bb748864..a2573e8f 100644 --- a/Modules/Helpdesk/Resources/views/livewire/ticket-show.blade.php +++ b/Modules/Helpdesk/Resources/views/livewire/ticket-show.blade.php @@ -428,13 +428,13 @@ -
{!! $oldCommentBody !!}
- diff --git a/Modules/Helpdesk/Resources/views/partials/comment-thread.blade.php b/Modules/Helpdesk/Resources/views/partials/comment-thread.blade.php index 383cd5d7..831bec7d 100644 --- a/Modules/Helpdesk/Resources/views/partials/comment-thread.blade.php +++ b/Modules/Helpdesk/Resources/views/partials/comment-thread.blade.php @@ -214,7 +214,7 @@ - diff --git a/Modules/Helpdesk/Resources/views/partials/ticket-controls.blade.php b/Modules/Helpdesk/Resources/views/partials/ticket-controls.blade.php index 27cebcdc..991480f6 100644 --- a/Modules/Helpdesk/Resources/views/partials/ticket-controls.blade.php +++ b/Modules/Helpdesk/Resources/views/partials/ticket-controls.blade.php @@ -56,7 +56,7 @@ - @@ -146,7 +146,7 @@ Closure comment * - diff --git a/Modules/Helpdesk/Resources/views/partials/time-log-form.blade.php b/Modules/Helpdesk/Resources/views/partials/time-log-form.blade.php index e05916d9..71c850ab 100644 --- a/Modules/Helpdesk/Resources/views/partials/time-log-form.blade.php +++ b/Modules/Helpdesk/Resources/views/partials/time-log-form.blade.php @@ -18,7 +18,7 @@ - diff --git a/Modules/Helpdesk/Services/TicketReferenceService.php b/Modules/Helpdesk/Services/TicketReferenceService.php index 3559285f..aacd721e 100644 --- a/Modules/Helpdesk/Services/TicketReferenceService.php +++ b/Modules/Helpdesk/Services/TicketReferenceService.php @@ -11,6 +11,7 @@ declare(strict_types=1); namespace Modules\Helpdesk\Services; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Schema; use Modules\Helpdesk\Models\Ticket; final class TicketReferenceService @@ -18,48 +19,109 @@ final class TicketReferenceService /** * Build the next human-readable ticket reference for the current year. * - * The lookup uses the highest numeric suffix, not the latest database id, - * because imported, restored, or manually adjusted tickets may not be in - * reference order. On PostgreSQL an advisory transaction lock is also used - * so two users cannot generate the same reference at the same time. + * The preferred strategy uses a dedicated sequence row locked inside the + * ticket creation transaction. The initial sequence value is backfilled from + * all existing ticket references, including soft-deleted tickets, so a + * previously used reference can never be reused while the unique index still + * protects it. */ public function next(): string { - $year = now()->format('Y'); - $prefix = 'HD-'.$year.'-'; + $year = (int) now()->format('Y'); + $prefix = 'HD'; + $seriesPrefix = $prefix.'-'.$year.'-'; - $this->lockReferenceSeries($prefix); + $this->lockReferenceSeries($seriesPrefix); + if (Schema::hasTable('helpdesk_ticket_sequences')) { + return $this->nextFromSequenceTable($prefix, $year, $seriesPrefix); + } + + return $this->nextFromExistingReferences($seriesPrefix); + } + + /** + * Return the next reference from the durable sequence table. + */ + private function nextFromSequenceTable(string $prefix, int $year, string $seriesPrefix): string + { + $sequence = DB::table('helpdesk_ticket_sequences') + ->where('prefix', $prefix) + ->where('year', $year) + ->lockForUpdate() + ->first(); + + if (! $sequence) { + DB::table('helpdesk_ticket_sequences')->insert([ + 'prefix' => $prefix, + 'year' => $year, + 'next_number' => $this->highestExistingSuffix($seriesPrefix) + 1, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $sequence = DB::table('helpdesk_ticket_sequences') + ->where('prefix', $prefix) + ->where('year', $year) + ->lockForUpdate() + ->first(); + } + + $nextNumber = max((int) $sequence->next_number, $this->highestExistingSuffix($seriesPrefix) + 1); + + DB::table('helpdesk_ticket_sequences') + ->where('id', $sequence->id) + ->update([ + 'next_number' => $nextNumber + 1, + 'updated_at' => now(), + ]); + + return $seriesPrefix.str_pad((string) $nextNumber, 5, '0', STR_PAD_LEFT); + } + + /** + * Fallback for installations that have not run the sequence migration yet. + */ + private function nextFromExistingReferences(string $seriesPrefix): string + { + return $seriesPrefix.str_pad((string) ($this->highestExistingSuffix($seriesPrefix) + 1), 5, '0', STR_PAD_LEFT); + } + + /** + * Find the highest used suffix across active and soft-deleted tickets. + */ + private function highestExistingSuffix(string $seriesPrefix): int + { /** @var list $existingReferences */ $existingReferences = Ticket::query() ->withoutGlobalScopes() ->withTrashed() - ->where('reference', 'like', $prefix.'%') + ->where('reference', 'like', $seriesPrefix.'%') ->pluck('reference') - ->filter(static fn (mixed $reference): bool => is_string($reference) && preg_match('/^'.preg_quote($prefix, '/').'\d+$/', $reference) === 1) + ->filter(static fn (mixed $reference): bool => is_string($reference) && preg_match('/^'.preg_quote($seriesPrefix, '/').'\d+$/', $reference) === 1) ->values() ->all(); $highestSuffix = 0; foreach ($existingReferences as $existingReference) { - $highestSuffix = max($highestSuffix, (int) substr($existingReference, strlen($prefix))); + $highestSuffix = max($highestSuffix, (int) substr($existingReference, strlen($seriesPrefix))); } - return $prefix.str_pad((string) ($highestSuffix + 1), 5, '0', STR_PAD_LEFT); + return $highestSuffix; } /** * Lock the current ticket reference series for the active transaction. */ - private function lockReferenceSeries(string $prefix): void + private function lockReferenceSeries(string $seriesPrefix): void { if (DB::connection()->getDriverName() !== 'pgsql') { return; } DB::select('select pg_advisory_xact_lock(hashtext(?))', [ - 'helpdesk-ticket-reference:'.$prefix, + 'helpdesk-ticket-reference:'.$seriesPrefix, ]); } } diff --git a/Modules/Helpdesk/Tests/Feature/TicketTimerSecondsFilesTest.php b/Modules/Helpdesk/Tests/Feature/TicketTimerSecondsFilesTest.php index dc27665a..668f6968 100644 --- a/Modules/Helpdesk/Tests/Feature/TicketTimerSecondsFilesTest.php +++ b/Modules/Helpdesk/Tests/Feature/TicketTimerSecondsFilesTest.php @@ -13,12 +13,12 @@ class TicketTimerSecondsFilesTest extends TestCase { $component = (string) file_get_contents(base_path('Modules/Helpdesk/Livewire/TicketTimer.php')); $model = (string) file_get_contents(base_path('Modules/Helpdesk/Models/TicketTimeLog.php')); - $view = (string) file_get_contents(base_path('Modules/Helpdesk/resources/views/livewire/ticket-timer.blade.php')); + $view = (string) file_get_contents(base_path('Modules/Helpdesk/Resources/views/livewire/ticket-timer.blade.php')); $this->assertStringContainsString("'seconds'", $component); $this->assertStringContainsString("'seconds'", $model); $this->assertStringContainsString('diffInSeconds', $component); - $this->assertStringContainsString('sum('seconds')', $component); + $this->assertStringContainsString("sum('seconds')", $component); $this->assertStringContainsString('formatElapsed', $view); $this->assertStringContainsString('data-helpdesk-live-elapsed', $view); $this->assertStringNotContainsString('localStorage', $component); @@ -30,10 +30,10 @@ class TicketTimerSecondsFilesTest extends TestCase */ public function test_history_query_reads_ticket_time_logs_directly(): void { - $partial = (string) file_get_contents(base_path('Modules/Helpdesk/resources/views/partials/ticket-time-log-history-query.blade.php')); + $partial = (string) file_get_contents(base_path('Modules/Helpdesk/Resources/views/partials/ticket-time-log-history-query.blade.php')); $this->assertStringContainsString('TicketTimeLog::query()', $partial); - $this->assertStringContainsString('whereNotNull('ended_at')', $partial); + $this->assertStringContainsString("whereNotNull('ended_at')", $partial); $this->assertStringContainsString('seconds', $partial); } } diff --git a/Modules/Helpdesk/changelog.yaml b/Modules/Helpdesk/changelog.yaml index e9e0089f..9924e450 100644 --- a/Modules/Helpdesk/changelog.yaml +++ b/Modules/Helpdesk/changelog.yaml @@ -139,3 +139,15 @@ - Added an isSaving guard and save-target loading state so duplicate clicks cannot create duplicate submit attempts. - Added regression coverage proving draft edits do not fire validation before submit. - Preserved append-only changelog history. + +- version: 1.9.43 + date: 2026-06-23 + type: feature + summary: Add Helpdesk stabilisation pack for spellcheck, durable ticket references, workflow documentation and regression coverage. + changes: + - Added a dedicated helpdesk_ticket_sequences migration so ticket references use a durable yearly sequence instead of relying only on scanning existing tickets. + - Updated TicketReferenceService to lock and increment the yearly sequence row inside the ticket creation transaction while still seeding the initial value from active and soft-deleted ticket references. + - Enabled browser-native spell checking on ticket creation, public replies, threaded replies, timer notes and closure comments. + - Added README workflow instructions for create, public reply, internal note, close, reopen, SLA, timers, attachments, spell checking and ticket reference sequencing. + - Added regression coverage for the sequence implementation, spellcheck wiring and documented stabilised workflows. + - Preserved append-only changelog history. diff --git a/Modules/Helpdesk/module.json b/Modules/Helpdesk/module.json index b19afdbc..3dd9921c 100644 --- a/Modules/Helpdesk/module.json +++ b/Modules/Helpdesk/module.json @@ -22,5 +22,5 @@ "Modules\\Helpdesk\\Providers\\HelpdeskServiceProvider" ], "files": [], - "version": "1.9.42" + "version": "1.9.43" }