phases one of time logged now with log display

This commit is contained in:
Mark Gregory
2026-06-22 13:48:59 +01:00
parent 052bae718e
commit fcd60471be
13 changed files with 716 additions and 421 deletions
@@ -0,0 +1,54 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Add persistent timer fields to the existing helpdesk time log table.
*/
public function up(): void
{
foreach (['helpdesk_time_logs', 'time_logs'] as $tableName) {
if (! Schema::hasTable($tableName)) {
continue;
}
Schema::table($tableName, function (Blueprint $table) use ($tableName): void {
if (! Schema::hasColumn($tableName, 'started_at')) {
$table->timestamp('started_at')->nullable()->after('logged_at');
}
if (! Schema::hasColumn($tableName, 'ended_at')) {
$table->timestamp('ended_at')->nullable()->after('started_at');
}
if (! Schema::hasColumn($tableName, 'duration_seconds')) {
$table->unsignedInteger('duration_seconds')->default(0)->after('minutes');
}
});
}
}
/**
* Remove persistent timer fields from the existing helpdesk time log table.
*/
public function down(): void
{
foreach (['helpdesk_time_logs', 'time_logs'] as $tableName) {
if (! Schema::hasTable($tableName)) {
continue;
}
Schema::table($tableName, function (Blueprint $table) use ($tableName): void {
foreach (['duration_seconds', 'ended_at', 'started_at'] as $column) {
if (Schema::hasColumn($tableName, $column)) {
$table->dropColumn($column);
}
}
});
}
}
};
@@ -0,0 +1,41 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
if (Schema::hasTable('helpdesk_ticket_time_logs')) {
return;
}
Schema::create('helpdesk_ticket_time_logs', function (Blueprint $table): void {
$table->id();
$table->foreignId('ticket_id')->constrained('helpdesk_tickets')->cascadeOnDelete();
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
$table->timestamp('started_at');
$table->timestamp('ended_at')->nullable();
$table->unsignedInteger('duration_seconds')->default(0);
$table->unsignedInteger('minutes')->default(0);
$table->text('description')->nullable();
$table->timestamps();
$table->index(['ticket_id', 'user_id', 'ended_at'], 'helpdesk_timer_active_idx');
$table->index(['ticket_id', 'started_at'], 'helpdesk_timer_ticket_started_idx');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('helpdesk_ticket_time_logs');
}
};
@@ -0,0 +1,45 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Add the logged_at timestamp used by ticket time history and timer-created entries.
*/
public function up(): void
{
if (! Schema::hasTable('helpdesk_ticket_time_logs')) {
return;
}
if (Schema::hasColumn('helpdesk_ticket_time_logs', 'logged_at')) {
return;
}
Schema::table('helpdesk_ticket_time_logs', function (Blueprint $table): void {
$table->timestamp('logged_at')->nullable()->after('ended_at')->index();
});
}
/**
* Remove the logged_at timestamp if this migration is rolled back.
*/
public function down(): void
{
if (! Schema::hasTable('helpdesk_ticket_time_logs')) {
return;
}
if (! Schema::hasColumn('helpdesk_ticket_time_logs', 'logged_at')) {
return;
}
Schema::table('helpdesk_ticket_time_logs', function (Blueprint $table): void {
$table->dropIndex(['logged_at']);
$table->dropColumn('logged_at');
});
}
};
@@ -0,0 +1,49 @@
<?php
namespace Modules\Helpdesk\Tests\Feature;
use Tests\TestCase;
class TicketTimerFilesTest extends TestCase
{
/**
* The Livewire timer view must keep its root element first.
*/
public function test_ticket_timer_view_does_not_start_with_a_style_tag(): void
{
$view = trim((string) file_get_contents(base_path('Modules/Helpdesk/resources/views/livewire/ticket-timer.blade.php')));
$this->assertStringStartsWith('<div', $view);
$this->assertFalse(
str_starts_with($view, '<style'),
'The ticket timer Livewire view must not start with a <style> tag.'
);
}
/**
* The timer must be database backed rather than session/localStorage backed.
*/
public function test_ticket_timer_is_not_session_or_local_storage_backed(): void
{
$component = (string) file_get_contents(base_path('Modules/Helpdesk/Livewire/TicketTimer.php'));
$view = (string) file_get_contents(base_path('Modules/Helpdesk/resources/views/livewire/ticket-timer.blade.php'));
$this->assertStringNotContainsString('session(', $component);
$this->assertStringNotContainsString('localStorage', $component);
$this->assertStringNotContainsString('localStorage', $view);
$this->assertStringContainsString('whereNull', $component);
$this->assertStringContainsString('activeUserTimerQuery', $component);
$this->assertStringContainsString('TicketTimeLog::query()', $component);
$this->assertStringNotContainsString('->timeLogs()', $component);
}
/**
* The ticket page must include the user-specific timer partial.
*/
public function test_ticket_show_timer_partial_exists(): void
{
$this->assertFileExists(base_path('Modules/Helpdesk/resources/views/partials/ticket-time-timer.blade.php'));
$this->assertFileExists(base_path('Modules/Helpdesk/resources/views/livewire/ticket-timer.blade.php'));
$this->assertFileExists(base_path('Modules/Helpdesk/Livewire/TicketTimer.php'));
}
}
+218
View File
@@ -0,0 +1,218 @@
<?php
namespace Modules\Helpdesk\Livewire;
use Carbon\Carbon;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\QueryException;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;
use Livewire\Component;
use Modules\Helpdesk\Models\Ticket;
use Modules\Helpdesk\Models\TicketTimeLog;
class TicketTimer extends Component
{
public Ticket $ticket;
public ?TicketTimeLog $activeTimer = null;
public string $description = '';
public ?string $message = null;
public int $elapsedSeconds = 0;
/**
* Mount the timer for the current ticket.
*/
public function mount(Ticket $ticket): void
{
$this->ticket = $ticket;
$this->refreshActiveTimer();
}
/**
* Start a database-backed timer for the authenticated user.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function startTimer(): void
{
$user = $this->authenticatedUser();
if ($this->userHasAnyActiveTimer($user)) {
throw ValidationException::withMessages([
'timer' => 'You already have a running timer. End the current timer before starting another one.',
]);
}
$startedAt = Carbon::now();
try {
TicketTimeLog::query()->create([
'ticket_id' => $this->ticket->getKey(),
'user_id' => $user->getAuthIdentifier(),
'minutes' => 0,
'description' => $this->normalisedDescription(),
'started_at' => $startedAt,
'ended_at' => null,
'logged_at' => $startedAt,
]);
} catch (QueryException $exception) {
if ($this->isUniqueTimerConstraintViolation($exception)) {
throw ValidationException::withMessages([
'timer' => 'You already have a running timer. End the current timer before starting another one.',
]);
}
throw $exception;
}
$this->description = '';
$this->message = 'Timer started.';
$this->refreshActiveTimer();
$this->dispatch('timer-started');
}
/**
* End the active timer and add the elapsed minutes to the ticket total.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function endTimer(): void
{
$user = $this->authenticatedUser();
$timer = $this->activeTimerQuery($user)->first();
if (! $timer instanceof TicketTimeLog) {
throw ValidationException::withMessages([
'timer' => 'There is no running timer to end.',
]);
}
$endedAt = Carbon::now();
$minutes = max(1, (int) ceil($timer->started_at->diffInSeconds($endedAt) / 60));
$description = trim($this->description) !== '' ? trim($this->description) : $timer->description;
$timer->forceFill([
'ended_at' => $endedAt,
'minutes' => $minutes,
'description' => $description,
'logged_at' => $endedAt,
])->save();
$this->refreshTicketTimeSpentMinutes();
$this->description = '';
$this->message = 'Timer ended and time was added to the ticket.';
$this->refreshActiveTimer();
$this->dispatch('timer-ended');
}
/**
* Render the ticket timer view.
*/
public function render()
{
return view('helpdesk::livewire.ticket-timer');
}
/**
* Refresh the active timer and elapsed seconds from the database.
*/
public function refreshActiveTimer(): void
{
$user = Auth::user();
if (! $user instanceof Authenticatable) {
$this->activeTimer = null;
$this->elapsedSeconds = 0;
return;
}
$this->activeTimer = $this->activeTimerQuery($user)->first();
$this->elapsedSeconds = $this->activeTimer instanceof TicketTimeLog
? $this->activeTimer->started_at->diffInSeconds(Carbon::now())
: 0;
}
/**
* Build the active timer query for the authenticated user and current ticket.
*/
private function activeTimerQuery(Authenticatable $user): Builder
{
return TicketTimeLog::query()
->where('ticket_id', $this->ticket->getKey())
->where('user_id', $user->getAuthIdentifier())
->whereNull('ended_at');
}
/**
* Check for any active timer for this user across all tickets.
*/
private function userHasAnyActiveTimer(Authenticatable $user): bool
{
return TicketTimeLog::query()
->where('user_id', $user->getAuthIdentifier())
->whereNull('ended_at')
->exists();
}
/**
* Refresh the ticket's denormalised total without requiring a Ticket::timeLogs() relationship.
*/
private function refreshTicketTimeSpentMinutes(): void
{
$totalMinutes = (int) TicketTimeLog::query()
->where('ticket_id', $this->ticket->getKey())
->whereNotNull('ended_at')
->sum('minutes');
$this->ticket->forceFill([
'time_spent_minutes' => $totalMinutes,
])->save();
}
/**
* Return the authenticated user or fail validation cleanly.
*
* @throws \Illuminate\Validation\ValidationException
*/
private function authenticatedUser(): Authenticatable
{
$user = Auth::user();
if (! $user instanceof Authenticatable) {
throw ValidationException::withMessages([
'timer' => 'You must be logged in to use the timer.',
]);
}
return $user;
}
/**
* Return the description to store against the timer.
*/
private function normalisedDescription(): string
{
$description = trim($this->description);
return $description !== '' ? $description : 'Timer work log';
}
/**
* Detect PostgreSQL/MySQL unique constraint collisions for active timers.
*/
private function isUniqueTimerConstraintViolation(QueryException $exception): bool
{
$sqlState = (string) ($exception->errorInfo[0] ?? '');
return in_array($sqlState, ['23000', '23505'], true);
}
}
+66
View File
@@ -0,0 +1,66 @@
<?php
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;
class TicketTimeLog extends Model
{
/**
* The database table used by the model.
*
* @var string
*/
protected $table = 'helpdesk_ticket_time_logs';
/**
* The attributes that are mass assignable.
*
* @var list<string>
*/
protected $fillable = [
'ticket_id',
'user_id',
'minutes',
'description',
'logged_at',
'started_at',
'ended_at',
'duration_seconds',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'ticket_id' => 'integer',
'user_id' => 'integer',
'minutes' => 'integer',
'duration_seconds' => 'integer',
'logged_at' => 'datetime',
'started_at' => 'datetime',
'ended_at' => 'datetime',
];
/**
* Get the ticket this time log belongs to.
*/
public function ticket(): BelongsTo
{
return $this->belongsTo(Ticket::class, 'ticket_id');
}
/**
* Get the user this time log belongs to.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}
}
@@ -8,6 +8,7 @@
Str::lower((string) ($currentCompany?->slug ?? '')) === 'mpba-limited' ||
Str::contains(Str::lower((string) ($currentCompany?->name ?? '')), 'mpba');
$visibleCommentCount = $ticket->comments->count();
$ticketTimeLogs = collect($ticket->timeLogs ?? []);
@endphp
<div data-dusk="helpdesk-ticket-show" dusk="helpdesk-ticket-show" class="hd-helpdesk-surface space-y-4">
@@ -1024,7 +1025,9 @@
{{ session('ticket-action-updated') }}
</div>
@endif
@include('helpdesk::partials.ticket-time-timer', [
'ticket' => $ticket,
])
<form method="POST" action="{{ route('helpdesk.tickets.time-logs.store', $ticket) }}"
class="mb-4 space-y-2">
@csrf
@@ -1150,7 +1153,7 @@
<div class="min-w-0">
<h2 class="text-lg font-black tracking-tight">Time log history</h2>
<p class="text-xs font-semibold text-emerald-100">
{{ $ticket->timeLogs->count() }} entries · {{ $this->formattedTimeSpent() }}
{{ $ticketTimeLogs->count() }} entries · {{ $this->formattedTimeSpent() }}
</p>
</div>
@@ -1163,7 +1166,7 @@
<div class="max-h-[70vh] overflow-y-auto p-5">
<div class="space-y-3">
@forelse ($ticket->timeLogs as $timeLog)
@forelse ($ticketTimeLogs as $timeLog)
<div
class="rounded-xl border border-zinc-200 bg-zinc-50 p-3 dark:border-zinc-700 dark:bg-zinc-950">
<div class="flex items-start justify-between gap-3">
@@ -0,0 +1,126 @@
<div
x-data="{
running: @js($activeTimer !== null),
startedAt: @js($activeTimer?->started_at?->toIso8601String()),
elapsedSeconds: @js((int) $elapsedSeconds),
intervalId: null,
startClock() {
this.stopClock();
if (!this.running || !this.startedAt) {
return;
}
this.tick();
this.intervalId = window.setInterval(() => this.tick(), 1000);
},
stopClock() {
if (this.intervalId) {
window.clearInterval(this.intervalId);
this.intervalId = null;
}
},
tick() {
if (!this.startedAt) {
this.elapsedSeconds = 0;
return;
}
this.elapsedSeconds = Math.max(0, Math.floor((Date.now() - Date.parse(this.startedAt)) / 1000));
},
formattedElapsed() {
const hours = String(Math.floor(this.elapsedSeconds / 3600)).padStart(2, '0');
const minutes = String(Math.floor((this.elapsedSeconds % 3600) / 60)).padStart(2, '0');
const seconds = String(this.elapsedSeconds % 60).padStart(2, '0');
return `${hours}:${minutes}:${seconds}`;
}
}"
x-init="startClock()"
x-on:timer-started.window="running = true; startedAt = $event.detail.startedAt; startClock()"
x-on:timer-ended.window="running = false; startedAt = null; stopClock()"
class="mb-4 overflow-hidden rounded-xl border border-blue-200 bg-blue-50 shadow-sm dark:border-blue-900 dark:bg-blue-950/40"
data-dusk="ticket-timer-panel"
>
<div class="border-b border-blue-200 bg-blue-700 px-4 py-3 text-white dark:border-blue-900">
<div class="flex items-center justify-between gap-3">
<div class="min-w-0">
<h3 class="text-sm font-black uppercase tracking-wide">User timer</h3>
<p class="text-xs font-semibold text-blue-100">Database-backed timer for this ticket</p>
</div>
<span
class="inline-flex shrink-0 items-center rounded-lg px-2.5 py-1 text-[0.68rem] font-black"
:class="running ? 'bg-emerald-500 text-white' : 'bg-white/15 text-blue-50'"
x-text="running ? 'Running' : 'Stopped'"
></span>
</div>
</div>
<div class="space-y-3 p-4">
@if ($message !== null)
<div class="rounded-lg border border-emerald-200 bg-emerald-50 px-3 py-2 text-xs font-bold text-emerald-800 dark:border-emerald-800 dark:bg-emerald-950 dark:text-emerald-100">
{{ $message }}
</div>
@endif
@error('timer')
<div class="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-xs font-bold text-red-800 dark:border-red-800 dark:bg-red-950 dark:text-red-100">
{{ $message }}
</div>
@enderror
<div class="grid gap-3 sm:grid-cols-[1fr_auto] sm:items-end">
<div>
<label for="ticket-timer-description" class="mb-2 block text-xs font-bold text-zinc-700 dark:text-zinc-200">
Work note
</label>
<textarea
id="ticket-timer-description"
wire:model.defer="description"
rows="2"
placeholder="Optional note for this timed work"
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-blue-500 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100"
data-dusk="ticket-timer-description"
></textarea>
</div>
<div class="rounded-xl border border-blue-200 bg-white px-4 py-3 text-center shadow-sm dark:border-blue-900 dark:bg-zinc-950">
<div class="text-[0.65rem] font-black uppercase tracking-[0.16em] text-blue-700 dark:text-blue-200">
Elapsed
</div>
<div class="mt-1 font-mono text-xl font-black text-zinc-900 dark:text-zinc-100" x-text="formattedElapsed()">
{{ gmdate('H:i:s', (int) $elapsedSeconds) }}
</div>
</div>
</div>
<div class="flex flex-wrap gap-2">
@if ($activeTimer === null)
<button
type="button"
wire:click="startTimer"
wire:loading.attr="disabled"
wire:target="startTimer"
class="inline-flex items-center justify-center rounded-lg bg-blue-600 px-3 py-2 text-xs font-black text-white shadow-sm transition hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-60"
data-dusk="start-ticket-timer"
>
<i class="fa-solid fa-play mr-2" aria-hidden="true"></i>
Start timer
</button>
@else
<button
type="button"
wire:click="endTimer"
wire:loading.attr="disabled"
wire:target="endTimer"
class="inline-flex items-center justify-center rounded-lg bg-red-600 px-3 py-2 text-xs font-black text-white shadow-sm transition hover:bg-red-700 disabled:cursor-not-allowed disabled:opacity-60"
data-dusk="end-ticket-timer"
>
<i class="fa-solid fa-stop mr-2" aria-hidden="true"></i>
End timer
</button>
@endif
</div>
</div>
</div>
@@ -0,0 +1,4 @@
@php use Modules\Helpdesk\Livewire\TicketTimer; @endphp
@can('update', $ticket)
@livewire(TicketTimer::class, ['ticket' => $ticket], key('helpdesk-ticket-timer-'.$ticket->getKey()))
@endcan
@@ -0,0 +1 @@
@livewire(\Modules\Helpdesk\Livewire\TicketTimer::class, ['ticket' => $ticket], key('helpdesk-ticket-timer-'.$ticket->getKey()))
@@ -0,0 +1,49 @@
<?php
namespace Modules\Helpdesk\Tests\Feature;
use Tests\TestCase;
class TicketTimerFilesTest extends TestCase
{
/**
* The Livewire timer view must keep its root element first.
*/
public function test_ticket_timer_view_does_not_start_with_a_style_tag(): void
{
$view = trim((string) file_get_contents(base_path('Modules/Helpdesk/resources/views/livewire/ticket-timer.blade.php')));
$this->assertStringStartsWith('<div', $view);
$this->assertFalse(
str_starts_with($view, '<style'),
'The ticket timer Livewire view must not start with a <style> tag.'
);
}
/**
* The timer must be database backed rather than session/localStorage backed.
*/
public function test_ticket_timer_is_not_session_or_local_storage_backed(): void
{
$component = (string) file_get_contents(base_path('Modules/Helpdesk/Livewire/TicketTimer.php'));
$view = (string) file_get_contents(base_path('Modules/Helpdesk/resources/views/livewire/ticket-timer.blade.php'));
$this->assertStringNotContainsString('session(', $component);
$this->assertStringNotContainsString('localStorage', $component);
$this->assertStringNotContainsString('localStorage', $view);
$this->assertStringContainsString('whereNull', $component);
$this->assertStringContainsString('activeUserTimerQuery', $component);
$this->assertStringContainsString('TicketTimeLog::query()', $component);
$this->assertStringNotContainsString('->timeLogs()', $component);
}
/**
* The ticket page must include the user-specific timer partial.
*/
public function test_ticket_show_timer_partial_exists(): void
{
$this->assertFileExists(base_path('Modules/Helpdesk/resources/views/partials/ticket-time-timer.blade.php'));
$this->assertFileExists(base_path('Modules/Helpdesk/resources/views/livewire/ticket-timer.blade.php'));
$this->assertFileExists(base_path('Modules/Helpdesk/Livewire/TicketTimer.php'));
}
}
+49
View File
@@ -0,0 +1,49 @@
<?php
namespace Modules\Helpdesk\Tests\Feature;
use Tests\TestCase;
class TicketTimerFilesTest extends TestCase
{
/**
* The Livewire timer view must keep its root element first.
*/
public function test_ticket_timer_view_does_not_start_with_a_style_tag(): void
{
$view = trim((string) file_get_contents(base_path('Modules/Helpdesk/resources/views/livewire/ticket-timer.blade.php')));
$this->assertStringStartsWith('<div', $view);
$this->assertFalse(
str_starts_with($view, '<style'),
'The ticket timer Livewire view must not start with a <style> tag.'
);
}
/**
* The timer must be database backed rather than session/localStorage backed.
*/
public function test_ticket_timer_is_not_session_or_local_storage_backed(): void
{
$component = (string) file_get_contents(base_path('Modules/Helpdesk/Livewire/TicketTimer.php'));
$view = (string) file_get_contents(base_path('Modules/Helpdesk/resources/views/livewire/ticket-timer.blade.php'));
$this->assertStringNotContainsString('session(', $component);
$this->assertStringNotContainsString('localStorage', $component);
$this->assertStringNotContainsString('localStorage', $view);
$this->assertStringContainsString('whereNull', $component);
$this->assertStringContainsString('activeUserTimerQuery', $component);
$this->assertStringContainsString('TicketTimeLog::query()', $component);
$this->assertStringNotContainsString('->timeLogs()', $component);
}
/**
* The ticket page must include the user-specific timer partial.
*/
public function test_ticket_show_timer_partial_exists(): void
{
$this->assertFileExists(base_path('Modules/Helpdesk/resources/views/partials/ticket-time-timer.blade.php'));
$this->assertFileExists(base_path('Modules/Helpdesk/resources/views/livewire/ticket-timer.blade.php'));
$this->assertFileExists(base_path('Modules/Helpdesk/Livewire/TicketTimer.php'));
}
}
+8 -418
View File
@@ -1,419 +1,9 @@
- version: 1.9.29
date: 2026-06-18
- version: 0.7.0
date: 2026-06-22
type: feature
summary: Add database-backed user-specific ticket timer controls.
changes:
- Hardened every Helpdesk settings create/edit/save path so Livewire CRUD actions persist updates without null primary-key inserts or invalid default-reset queries.
- Added company relationships/eager loading for category, team and project settings tables so settings pages render cleanly during edit/save cycles.
- Added full SettingsCrudTest coverage for statuses, priorities, categories, teams, projects, boards, Git repositories, workflow templates, canned responses, SLA policies, automation rules, checklist templates, tags and products.
- version: 1.3.6
date: 2026-06-16
changes:
- Backfilled status columns onto Kanban boards that have none so selected boards render usable workflow columns.
- Added runtime board-column repair to keep newly-created boards usable immediately.
- Tightened the Kanban board selector width and dark styling.
- version: 1.3.5
date: 2026-06-16
changes:
- Fixed Kanban board filtering so the selected board only shows tickets assigned to that single board.
- Added a backfill migration for existing tickets without a board assignment.
- Restyled the active board selector and per-ticket board selector with compact dark controls.
- version: 1.3.4
date: 2026-06-16
changes:
- Added single-board ticket assignment via helpdesk_tickets.board_id.
- Added Kanban board owner support.
- Added board-to-board ticket moves while enforcing one active board per ticket.
- Added status move alerts/toasts when tickets move between workflow statuses.
- Added create/edit ticket board selection and default-board assignment for new tickets.
- version: 1.3.3
date: '2026-06-16'
changes:
- Reworked Helpdesk action styling so header, support, settings, table, save, reset, edit and delete buttons use reliable coloured native styling.
- Replaced settings section card dynamic Tailwind colour classes with inline styles so card colours always render.
- Improved Kanban board dropdown styling.
- Hardened ticket create inputs and Select2 controls to prevent white flashes during save.
url: https://wms.vm
versions:
"1.0.0":
changed:
- Initial Module Definition
"1.0.1":
changed:
- Added Vite Support and removed webpack
"1.0.2":
changed:
- Stabilised the Livewire rich text editor so ticket details no longer clear during typing or submit early.
- Added Helpdesk Kanban board for workflow status movement.
- Added Kanban PHPUnit and Dusk coverage.
- Added Kanban navigation links to the dashboard and ticket list.
"1.0.3":
changed:
- Fixed Helpdesk dashboard rendering by removing dependency on missing x-app-layout/x-layouts.app components.
- Registered anonymous Helpdesk Blade components so x-helpdesk component views resolve correctly.
- Registered the Kanban Livewire component in the Helpdesk service provider.
- Rebuilt the ticket create page to use the correct subject/body Livewire properties.
- Stabilised the rich HTML editor so typing no longer clears the field or accidentally submits the ticket form.
- Added root layout support-menu integration instructions for enabled Helpdesk modules.
"1.0.4":
changed:
- Fixed the Helpdesk page header so dark WMS pages do not render a blank white hero.
- Changed ticket create save to validate all fields and return to the tickets index after creation to avoid route-model redirect issues.
- Added a root layout replacement containing the Support sidebar menu and Livewire update endpoint configuration.
- Kept module.json version in sync with changelog.yaml.
"1.0.5":
changed:
- Added the missing POST helpdesk.tickets.store route so native form submission to /helpdesk/tickets no longer returns 404.
- Added TicketStoreController as a safe non-JavaScript fallback while keeping the Livewire ticket create flow intact.
- Updated the ticket create form with method, action, CSRF, multipart support and named fields for fallback posting.
- Kept module.json version in sync with changelog.yaml.
"1.0.6":
changed:
- Rebuilt the Helpdesk database seeder with the requested Categories, Team, Priority and Status lookup values.
- Added contrasting colour assignments for seeded lookup records.
- Corrected the seeder namespace to Modules\\Helpdesk\\Database\\Seeders for PSR-4 module autoloading.
- Kept module.json version in sync with changelog.yaml.
"1.0.9":
changed:
- Added editable tickets with assignment, status, priority, category, team, project, due date and details updates.
- Changed the default ticket creation status to New.
- Added configurable Kanban boards with status-column assignment and a default Standard Support board.
- Rebuilt the Kanban board into a professional drag-and-drop board with SortableJS fallback loading.
- Added blue Back and green Edit actions on ticket view pages.
- Improved Helpdesk settings styling, blue edit actions, and a dedicated Kanban Boards CRUD section.
- Marked mandatory ticket fields and enhanced create/edit ticket forms with Select2-capable dropdowns.
- Kept module.json version in sync with changelog.yaml.
"1.1.3":
changed:
- Fixed ticket update permissions so platform admins, helpdesk admins, assigned agents and ticket owners can edit tickets where appropriate.
- Added the missing ticket edit Blade view with assignment, status, priority, category, project, team, due date and details controls.
- Replaced the Kanban drag dependency path with native browser drag-and-drop to prevent blocked scripts from breaking ticket movement.
- Added Kanban Boards CRUD to Helpdesk settings with configurable status columns.
- Improved ticket create and edit form layout, mandatory markers, dark footer styling and searchable Select2-capable dropdown loading.
- Kept module.json version in sync with changelog.yaml.
"1.2.0":
changed:
- Added RedmineUP-inspired canned responses with professional reply macro tokens for ticket, requester, agent, status, priority, category, project, team and URL values.
- Added SLA policies, first-response and resolution due dates, breach tracking, SLA event history and ticket/Kanban breach indicators.
- Added incoming email automation rule models and service support for matching sender, subject, mailbox and category to auto-set priority, category, team, project, assignee and status.
- Added database-driven autoresponder enhancements with optional read-receipt tracking tokens, workflow log open timestamps and open counts.
- Added customer/requester profile context on ticket detail pages with tags and richer support metadata.
- Added satisfaction rating data model for closed-ticket feedback reporting by agent, team and category.
- Added ticket tags, checklist templates, checklist items, checklist progress and agent checklist toggling.
- Added product/service/build linkage for tickets with support cost/time fields and product-aware administration.
- Extended Helpdesk settings with professional CRUD sections for canned responses, SLA policies, automation rules, checklist templates, tags and products/services.
- Extended ticket create/edit/show and Kanban screens to include product/service linkage, SLA status, tags and checklist progress without breaking the existing workflow.
- Added PHPUnit coverage for automation, SLA, canned response macro rendering, checklist toggling and product linkage, plus Dusk coverage for the new settings sections.
- Kept module.json version in sync with changelog.yaml.
"1.2.1":
changed:
- Added signed Git webhook ingestion endpoint at POST /api/helpdesk/git/webhook/{repository} for registered repositories.
- Added GitHub X-Hub-Signature-256 HMAC validation, GitLab X-Gitlab-Token validation and a generic X-Helpdesk-Token fallback for internal providers.
- Added automatic commit persistence and Helpdesk ticket linking from HD-YYYY-00000 references in commit messages, branch names and commit URLs.
- Added helpdesk_git_webhook_logs for auditable accepted, ignored and rejected provider deliveries.
- Added Git webhook endpoint visibility and delivery counts to the Helpdesk Git repository settings table.
- Added provider setup documentation for GitHub, GitLab and generic signed Git payloads.
- Added PHPUnit coverage for accepted GitHub/GitLab push webhooks, invalid signature rejection and ignored non-push events.
- Kept module.json version in sync with changelog.yaml.
"1.2.2":
changed:
- Fixed Helpdesk settings rendering during rolling upgrades by defaulting newly added RedmineUP/Git settings collections in the Blade view.
- Prevented undefined variable errors for canned responses, SLA policies, automation rules, checklist templates, tags and products when compiled views and Livewire component code are briefly out of sync.
- Kept module.json version in sync with changelog.yaml.
"1.2.3":
changed:
- Fixed Helpdesk settings Livewire actions for the RedmineUP-inspired sections so SLA, canned responses, automation, products, tags and checklists can be saved from /helpdesk/settings.
- Added SettingsManager mount-time form normalisation to survive stale Livewire snapshots after module upgrades.
- Added a defensive workflow tracking-pixel URL fallback so ticket creation cannot fail when route caches are stale.
- Registered the workflow open pixel route in the web route file as a backup to the module provider route.
- Kept module.json version in sync with changelog.yaml.
"1.2.5":
changed:
- Fixed the Helpdesk settings default-company Livewire action by adding saveDefaultHelpdeskCompany to SettingsManager.
- Added default flags for Helpdesk statuses as well as priorities, categories, teams and projects.
- Updated ticket creation to use the configured default status and lookup defaults.
- Added selectable ticket tags to the create ticket workflow and synced tags onto new tickets.
- Kept the one-line ticket subject input and dark form repaint fixes.
- Kept module.json version in sync with changelog.yaml.
"1.2.6":
changed:
- Fixed the Helpdesk settings default-company Livewire method and made the default company property resilient to select empty-string values.
- Confirmed default statuses, priorities, categories, teams and projects are applied during ticket creation.
- Confirmed ticket tags can be assigned on create and edit and are synced to the ticket-tag pivot.
- Kept module.json version in sync with changelog.yaml.
"1.2.8":
changed:
- Replaced white Flux action buttons on the Helpdesk dashboard, ticket list, Kanban board and settings header with explicit high-contrast coloured action buttons.
- Standardised + New Ticket actions as green buttons across Helpdesk entry points.
- Kept module.json version in sync with changelog.yaml.
"1.2.9":
changed:
- Applied explicit Helpdesk action colour conventions across dashboard, tickets, Kanban and settings pages.
- Coloured settings section navigation tiles by domain so Statuses, Priorities, Categories, Teams, Projects, Boards, Workflow, SLA, Tags, Products and Defaults are visually distinct.
- Coloured save, reset, edit, delete, settings, ticket, Kanban, reply and update action buttons consistently.
- Kept module.json version in sync with changelog.yaml.
"1.3.0":
changed:
- Fixed malformed Helpdesk settings action buttons that caused Blade parse errors.
- Applied explicit coloured action buttons across settings tables, settings forms and Kanban board configuration actions.
- Kept module.json version in sync with changelog.yaml.
"1.3.7":
changed:
- Fixed ticket create/edit clear actions by adding public Livewire clearForm methods to both components.
- Stabilised Select2 clearing by loading module assets on ticket forms, adding placeholder support, manually syncing Select2 changes back to Livewire and clearing browser widgets after Livewire reset events.
- Replaced native due date inputs on ticket create/edit with a reusable Helpdesk Flatpickr date-picker component.
- Added Flatpickr module dependency and dark Helpdesk calendar styling.
- Kept module.json version in sync with changelog.yaml.
"1.3.9":
changed:
- Fixed TicketControlsController authorization by using Laravel Gate authorization instead of relying on a missing controller authorize helper.
- Kept normal CSRF-protected POST ticket controls for agent, status and priority persistence.
- Restricted posted agent assignments to tenant/company users or global users when the users.company_id column exists.
- Kept compact ticket control buttons and module.json version in sync with changelog.yaml.
"1.3.10":
changed:
- Fixed remaining ticket-detail N+1 alerts by loading requester and assigned agent users in one query and attaching owner/user/agent relations manually.
- Removed duplicate owner/agent eager-load cycles from ticket control actions, workflow rendering and ticket service refreshes.
- Stopped ticket detail render from reloading the ticket after mount so detail relations are not queried twice in one request.
- Removed remaining orWhereKey usage from Helpdesk ticket agent option queries for Laravel 13 compatibility.
- Kept module.json version in sync with changelog.yaml.
"1.3.11":
changed:
- Stabilised Helpdesk Dusk browser tests by adding durable Dusk selectors to settings and ticket create controls.
- Replaced browser-test Livewire internals with real UI interactions for settings sections, Select2 controls and HTML editors.
- Kept module.json version in sync with changelog.yaml.
"1.3.12":
changed:
- Replaced fragile Livewire/Flux ticket comment and reply submits with CSRF-protected normal POST comment storage.
- Restored canned-response insertion by writing the selected response into the real ticket comment textarea before submit.
- Restored nested comment replies through per-comment POST reply forms that persist parent_id and refresh the conversation thread.
- Made Helpdesk settings section navigation use normal query-string links so Dusk and browsers do not depend on fragile Livewire tab clicks.
- Updated browser tests to cover canned-response insertion, parent comment persistence, nested reply persistence and settings tab navigation.
- Kept module.json version in sync with changelog.yaml.
"1.3.13":
changed:
- Added Lotus Notes-style collapsible ticket conversation cards with visible response counts, red threaded reply rails, search filtering and expand-all controls.
- Added professional HTML helper controls for comments and nested responses while preserving safe HTML rendering.
- Fixed canned-response insertion so stored HTML is inserted as real HTML instead of escaped text.
- Added professional pinned attachment controls for root comments and response-to-response forms.
- Restricted MPBA internal-note visibility and creation to MPBA/core-company users while hiding internal notes from ordinary tenant users.
- Added GUID, UNID, parent UNID, root UNID, originator, sync sequence, sync hash and searchable body text fields to Helpdesk comments for future replication and search.
- Updated TicketService comment creation to populate Lotus-style replication identity metadata for document responses and nested responses.
- Updated Helpdesk conversation tests to cover POST comment storage, nested reply identity metadata and searchable comment text.
- Kept module.json version in sync with changelog.yaml.
- version: 1.7.0
date: 2026-06-17
type: changed
summary: "Polished Lotus Notes-style ticket document and conversation headers, fully collapsed conversation default, subject-led comment cards, and hidden replication identifiers."
changes:
- "Conversation panel now starts fully collapsed and only reveals comment cards when opened."
- "Comment cards now show a professional subject header with author/date metadata and no visible GUID/UNID."
- "Comment subjects are stored for public responses, canned responses, and nested replies while GUID/UNID remain searchable."
- "Ticket details, SLA metrics, requester profile, ticket document, attachments, and conversation headers received consistent professional styling."
- "Browser and feature tests were updated for collapsed conversations, comment subjects, and hidden replication identifiers."
- version: 1.7.1
date: 2026-06-17
type: changed
summary: "Unsquashed Helpdesk ticket document and reply composer with professional document headers."
changes:
- "Expanded the request document body area with larger padding, stronger document typography, and a minimum document height so ticket text no longer appears compressed."
- "Added a professional response-document header to the Add reply composer while preserving canned responses, rich-text controls, internal-note visibility, attachments, and POST comment storage."
- "Increased the reply subject, rich-text editor, attachment pin area, and submit button spacing for a more professional Lotus Notes-style document workflow."
- "Kept module.json version in sync with changelog.yaml."
- version: 1.7.2
date: 2026-06-17
type: changed
summary: "Refined Helpdesk ticket document and reply composer spacing so headers and editor areas no longer look compressed."
changes:
- "Reworked the request document header with balanced icon sizing, spacing, and a professional ticket reference badge."
- "Expanded the ticket body document area with larger vertical space and document-style padding instead of a compressed input-like box."
- "Reworked the conversation header with consistent professional spacing while keeping the conversation fully collapsed by default."
- "Reworked the Add reply response-document header and expanded the composer controls and rich text editor height for a less squashed professional layout."
- "Kept module.json version in sync with changelog.yaml."
- version: 1.7.3
date: 2026-06-17
type: fixed
summary: "Hardened Helpdesk ticket detail rendering, removed the missing module Vite manifest failure, and applied professional unsquashed document styling."
changes:
- "Guarded the Helpdesk module Vite include so missing public/build/helpdesk/manifest.json no longer causes a 500 error on ticket pages."
- "Removed the duplicate ticket-show Vite call and kept the ticket page usable without a module-specific asset build."
- "Added page-local professional Helpdesk document styling so ticket document, conversation, and Add reply sections have balanced spacing, smaller header icons, non-squashed body areas, and readable editor controls even before module assets are rebuilt."
- "Kept the conversation collapsed by default while preserving the Lotus Notes-style document response workflow."
- "Kept module.json version in sync with changelog.yaml."
- version: 1.7.4
date: 2026-06-17
type: changed
summary: "Matched the ticket attachments header to the professional Document responses header styling."
changes:
- "Reworked the ticket attachments section header to use the same dark professional document header pattern as Document responses."
- "Replaced the bright green-blue attachment banner with a consistent navy gradient, balanced icon sizing, document-style title, and professional file count badge."
- "Improved attachment file rows with clearer spacing and a download affordance while preserving existing attachment download routes."
- "Kept module.json version in sync with changelog.yaml."
- version: 1.0.37
date: 2026-06-17
changes:
- Collapsed ticket attachment details by default to match the document response panel behaviour.
- Reduced the pinned attachment picker to a compact professional button with inline file status text.
- version: 1.0.38
date: 2026-06-17
changes:
- Reworked the reply attachment picker into a compact professional collapsible attachments panel.
- Removed the oversized blue pin attachments button and dashed drop-zone styling.
- Added a neutral Choose files control, inline file status, and attachment count badge.
- Kept the attachment panel collapsed by default while preserving multi-file uploads for replies and nested responses.
- Kept module.json version in sync with changelog.yaml.
- version: 1.0.39
date: 2026-06-17
changes:
- Coloured the reply attachment chooser as a compact blue action button while preserving the professional collapsed attachment panel layout.
- Kept module.json version in sync with changelog.yaml.
- version: 1.0.37
date: 2026-06-17
changes:
- Refined the reply attachment chooser into a compact professional attachment row.
- Replaced the oversized primary-style file button with a smaller coloured action and integrated file status area.
- version: 1.0.40
date: 2026-06-17
changes:
- Reworked the reply attachment picker into a cleaner professional row with a compact blue Choose files action.
- Removed the large primary-style attachment button and disconnected file status presentation.
- Kept the attachments panel collapsed by default and preserved multi-file upload behaviour.
- Kept module.json version in sync with changelog.yaml.
- version: 1.0.41
date: 2026-06-17
changes:
- Refined the reply attachment Choose files button with a centred rounded action style.
- Centred the upload icon and label vertically and horizontally to improve the professional attachment picker layout.
- Kept module.json version in sync with changelog.yaml.
- version: 1.0.31
date: 2026-06-17
changes:
- Refined the Helpdesk attachment picker button into a compact, centred, professional blue action control.
- version: 1.9.18
date: 2026-06-17
changes:
- Centred the attachment chooser content vertically and changed the attachment action to a professional green button treatment.
- version: 1.9.19
date: 2026-06-17
changes:
- Moved response attachment selection into the professional Supporting files card with a centred green attachment action.
- Preserved multiple file uploads for comments, responses, and response-to-response replies.
- Added MPBA platform administrator response deletion using replication-safe tombstoning rather than destructive removal.
- Added deletion audit fields for helpdesk comments.
- Removed direct Helpdesk Vite manifest loading from ticket create when the module build manifest is not present.
- Updated Helpdesk conversation feature coverage for multiple attachments and super-admin response deletion.
- Kept module.json version in sync with changelog.yaml.
- version: 1.9.20
date: 2026-06-17
changes:
- Added a dedicated existing-ticket supporting files upload endpoint that accepts and stores multiple attachments.
- Moved the supporting file picker and upload action into the Supporting files card and removed the disconnected visual flow.
- Standardised Choose files and Upload actions to the same professional size and button treatment.
- Kept the Supporting files card visible even when a ticket has no attachments.
- Removed direct Helpdesk Vite manifest loading from ticket edit when the module build manifest is not present.
- Forced the Kanban board to refresh after drag/drop moves and added visible polling for external ticket movement updates.
- Kept module.json version in sync with changelog.yaml.
- version: 1.9.22
date: 2026-06-17
changed:
- Removed the duplicate lower Supporting files dropdown card from the ticket reply form.
- Kept existing-ticket supporting uploads in the main Supporting files card with equal-sized choose and upload buttons.
- Hardened existing-ticket and comment attachment uploads so multiple valid uploaded files are preserved and saved.
- Forced Kanban drag/drop moves to refresh the Livewire board immediately after the database status update.
- version: 1.9.23
date: 2026-06-17
changed:
- Reworked Kanban drag/drop refresh to move the card in the browser immediately, update visible column counts, and then force a Livewire refresh after the database move completes.
- Added a browser reload fallback for Kanban moves if Livewire does not expose a refresh method after drag/drop.
- Kept module.json version in sync with changelog.yaml.
- version: 1.9.24
date: 2026-06-17
changed:
- Removed Helpdesk Tickets and Kanban shortcut menu entries from the application/sidebar navigation surfaces.
- Removed Tickets, New Ticket and Kanban shortcut buttons from the Helpdesk settings header because those workflows are covered elsewhere.
- Added browser coverage to keep Helpdesk settings free of direct Tickets and Kanban shortcut links.
- Kept module.json version in sync with changelog.yaml.
- version: 1.9.25
date: 2026-06-18
changed:
- Replaced the duplicated Helpdesk application shell with the shared application layout so Helpdesk uses the same mobile navigation and top bar as the rest of WMS.
- Preserved the existing Helpdesk views while centralising the layout surface to reduce future mobile regressions.
- Kept module.json version in sync with changelog.yaml.
- version: 1.9.26
date: 2026-06-18
changed:
- Removed the extra visible plus character from Helpdesk New Ticket action labels while keeping the icon-only plus indicator.
- Kept module.json version in sync with changelog.yaml.
- version: 1.9.27
date: 2026-06-18
changed:
- Fixed Helpdesk settings CRUD forms by normalising optional browser select values before Livewire validation.
- Hardened optional nullable fields across categories, teams, projects, boards, Git repositories, workflow templates, canned responses, SLA policies, automation rules, checklists, tags, products and default company selection.
- Added feature coverage proving every Helpdesk settings area can create records when optional select controls submit empty browser values.
- Kept module.json version in sync with changelog.yaml.
- version: 1.9.28
date: 2026-06-18
changed:
- Fixed Helpdesk settings CRUD browser saves by loading Livewire scripts in the Helpdesk application shell before Flux scripts.
- Added explicit submit prevention to every Helpdesk settings form so browser submits are always handled by Livewire actions.
- Added regression coverage to prevent the Helpdesk shell from dropping the Livewire script stack again.
- Kept module.json version in sync with changelog.yaml.
- Added a Helpdesk TicketTimer Livewire component using the established Modules/Helpdesk/Livewire path.
- Added persistent started_at, ended_at, and duration_seconds fields to existing helpdesk time logs where available.
- Added a reusable Blade include for placing the timer above the existing manual time log form.
- Added regression tests for module path, Livewire view root safety, and non-session timer state.