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> */ 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 $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(), ], ); } } }