297 lines
10 KiB
PHP
Executable File
297 lines
10 KiB
PHP
Executable File
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Modules\Helpdesk\Livewire;
|
|
|
|
use Illuminate\Contracts\View\View;
|
|
use Illuminate\Database\Eloquent\Collection;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Livewire\Attributes\On;
|
|
use Livewire\Component;
|
|
use Modules\Helpdesk\Models\KanbanBoard as KanbanBoardModel;
|
|
use Modules\Helpdesk\Models\Status;
|
|
use Modules\Helpdesk\Models\Ticket;
|
|
use Modules\Helpdesk\Services\WorkflowNotificationService;
|
|
|
|
final class KanbanBoard extends Component
|
|
{
|
|
public ?int $board_id = null;
|
|
|
|
public int $refreshNonce = 0;
|
|
|
|
public bool $isMovingTicket = false;
|
|
|
|
public function mount(): void
|
|
{
|
|
$requestedBoardId = request()->query('board_id');
|
|
|
|
if (is_numeric($requestedBoardId)) {
|
|
$requestedBoardId = (int) $requestedBoardId;
|
|
|
|
if (KanbanBoardModel::query()->whereKey($requestedBoardId)->exists()) {
|
|
$this->board_id = $requestedBoardId;
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
$this->board_id = KanbanBoardModel::query()
|
|
->where('is_default', true)
|
|
->orderBy('sort_order')
|
|
->value('id') ?: KanbanBoardModel::query()->orderBy('sort_order')->value('id');
|
|
}
|
|
|
|
/**
|
|
* Move a ticket to another workflow status on the active board.
|
|
*/
|
|
public function moveTicket(int|string $ticketId, int|string $statusId, WorkflowNotificationService $workflow): void
|
|
{
|
|
if ($this->isMovingTicket) {
|
|
return;
|
|
}
|
|
|
|
$this->isMovingTicket = true;
|
|
|
|
try {
|
|
$ticket = Ticket::query()->visibleTo(Auth::user())->findOrFail((int) $ticketId);
|
|
$status = Status::query()->findOrFail((int) $statusId);
|
|
$board = $this->activeBoard();
|
|
$oldStatusName = $ticket->status?->name ?? 'previous status';
|
|
|
|
$this->authorize('update', $ticket);
|
|
|
|
if ($board !== null && ! $board->statuses()->whereKey($status->id)->exists()) {
|
|
$this->addStatusToBoard($board->id, $status->id);
|
|
}
|
|
|
|
$ticket->forceFill([
|
|
'board_id' => $board?->id,
|
|
'status_id' => $status->id,
|
|
'completed_at' => $status->is_closed ? now() : null,
|
|
'last_activity_at' => now(),
|
|
])->save();
|
|
|
|
$freshTicket = $ticket->fresh(['user:id,name,email', 'agent:id,name,email', 'status', 'priority', 'category', 'project', 'team', 'product', 'tags', 'checklistItems', 'board']) ?? $ticket;
|
|
|
|
$workflow->dispatch(
|
|
$freshTicket,
|
|
$status->is_closed ? 'ticket.closed' : 'ticket.updated',
|
|
Auth::user(),
|
|
);
|
|
|
|
$message = 'Ticket '.$freshTicket->reference.' moved from '.$oldStatusName.' to '.$status->name.'.';
|
|
session()->flash('success', $message);
|
|
|
|
$this->refreshBoard();
|
|
$this->dispatch('helpdesk-kanban-moved', ticketId: (int) $freshTicket->id, statusId: (int) $freshTicket->status_id);
|
|
$this->dispatch('thredary:toast', type: 'success', title: 'Ticket moved', message: $message);
|
|
} finally {
|
|
$this->isMovingTicket = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Move a ticket from its current board to another board. A ticket can only belong to one board.
|
|
*/
|
|
public function moveTicketToBoard(int|string $ticketId, int|string|null $boardId, WorkflowNotificationService $workflow): void
|
|
{
|
|
$ticket = Ticket::query()->with('board')->visibleTo(Auth::user())->findOrFail((int) $ticketId);
|
|
$board = KanbanBoardModel::query()->with(['statuses', 'owner:id,name,email'])->findOrFail((int) $boardId);
|
|
$oldBoardName = $ticket->board?->name ?? 'No board';
|
|
|
|
$this->authorize('update', $ticket);
|
|
|
|
if (! $board->statuses->contains('id', $ticket->status_id)) {
|
|
$firstStatus = $board->statuses->first();
|
|
|
|
if ($firstStatus !== null) {
|
|
$ticket->status_id = $firstStatus->id;
|
|
$ticket->completed_at = $firstStatus->is_closed ? now() : null;
|
|
} else {
|
|
$this->addStatusToBoard($board->id, (int) $ticket->status_id);
|
|
}
|
|
}
|
|
|
|
$ticket->forceFill([
|
|
'board_id' => $board->id,
|
|
'last_activity_at' => now(),
|
|
])->save();
|
|
|
|
$this->board_id = $board->id;
|
|
|
|
$fresh = $ticket->fresh(['user:id,name,email', 'agent:id,name,email', 'status', 'priority', 'category', 'project', 'team', 'board']) ?? $ticket;
|
|
$workflow->dispatch($fresh, 'ticket.updated', Auth::user());
|
|
|
|
$message = 'Ticket '.$fresh->reference.' moved from '.$oldBoardName.' to '.$board->name.'.';
|
|
session()->flash('success', $message);
|
|
|
|
$this->refreshBoard();
|
|
$this->dispatch('helpdesk-kanban-moved', ticketId: (int) $fresh->id, statusId: (int) $fresh->status_id);
|
|
$this->dispatch('thredary:toast', type: 'success', title: 'Board changed', message: $message);
|
|
}
|
|
|
|
/**
|
|
* Force the board to fetch fresh database rows after moves or polling.
|
|
*/
|
|
#[On('helpdesk-kanban-refresh')]
|
|
public function refreshBoard(): void
|
|
{
|
|
$this->refreshNonce++;
|
|
unset($this->ticketsByStatus);
|
|
}
|
|
|
|
/**
|
|
* Normalise the board selector value from Livewire/browser select events.
|
|
*/
|
|
public function updatedBoardId(int|string|null $value): void
|
|
{
|
|
$this->board_id = is_numeric($value) ? (int) $value : null;
|
|
$this->refreshBoard();
|
|
}
|
|
|
|
/**
|
|
* Switch the visible board without relying on DOM morph side effects.
|
|
*/
|
|
public function selectBoard(int|string|null $value): void
|
|
{
|
|
$this->board_id = is_numeric($value) ? (int) $value : null;
|
|
$this->refreshBoard();
|
|
}
|
|
|
|
/**
|
|
* Render the Kanban board.
|
|
*/
|
|
public function render(): View
|
|
{
|
|
$boards = KanbanBoardModel::query()
|
|
->with(['statuses', 'owner:id,name,email'])
|
|
->orderBy('sort_order')
|
|
->orderBy('name')
|
|
->get();
|
|
|
|
$board = $this->resolveSelectedBoard($boards);
|
|
$this->board_id = $board?->id;
|
|
|
|
if ($board instanceof KanbanBoardModel && $board->statuses->isEmpty()) {
|
|
$this->ensureBoardHasStatuses($board);
|
|
$board->load(['statuses', 'owner:id,name,email']);
|
|
|
|
$boards = KanbanBoardModel::query()
|
|
->with(['statuses', 'owner:id,name,email'])
|
|
->orderBy('sort_order')
|
|
->orderBy('name')
|
|
->get();
|
|
}
|
|
|
|
return view('modules.helpdesk.livewire.kanban-board', [
|
|
'boards' => $boards,
|
|
'board' => $board,
|
|
'statuses' => $board?->statuses ?? collect(),
|
|
'ticketsByStatus' => $this->ticketsByStatus($board),
|
|
'refreshNonce' => $this->refreshNonce,
|
|
])->layout('layouts.app');
|
|
}
|
|
|
|
/**
|
|
* Get visible tickets grouped by status for the selected board only.
|
|
*
|
|
* @return array<int, Collection<int, Ticket>>
|
|
*/
|
|
private function ticketsByStatus(?KanbanBoardModel $board): array
|
|
{
|
|
if ($board === null) {
|
|
return [];
|
|
}
|
|
|
|
return Ticket::query()
|
|
->leftJoin('users as helpdesk_agents', 'helpdesk_agents.id', '=', 'helpdesk_tickets.agent_id')
|
|
->select('helpdesk_tickets.*', 'helpdesk_agents.name as agent_name')
|
|
->with(['user:id,name,email', 'status', 'priority', 'category', 'project', 'team', 'product', 'tags', 'checklistItems', 'board'])
|
|
->visibleTo(Auth::user())
|
|
->where('board_id', $board->id)
|
|
->latest('last_activity_at')
|
|
->limit(300)
|
|
->get()
|
|
->groupBy('status_id')
|
|
->all();
|
|
}
|
|
|
|
/**
|
|
* Resolve the current board from the selected board ID.
|
|
*/
|
|
private function activeBoard(): ?KanbanBoardModel
|
|
{
|
|
if ($this->board_id !== null) {
|
|
return KanbanBoardModel::query()->find((int) $this->board_id);
|
|
}
|
|
|
|
return KanbanBoardModel::query()
|
|
->where('is_default', true)
|
|
->orderBy('sort_order')
|
|
->first() ?: KanbanBoardModel::query()->orderBy('sort_order')->first();
|
|
}
|
|
|
|
/**
|
|
* Resolve the selected board from an already-loaded board collection.
|
|
*
|
|
* @param Collection<int, KanbanBoardModel> $boards
|
|
*/
|
|
private function resolveSelectedBoard(Collection $boards): ?KanbanBoardModel
|
|
{
|
|
if ($boards->isEmpty()) {
|
|
return null;
|
|
}
|
|
|
|
if ($this->board_id !== null) {
|
|
$selected = $boards->firstWhere('id', (int) $this->board_id);
|
|
|
|
if ($selected instanceof KanbanBoardModel) {
|
|
return $selected;
|
|
}
|
|
}
|
|
|
|
$default = $boards->firstWhere('is_default', true);
|
|
|
|
return $default instanceof KanbanBoardModel ? $default : $boards->first();
|
|
}
|
|
|
|
/**
|
|
* Attach a missing status to a board when a move requires it.
|
|
*/
|
|
private function addStatusToBoard(int $boardId, int $statusId): void
|
|
{
|
|
DB::table('helpdesk_kanban_board_statuses')->updateOrInsert(
|
|
['board_id' => $boardId, 'status_id' => $statusId],
|
|
['sort_order' => 999, 'updated_at' => now(), 'created_at' => now()],
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Ensure a board has workflow columns. This keeps newly-created boards usable immediately.
|
|
*/
|
|
private function ensureBoardHasStatuses(KanbanBoardModel $board): void
|
|
{
|
|
if ($board->statuses()->exists()) {
|
|
return;
|
|
}
|
|
|
|
$statuses = Status::query()
|
|
->orderBy('sort_order')
|
|
->orderBy('name')
|
|
->get(['id']);
|
|
|
|
foreach ($statuses as $index => $status) {
|
|
DB::table('helpdesk_kanban_board_statuses')->updateOrInsert(
|
|
['board_id' => $board->id, 'status_id' => $status->id],
|
|
[
|
|
'sort_order' => ($index + 1) * 10,
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
}
|