phases one of time logged now with log display
This commit is contained in:
+54
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
+41
@@ -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');
|
||||
}
|
||||
};
|
||||
+45
@@ -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'));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user