start to add new features
This commit is contained in:
@@ -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.
|
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.
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,7 @@
|
|||||||
class="rounded-lg px-2 py-1 text-sm hover:bg-zinc-200 dark:hover:bg-zinc-800">Clear</button>
|
class="rounded-lg px-2 py-1 text-sm hover:bg-zinc-200 dark:hover:bg-zinc-800">Clear</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div x-ref="editor" contenteditable="true" x-on:input.debounce.150ms="sync" x-on:keyup="sync"
|
<div x-ref="editor" contenteditable="true" spellcheck="true" lang="en-GB" x-on:input.debounce.150ms="sync" x-on:keyup="sync"
|
||||||
x-on:paste="$nextTick(() => sync())" x-on:blur="sync"
|
x-on:paste="$nextTick(() => sync())" x-on:blur="sync"
|
||||||
class="prose prose-sm min-h-[{{ (int) $rows * 28 }}px] max-w-none bg-white p-4 text-sm text-zinc-900 outline-none dark:prose-invert dark:bg-zinc-950 dark:text-zinc-100"
|
class="prose prose-sm min-h-[{{ (int) $rows * 28 }}px] max-w-none bg-white p-4 text-sm text-zinc-900 outline-none dark:prose-invert dark:bg-zinc-950 dark:text-zinc-100"
|
||||||
data-placeholder="{{ $placeholder }}"></div>
|
data-placeholder="{{ $placeholder }}"></div>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
Subject <span class="helpdesk-required-star">*</span>
|
Subject <span class="helpdesk-required-star">*</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<input id="subject" type="text" maxlength="180" wire:model.defer="subject"
|
<input id="subject" type="text" spellcheck="true" lang="en-GB" maxlength="180" wire:model.defer="subject"
|
||||||
class="mt-2 block h-14 w-full max-w-4xl rounded-xl border border-zinc-700 bg-zinc-950 px-4 text-base font-medium text-zinc-100 shadow-sm outline-none transition-colors duration-150 placeholder:text-zinc-500 focus:border-blue-500 focus:bg-zinc-950 focus:ring-2 focus:ring-blue-500/40"
|
class="mt-2 block h-14 w-full max-w-4xl rounded-xl border border-zinc-700 bg-zinc-950 px-4 text-base font-medium text-zinc-100 shadow-sm outline-none transition-colors duration-150 placeholder:text-zinc-500 focus:border-blue-500 focus:bg-zinc-950 focus:ring-2 focus:ring-blue-500/40"
|
||||||
data-dusk="ticket-subject" dusk="ticket-subject" autocomplete="off" />
|
data-dusk="ticket-subject" dusk="ticket-subject" autocomplete="off" />
|
||||||
|
|
||||||
@@ -269,12 +269,12 @@
|
|||||||
|
|
||||||
<div id="body-editor"
|
<div id="body-editor"
|
||||||
class="min-h-[18rem] w-full px-4 py-3 text-sm leading-6 text-zinc-100 outline-none empty:before:text-zinc-500 empty:before:content-[attr(data-placeholder)]"
|
class="min-h-[18rem] w-full px-4 py-3 text-sm leading-6 text-zinc-100 outline-none empty:before:text-zinc-500 empty:before:content-[attr(data-placeholder)]"
|
||||||
contenteditable="true" data-helpdesk-details-editor
|
contenteditable="true" spellcheck="true" lang="en-GB" data-helpdesk-details-editor
|
||||||
data-placeholder="Describe the issue, expected outcome, steps to reproduce and any useful context..."
|
data-placeholder="Describe the issue, expected outcome, steps to reproduce and any useful context..."
|
||||||
data-dusk="ticket-body-editor" dusk="ticket-body-editor">{!! $body ?? '' !!}</div>
|
data-dusk="ticket-body-editor" dusk="ticket-body-editor">{!! $body ?? '' !!}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<textarea id="body" wire:model="body" class="sr-only" tabindex="-1" aria-hidden="true"></textarea>
|
<textarea id="body" spellcheck="true" lang="en-GB" wire:model="body" class="sr-only" tabindex="-1" aria-hidden="true"></textarea>
|
||||||
|
|
||||||
@error('body')
|
@error('body')
|
||||||
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
|
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
|
||||||
|
|||||||
@@ -428,13 +428,13 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="ticket-comment-body-editor" dusk="ticket-comment-editor" contenteditable="true"
|
<div id="ticket-comment-body-editor" dusk="ticket-comment-editor" contenteditable="true" spellcheck="true" lang="en-GB"
|
||||||
role="textbox" aria-multiline="true" data-helpdesk-rich-editor
|
role="textbox" aria-multiline="true" data-helpdesk-rich-editor
|
||||||
data-helpdesk-sync-target="ticket-comment-body"
|
data-helpdesk-sync-target="ticket-comment-body"
|
||||||
class="prose block min-h-[12rem] w-full max-w-none rounded-xl border border-zinc-300 bg-white p-4 text-sm leading-6 text-zinc-900 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100 dark:prose-invert">
|
class="prose block min-h-[12rem] w-full max-w-none rounded-xl border border-zinc-300 bg-white p-4 text-sm leading-6 text-zinc-900 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100 dark:prose-invert">
|
||||||
{!! $oldCommentBody !!}</div>
|
{!! $oldCommentBody !!}</div>
|
||||||
|
|
||||||
<textarea dusk="comment-body" data-dusk="comment-body" id="ticket-comment-body"
|
<textarea dusk="comment-body" spellcheck="true" lang="en-GB" data-dusk="comment-body" id="ticket-comment-body"
|
||||||
name="commentBody" required
|
name="commentBody" required
|
||||||
class="sr-only" tabindex="-1" aria-hidden="true">{{ $oldCommentBody }}</textarea>
|
class="sr-only" tabindex="-1" aria-hidden="true">{{ $oldCommentBody }}</textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -214,7 +214,7 @@
|
|||||||
<button type="button" data-helpdesk-wrap="<code>" data-helpdesk-wrap-end="</code>"
|
<button type="button" data-helpdesk-wrap="<code>" data-helpdesk-wrap-end="</code>"
|
||||||
class="rounded-lg border px-2 py-1 text-xs">Code</button>
|
class="rounded-lg border px-2 py-1 text-xs">Code</button>
|
||||||
</div>
|
</div>
|
||||||
<textarea id="reply-body-{{ $comment->id }}" dusk="reply-body-{{ $comment->id }}" name="commentBody" rows="4"
|
<textarea spellcheck="true" lang="en-GB" id="reply-body-{{ $comment->id }}" dusk="reply-body-{{ $comment->id }}" name="commentBody" rows="4"
|
||||||
required
|
required
|
||||||
class="block w-full rounded-xl border border-zinc-300 bg-white p-3 text-sm text-zinc-900 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100"
|
class="block w-full rounded-xl border border-zinc-300 bg-white p-3 text-sm text-zinc-900 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100"
|
||||||
placeholder="Write a threaded Lotus Notes-style response..."></textarea>
|
placeholder="Write a threaded Lotus Notes-style response..."></textarea>
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<textarea name="description" rows="2" placeholder="Optional work note"
|
<textarea spellcheck="true" lang="en-GB" name="description" rows="2" placeholder="Optional work note"
|
||||||
class="block w-full rounded-lg border border-zinc-300 bg-white p-2 text-xs font-semibold text-zinc-900 shadow-sm focus:border-emerald-500 focus:ring-emerald-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100"
|
class="block w-full rounded-lg border border-zinc-300 bg-white p-2 text-xs font-semibold text-zinc-900 shadow-sm focus:border-emerald-500 focus:ring-emerald-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100"
|
||||||
data-dusk="ticket-time-description"></textarea>
|
data-dusk="ticket-time-description"></textarea>
|
||||||
|
|
||||||
@@ -146,7 +146,7 @@
|
|||||||
Closure comment <span class="text-red-600">*</span>
|
Closure comment <span class="text-red-600">*</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<textarea id="closure_comment" name="closure_comment" rows="5" minlength="10" maxlength="5000"
|
<textarea id="closure_comment" spellcheck="true" lang="en-GB" name="closure_comment" rows="5" minlength="10" maxlength="5000"
|
||||||
placeholder="Explain what was completed, verified, or agreed before closing this ticket."
|
placeholder="Explain what was completed, verified, or agreed before closing this ticket."
|
||||||
class="block w-full rounded-xl border border-zinc-300 bg-white p-3 text-sm font-semibold text-zinc-900 shadow-sm focus:border-red-500 focus:ring-red-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100"
|
class="block w-full rounded-xl border border-zinc-300 bg-white p-3 text-sm font-semibold text-zinc-900 shadow-sm focus:border-red-500 focus:ring-red-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100"
|
||||||
data-helpdesk-closure-comment>{{ old('closure_comment') }}</textarea>
|
data-helpdesk-closure-comment>{{ old('closure_comment') }}</textarea>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<textarea name="description" rows="2" placeholder="Optional work note"
|
<textarea spellcheck="true" lang="en-GB" name="description" rows="2" placeholder="Optional work note"
|
||||||
class="block w-full rounded-lg border border-zinc-300 bg-white p-2 text-xs font-semibold text-zinc-900 shadow-sm focus:border-emerald-500 focus:ring-emerald-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100"
|
class="block w-full rounded-lg border border-zinc-300 bg-white p-2 text-xs font-semibold text-zinc-900 shadow-sm focus:border-emerald-500 focus:ring-emerald-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100"
|
||||||
data-dusk="ticket-time-description">{{ old('description') }}</textarea>
|
data-dusk="ticket-time-description">{{ old('description') }}</textarea>
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ declare(strict_types=1);
|
|||||||
namespace Modules\Helpdesk\Services;
|
namespace Modules\Helpdesk\Services;
|
||||||
|
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
use Modules\Helpdesk\Models\Ticket;
|
use Modules\Helpdesk\Models\Ticket;
|
||||||
|
|
||||||
final class TicketReferenceService
|
final class TicketReferenceService
|
||||||
@@ -18,48 +19,109 @@ final class TicketReferenceService
|
|||||||
/**
|
/**
|
||||||
* Build the next human-readable ticket reference for the current year.
|
* Build the next human-readable ticket reference for the current year.
|
||||||
*
|
*
|
||||||
* The lookup uses the highest numeric suffix, not the latest database id,
|
* The preferred strategy uses a dedicated sequence row locked inside the
|
||||||
* because imported, restored, or manually adjusted tickets may not be in
|
* ticket creation transaction. The initial sequence value is backfilled from
|
||||||
* reference order. On PostgreSQL an advisory transaction lock is also used
|
* all existing ticket references, including soft-deleted tickets, so a
|
||||||
* so two users cannot generate the same reference at the same time.
|
* previously used reference can never be reused while the unique index still
|
||||||
|
* protects it.
|
||||||
*/
|
*/
|
||||||
public function next(): string
|
public function next(): string
|
||||||
{
|
{
|
||||||
$year = now()->format('Y');
|
$year = (int) now()->format('Y');
|
||||||
$prefix = 'HD-'.$year.'-';
|
$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<string> $existingReferences */
|
/** @var list<string> $existingReferences */
|
||||||
$existingReferences = Ticket::query()
|
$existingReferences = Ticket::query()
|
||||||
->withoutGlobalScopes()
|
->withoutGlobalScopes()
|
||||||
->withTrashed()
|
->withTrashed()
|
||||||
->where('reference', 'like', $prefix.'%')
|
->where('reference', 'like', $seriesPrefix.'%')
|
||||||
->pluck('reference')
|
->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()
|
->values()
|
||||||
->all();
|
->all();
|
||||||
|
|
||||||
$highestSuffix = 0;
|
$highestSuffix = 0;
|
||||||
|
|
||||||
foreach ($existingReferences as $existingReference) {
|
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.
|
* 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') {
|
if (DB::connection()->getDriverName() !== 'pgsql') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
DB::select('select pg_advisory_xact_lock(hashtext(?))', [
|
DB::select('select pg_advisory_xact_lock(hashtext(?))', [
|
||||||
'helpdesk-ticket-reference:'.$prefix,
|
'helpdesk-ticket-reference:'.$seriesPrefix,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ class TicketTimerSecondsFilesTest extends TestCase
|
|||||||
{
|
{
|
||||||
$component = (string) file_get_contents(base_path('Modules/Helpdesk/Livewire/TicketTimer.php'));
|
$component = (string) file_get_contents(base_path('Modules/Helpdesk/Livewire/TicketTimer.php'));
|
||||||
$model = (string) file_get_contents(base_path('Modules/Helpdesk/Models/TicketTimeLog.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'", $component);
|
||||||
$this->assertStringContainsString("'seconds'", $model);
|
$this->assertStringContainsString("'seconds'", $model);
|
||||||
$this->assertStringContainsString('diffInSeconds', $component);
|
$this->assertStringContainsString('diffInSeconds', $component);
|
||||||
$this->assertStringContainsString('sum('seconds')', $component);
|
$this->assertStringContainsString("sum('seconds')", $component);
|
||||||
$this->assertStringContainsString('formatElapsed', $view);
|
$this->assertStringContainsString('formatElapsed', $view);
|
||||||
$this->assertStringContainsString('data-helpdesk-live-elapsed', $view);
|
$this->assertStringContainsString('data-helpdesk-live-elapsed', $view);
|
||||||
$this->assertStringNotContainsString('localStorage', $component);
|
$this->assertStringNotContainsString('localStorage', $component);
|
||||||
@@ -30,10 +30,10 @@ class TicketTimerSecondsFilesTest extends TestCase
|
|||||||
*/
|
*/
|
||||||
public function test_history_query_reads_ticket_time_logs_directly(): void
|
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('TicketTimeLog::query()', $partial);
|
||||||
$this->assertStringContainsString('whereNotNull('ended_at')', $partial);
|
$this->assertStringContainsString("whereNotNull('ended_at')", $partial);
|
||||||
$this->assertStringContainsString('seconds', $partial);
|
$this->assertStringContainsString('seconds', $partial);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,3 +139,15 @@
|
|||||||
- Added an isSaving guard and save-target loading state so duplicate clicks cannot create duplicate submit attempts.
|
- 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.
|
- Added regression coverage proving draft edits do not fire validation before submit.
|
||||||
- Preserved append-only changelog history.
|
- 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.
|
||||||
|
|||||||
@@ -22,5 +22,5 @@
|
|||||||
"Modules\\Helpdesk\\Providers\\HelpdeskServiceProvider"
|
"Modules\\Helpdesk\\Providers\\HelpdeskServiceProvider"
|
||||||
],
|
],
|
||||||
"files": [],
|
"files": [],
|
||||||
"version": "1.9.42"
|
"version": "1.9.43"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user