diff --git a/Modules/Helpdesk/Controllers/TicketTimeLogController.php b/Modules/Helpdesk/Controllers/TicketTimeLogController.php new file mode 100644 index 00000000..1f627b2c --- /dev/null +++ b/Modules/Helpdesk/Controllers/TicketTimeLogController.php @@ -0,0 +1,35 @@ +authorize('update', $ticket); + + $validated = $request->validate([ + 'minutes' => ['required', 'integer', 'min:1', 'max:1440'], + 'description' => ['nullable', 'string', 'max:1000'], + ]); + + $ticket->timeLogs()->create([ + 'user_id' => $request->user()?->id, + 'minutes' => (int) $validated['minutes'], + 'description' => $validated['description'] ?? null, + 'logged_at' => now(), + ]); + + return back()->with('ticket-action-updated', 'Time logged successfully.'); + } +} diff --git a/Modules/Helpdesk/Database/Migrations/2026_06_22_000001_create_helpdesk_time_logs_table.php b/Modules/Helpdesk/Database/Migrations/2026_06_22_000001_create_helpdesk_time_logs_table.php new file mode 100644 index 00000000..e1932242 --- /dev/null +++ b/Modules/Helpdesk/Database/Migrations/2026_06_22_000001_create_helpdesk_time_logs_table.php @@ -0,0 +1,41 @@ +id(); + $table->foreignId('ticket_id')->constrained('helpdesk_tickets')->cascadeOnDelete(); + $table->foreignId('user_id')->constrained('users')->cascadeOnDelete(); + $table->unsignedInteger('minutes'); + $table->text('description')->nullable(); + $table->timestamp('logged_at')->useCurrent(); + $table->timestamps(); + + $table->index(['ticket_id', 'logged_at']); + $table->index(['user_id', 'logged_at']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('helpdesk_time_logs'); + } +}; diff --git a/Modules/Helpdesk/Helpdesk/Http/Controllers/TicketTimeLogController.php b/Modules/Helpdesk/Helpdesk/Http/Controllers/TicketTimeLogController.php new file mode 100644 index 00000000..1f627b2c --- /dev/null +++ b/Modules/Helpdesk/Helpdesk/Http/Controllers/TicketTimeLogController.php @@ -0,0 +1,35 @@ +authorize('update', $ticket); + + $validated = $request->validate([ + 'minutes' => ['required', 'integer', 'min:1', 'max:1440'], + 'description' => ['nullable', 'string', 'max:1000'], + ]); + + $ticket->timeLogs()->create([ + 'user_id' => $request->user()?->id, + 'minutes' => (int) $validated['minutes'], + 'description' => $validated['description'] ?? null, + 'logged_at' => now(), + ]); + + return back()->with('ticket-action-updated', 'Time logged successfully.'); + } +} diff --git a/Modules/Helpdesk/Helpdesk/Models/TimeLog.php b/Modules/Helpdesk/Helpdesk/Models/TimeLog.php new file mode 100644 index 00000000..9df473b3 --- /dev/null +++ b/Modules/Helpdesk/Helpdesk/Models/TimeLog.php @@ -0,0 +1,41 @@ + */ + protected $fillable = [ + 'ticket_id', + 'user_id', + 'minutes', + 'description', + 'logged_at', + ]; + + /** @var array */ + protected $casts = [ + 'minutes' => 'integer', + 'logged_at' => 'datetime', + ]; + + /** @return BelongsTo */ + public function ticket(): BelongsTo + { + return $this->belongsTo(Ticket::class, 'ticket_id'); + } + + /** @return BelongsTo */ + public function user(): BelongsTo + { + return $this->belongsTo(User::class, 'user_id'); + } +} diff --git a/Modules/Helpdesk/Helpdesk/Tests/Feature/TicketTimeLogTest.php b/Modules/Helpdesk/Helpdesk/Tests/Feature/TicketTimeLogTest.php new file mode 100644 index 00000000..6c6e3831 --- /dev/null +++ b/Modules/Helpdesk/Helpdesk/Tests/Feature/TicketTimeLogTest.php @@ -0,0 +1,55 @@ +create(); + $ticket = Ticket::factory()->create(); + + $this->actingAs($user) + ->post(route('helpdesk.tickets.time-logs.store', $ticket), [ + 'minutes' => 45, + 'description' => 'Investigated the issue and updated the ticket.', + ]) + ->assertRedirect(); + + $this->assertDatabaseHas('helpdesk_time_logs', [ + 'ticket_id' => $ticket->id, + 'user_id' => $user->id, + 'minutes' => 45, + 'description' => 'Investigated the issue and updated the ticket.', + ]); + } + + /** + * It rejects invalid logged minutes. + */ + public function test_ticket_time_log_requires_valid_minutes(): void + { + $user = User::factory()->create(); + $ticket = Ticket::factory()->create(); + + $this->actingAs($user) + ->from(route('helpdesk.tickets.show', $ticket)) + ->post(route('helpdesk.tickets.time-logs.store', $ticket), [ + 'minutes' => 0, + ]) + ->assertRedirect(route('helpdesk.tickets.show', $ticket)) + ->assertSessionHasErrors('minutes'); + } +} diff --git a/Modules/Helpdesk/Helpdesk/database/migrations/2026_06_22_000001_create_helpdesk_time_logs_table.php b/Modules/Helpdesk/Helpdesk/database/migrations/2026_06_22_000001_create_helpdesk_time_logs_table.php new file mode 100644 index 00000000..e1932242 --- /dev/null +++ b/Modules/Helpdesk/Helpdesk/database/migrations/2026_06_22_000001_create_helpdesk_time_logs_table.php @@ -0,0 +1,41 @@ +id(); + $table->foreignId('ticket_id')->constrained('helpdesk_tickets')->cascadeOnDelete(); + $table->foreignId('user_id')->constrained('users')->cascadeOnDelete(); + $table->unsignedInteger('minutes'); + $table->text('description')->nullable(); + $table->timestamp('logged_at')->useCurrent(); + $table->timestamps(); + + $table->index(['ticket_id', 'logged_at']); + $table->index(['user_id', 'logged_at']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('helpdesk_time_logs'); + } +}; diff --git a/Modules/Helpdesk/Helpdesk/resources/views/partials/time-log-form.blade.php b/Modules/Helpdesk/Helpdesk/resources/views/partials/time-log-form.blade.php new file mode 100644 index 00000000..4b26baf5 --- /dev/null +++ b/Modules/Helpdesk/Helpdesk/resources/views/partials/time-log-form.blade.php @@ -0,0 +1,42 @@ +
+ @csrf + + + +
+ + + +
+ + + + @error('minutes') +

{{ $message }}

+ @enderror + + @error('description') +

{{ $message }}

+ @enderror +
diff --git a/Modules/Helpdesk/Helpdesk/routes/time-logs.php b/Modules/Helpdesk/Helpdesk/routes/time-logs.php new file mode 100644 index 00000000..b463afe9 --- /dev/null +++ b/Modules/Helpdesk/Helpdesk/routes/time-logs.php @@ -0,0 +1,9 @@ +name('tickets.time-logs.store'); diff --git a/Modules/Helpdesk/Http/Controllers/TicketTimeLogController.php b/Modules/Helpdesk/Http/Controllers/TicketTimeLogController.php new file mode 100644 index 00000000..1f627b2c --- /dev/null +++ b/Modules/Helpdesk/Http/Controllers/TicketTimeLogController.php @@ -0,0 +1,35 @@ +authorize('update', $ticket); + + $validated = $request->validate([ + 'minutes' => ['required', 'integer', 'min:1', 'max:1440'], + 'description' => ['nullable', 'string', 'max:1000'], + ]); + + $ticket->timeLogs()->create([ + 'user_id' => $request->user()?->id, + 'minutes' => (int) $validated['minutes'], + 'description' => $validated['description'] ?? null, + 'logged_at' => now(), + ]); + + return back()->with('ticket-action-updated', 'Time logged successfully.'); + } +} diff --git a/Modules/Helpdesk/Models/TimeLog.php b/Modules/Helpdesk/Models/TimeLog.php new file mode 100644 index 00000000..9df473b3 --- /dev/null +++ b/Modules/Helpdesk/Models/TimeLog.php @@ -0,0 +1,41 @@ + */ + protected $fillable = [ + 'ticket_id', + 'user_id', + 'minutes', + 'description', + 'logged_at', + ]; + + /** @var array */ + protected $casts = [ + 'minutes' => 'integer', + 'logged_at' => 'datetime', + ]; + + /** @return BelongsTo */ + public function ticket(): BelongsTo + { + return $this->belongsTo(Ticket::class, 'ticket_id'); + } + + /** @return BelongsTo */ + public function user(): BelongsTo + { + return $this->belongsTo(User::class, 'user_id'); + } +} diff --git a/Modules/Helpdesk/Resources/views/livewire/ticket-show.blade.php b/Modules/Helpdesk/Resources/views/livewire/ticket-show.blade.php index 2920fe70..98166641 100644 --- a/Modules/Helpdesk/Resources/views/livewire/ticket-show.blade.php +++ b/Modules/Helpdesk/Resources/views/livewire/ticket-show.blade.php @@ -32,12 +32,136 @@ Back - @can('update', $ticket) - - - Edit - +
+
+
+ + + +
+

Ticket controls

+

Log time, assignment, status and priority

+
+
+
+ + @if (session('ticket-action-updated')) +
+ {{ session('ticket-action-updated') }} +
+ @endif + +
+ @csrf + + + +
+ + + +
+ + + + @error('minutes') +

{{ $message }}

+ @enderror + + @error('description') +

{{ $message }}

+ @enderror +
+ +
+ @csrf + + + + + @error('agent_id') +

{{ $message }}

+ @enderror + + +
+ +
+ @csrf + + + + + @error('status_id') +

{{ $message }}

+ @enderror + + +
+ +
+ @csrf + + + + + @error('priority_id') +

{{ $message }}

+ @enderror + + +
+
@endcan diff --git a/Modules/Helpdesk/Resources/views/partials/time-log-form.blade.php b/Modules/Helpdesk/Resources/views/partials/time-log-form.blade.php new file mode 100644 index 00000000..4b26baf5 --- /dev/null +++ b/Modules/Helpdesk/Resources/views/partials/time-log-form.blade.php @@ -0,0 +1,42 @@ +
+ @csrf + + + +
+ + + +
+ + + + @error('minutes') +

{{ $message }}

+ @enderror + + @error('description') +

{{ $message }}

+ @enderror +
diff --git a/Modules/Helpdesk/Routes/web.php b/Modules/Helpdesk/Routes/web.php index 6cfcf2ca..aaa4c4ba 100644 --- a/Modules/Helpdesk/Routes/web.php +++ b/Modules/Helpdesk/Routes/web.php @@ -10,6 +10,7 @@ use Modules\Helpdesk\Http\Controllers\TicketCommentDeleteController; use Modules\Helpdesk\Http\Controllers\TicketCommentStoreController; use Modules\Helpdesk\Http\Controllers\TicketAttachmentStoreController; use Modules\Helpdesk\Http\Controllers\TicketStoreController; +use Modules\Helpdesk\Http\Controllers\TicketTimeLogController; use Modules\Helpdesk\Http\Controllers\WorkflowOpenController; use Modules\Helpdesk\Livewire\KanbanBoard; use Modules\Helpdesk\Livewire\SettingsManager; @@ -39,3 +40,5 @@ Route::get('/tickets/{ticket}', TicketShow::class)->name('tickets.show'); Route::get('/attachments/{attachment}/download', AttachmentDownloadController::class) ->whereNumber('attachment') ->name('attachments.download'); +Route::post('/tickets/{ticket}/time-logs', [TicketTimeLogController::class, 'store']) + ->name('tickets.time-logs.store'); diff --git a/Modules/Helpdesk/Tests/Feature/TicketTimeLogTest.php b/Modules/Helpdesk/Tests/Feature/TicketTimeLogTest.php new file mode 100644 index 00000000..6c6e3831 --- /dev/null +++ b/Modules/Helpdesk/Tests/Feature/TicketTimeLogTest.php @@ -0,0 +1,55 @@ +create(); + $ticket = Ticket::factory()->create(); + + $this->actingAs($user) + ->post(route('helpdesk.tickets.time-logs.store', $ticket), [ + 'minutes' => 45, + 'description' => 'Investigated the issue and updated the ticket.', + ]) + ->assertRedirect(); + + $this->assertDatabaseHas('helpdesk_time_logs', [ + 'ticket_id' => $ticket->id, + 'user_id' => $user->id, + 'minutes' => 45, + 'description' => 'Investigated the issue and updated the ticket.', + ]); + } + + /** + * It rejects invalid logged minutes. + */ + public function test_ticket_time_log_requires_valid_minutes(): void + { + $user = User::factory()->create(); + $ticket = Ticket::factory()->create(); + + $this->actingAs($user) + ->from(route('helpdesk.tickets.show', $ticket)) + ->post(route('helpdesk.tickets.time-logs.store', $ticket), [ + 'minutes' => 0, + ]) + ->assertRedirect(route('helpdesk.tickets.show', $ticket)) + ->assertSessionHasErrors('minutes'); + } +}