Repo rebuild
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Helpdesk\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Modules\Helpdesk\Models\Status;
|
||||
use Modules\Helpdesk\Models\Ticket;
|
||||
use Modules\Helpdesk\Services\SlaService;
|
||||
use Modules\Helpdesk\Services\TicketService;
|
||||
use Modules\Helpdesk\Services\WorkflowNotificationService;
|
||||
|
||||
final class TicketControlsController extends Controller
|
||||
{
|
||||
/**
|
||||
* Persist the assigned agent from the ticket detail controls.
|
||||
*/
|
||||
public function updateAgent(Request $request, Ticket $ticket, WorkflowNotificationService $workflow): RedirectResponse
|
||||
{
|
||||
$this->hydrateUserRelations($ticket);
|
||||
|
||||
Gate::authorize('update', $ticket);
|
||||
|
||||
$validated = $request->validate([
|
||||
'agent_id' => [
|
||||
'nullable',
|
||||
'integer',
|
||||
Rule::exists('users', 'id')->where(function ($query) use ($ticket): void {
|
||||
if ($ticket->company_id !== null && Schema::hasColumn('users', 'company_id')) {
|
||||
$query->where(function ($query) use ($ticket): void {
|
||||
$query->where('company_id', $ticket->company_id)
|
||||
->orWhereNull('company_id');
|
||||
});
|
||||
}
|
||||
}),
|
||||
],
|
||||
]);
|
||||
|
||||
$agentId = $this->nullableInt($validated['agent_id'] ?? null);
|
||||
|
||||
$this->updateTicket($ticket, [
|
||||
'agent_id' => $agentId,
|
||||
'last_activity_at' => now(),
|
||||
]);
|
||||
|
||||
$fresh = $this->freshTicketForAction($ticket);
|
||||
$workflow->dispatch($fresh, 'ticket.updated', $this->actor());
|
||||
|
||||
return redirect()
|
||||
->route('helpdesk.tickets.show', $fresh)
|
||||
->with('ticket-action-updated', $agentId === null
|
||||
? 'Ticket is now unassigned.'
|
||||
: 'Ticket assigned to ' . ($fresh->agent?->name ?? 'selected agent') . '.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist the selected ticket status from the ticket detail controls.
|
||||
*/
|
||||
public function updateStatus(Request $request, Ticket $ticket, WorkflowNotificationService $workflow, SlaService $sla, TicketService $tickets): RedirectResponse
|
||||
{
|
||||
$this->hydrateUserRelations($ticket);
|
||||
|
||||
Gate::authorize('update', $ticket);
|
||||
|
||||
$validated = $request->validate([
|
||||
'status_id' => ['required', 'integer', Rule::exists('helpdesk_statuses', 'id')],
|
||||
'closure_comment' => ['nullable', 'string', 'min:10', 'max:5000'],
|
||||
'closure_internal' => ['nullable', 'boolean'],
|
||||
]);
|
||||
|
||||
$status = Status::query()->findOrFail((int) $validated['status_id']);
|
||||
|
||||
if ($status->is_closed) {
|
||||
validator($validated, [
|
||||
'closure_comment' => ['required', 'string', 'min:10', 'max:5000'],
|
||||
], [
|
||||
'closure_comment.required' => 'A closure comment is required before closing the ticket.',
|
||||
'closure_comment.min' => 'The closure comment must explain why the ticket is being closed.',
|
||||
])->validate();
|
||||
}
|
||||
|
||||
$this->updateTicket($ticket, [
|
||||
'status_id' => $status->id,
|
||||
'completed_at' => $status->is_closed ? now() : null,
|
||||
'last_activity_at' => now(),
|
||||
]);
|
||||
|
||||
$this->ensureBoardContainsStatus($ticket, (int) $status->id);
|
||||
|
||||
$fresh = $this->freshTicketForAction($ticket);
|
||||
$actor = $this->actor();
|
||||
|
||||
if ($status->is_closed) {
|
||||
$tickets->comment(
|
||||
$fresh,
|
||||
$actor,
|
||||
trim((string) $validated['closure_comment']),
|
||||
(bool) ($validated['closure_internal'] ?? false),
|
||||
[],
|
||||
null,
|
||||
'Closure comment',
|
||||
);
|
||||
|
||||
$fresh = $this->freshTicketForAction($ticket);
|
||||
$sla->recordClosure($fresh);
|
||||
}
|
||||
|
||||
$workflow->dispatch($fresh, $status->is_closed ? 'ticket.closed' : 'ticket.updated', $actor);
|
||||
|
||||
return redirect()
|
||||
->route('helpdesk.tickets.show', $fresh)
|
||||
->with('ticket-action-updated', 'Status updated to ' . ($fresh->status?->name ?? 'selected status') . '.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist the selected ticket priority from the ticket detail controls.
|
||||
*/
|
||||
public function updatePriority(Request $request, Ticket $ticket, WorkflowNotificationService $workflow): RedirectResponse
|
||||
{
|
||||
$this->hydrateUserRelations($ticket);
|
||||
|
||||
Gate::authorize('update', $ticket);
|
||||
|
||||
$validated = $request->validate([
|
||||
'priority_id' => ['required', 'integer', Rule::exists('helpdesk_priorities', 'id')],
|
||||
]);
|
||||
|
||||
$this->updateTicket($ticket, [
|
||||
'priority_id' => (int) $validated['priority_id'],
|
||||
'last_activity_at' => now(),
|
||||
]);
|
||||
|
||||
$fresh = $this->freshTicketForAction($ticket);
|
||||
$workflow->dispatch($fresh, 'ticket.updated', $this->actor());
|
||||
|
||||
return redirect()
|
||||
->route('helpdesk.tickets.show', $fresh)
|
||||
->with('ticket-action-updated', 'Priority updated to ' . ($fresh->priority?->name ?? 'selected priority') . '.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a direct guarded ticket update without relying on Livewire hydration.
|
||||
*
|
||||
* @param array<string, mixed> $updates
|
||||
*/
|
||||
private function updateTicket(Ticket $ticket, array $updates): void
|
||||
{
|
||||
if (Schema::hasColumn('helpdesk_tickets', 'updated_at')) {
|
||||
$updates['updated_at'] = now();
|
||||
}
|
||||
|
||||
DB::table('helpdesk_tickets')
|
||||
->where('id', $ticket->id)
|
||||
->update($updates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload the ticket after a control update without issuing separate owner/agent relation queries.
|
||||
*/
|
||||
private function freshTicketForAction(Ticket $ticket): Ticket
|
||||
{
|
||||
$fresh = $ticket->fresh([
|
||||
'status',
|
||||
'priority',
|
||||
'category',
|
||||
'project',
|
||||
'team',
|
||||
'product',
|
||||
]) ?? $ticket;
|
||||
|
||||
$this->hydrateUserRelations($fresh);
|
||||
|
||||
return $fresh;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load requester and assigned agent in one user query and attach all aliases used downstream.
|
||||
*/
|
||||
private function hydrateUserRelations(Ticket $ticket): void
|
||||
{
|
||||
$userIds = collect([$ticket->user_id, $ticket->agent_id])
|
||||
->filter()
|
||||
->map(static fn (mixed $id): int => (int) $id)
|
||||
->unique()
|
||||
->values();
|
||||
|
||||
if ($userIds->isEmpty()) {
|
||||
$ticket->setRelation('user', null);
|
||||
$ticket->setRelation('owner', null);
|
||||
$ticket->setRelation('agent', null);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$users = User::query()
|
||||
->with('company')
|
||||
->whereIn('id', $userIds)
|
||||
->get()
|
||||
->keyBy('id');
|
||||
|
||||
$owner = $users->get((int) $ticket->user_id);
|
||||
$agent = $ticket->agent_id === null ? null : $users->get((int) $ticket->agent_id);
|
||||
|
||||
$ticket->setRelation('user', $owner);
|
||||
$ticket->setRelation('owner', $owner);
|
||||
$ticket->setRelation('agent', $agent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep the active Kanban board usable when a ticket is moved to a status not yet assigned to the board.
|
||||
*/
|
||||
private function ensureBoardContainsStatus(Ticket $ticket, int $statusId): void
|
||||
{
|
||||
if ($ticket->board_id === null || ! Schema::hasTable('helpdesk_kanban_board_statuses')) {
|
||||
return;
|
||||
}
|
||||
|
||||
DB::table('helpdesk_kanban_board_statuses')->updateOrInsert(
|
||||
['board_id' => $ticket->board_id, 'status_id' => $statusId],
|
||||
['updated_at' => now(), 'created_at' => now()],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the authenticated user with company loaded for workflow rendering.
|
||||
*/
|
||||
private function actor(): User
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = Auth::user();
|
||||
$user->loadMissing('company');
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert form select values to nullable integers.
|
||||
*/
|
||||
private function nullableInt(mixed $value): ?int
|
||||
{
|
||||
if ($value === null || $value === '' || $value === '0' || $value === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (int) $value;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user