From bfea992994f281ce72bb1f96d083fcdd6d3ca31a Mon Sep 17 00:00:00 2001 From: Mark Gregory Date: Wed, 17 Jun 2026 14:03:54 +0100 Subject: [PATCH] workign kaban board --- .../TicketAttachmentStoreController.php | 3 +- .../TicketCommentStoreController.php | 1 + Modules/Helpdesk/Livewire/KanbanBoard.php | 67 +++++++++------- .../views/livewire/kanban-board.blade.php | 77 ++++++++++++++++--- .../views/livewire/ticket-show.blade.php | 17 ++-- .../Helpdesk/Services/AttachmentService.php | 4 + Modules/Helpdesk/changelog.yaml | 16 ++++ Modules/Helpdesk/module.json | 2 +- 8 files changed, 140 insertions(+), 47 deletions(-) diff --git a/Modules/Helpdesk/Http/Controllers/TicketAttachmentStoreController.php b/Modules/Helpdesk/Http/Controllers/TicketAttachmentStoreController.php index 31670ba7..8c50b0f8 100644 --- a/Modules/Helpdesk/Http/Controllers/TicketAttachmentStoreController.php +++ b/Modules/Helpdesk/Http/Controllers/TicketAttachmentStoreController.php @@ -33,7 +33,8 @@ final class TicketAttachmentStoreController extends Controller abort_if($user === null, 403); $uploadedFiles = $request->file('attachments', []); - $uploadedFiles = is_array($uploadedFiles) ? array_values(array_filter($uploadedFiles)) : [$uploadedFiles]; + $uploadedFiles = is_array($uploadedFiles) ? $uploadedFiles : [$uploadedFiles]; + $uploadedFiles = array_values(array_filter($uploadedFiles, static fn ($file): bool => $file instanceof \Illuminate\Http\UploadedFile && $file->isValid())); $attachments->storeMany($ticket, $uploadedFiles, (int) $user->id); diff --git a/Modules/Helpdesk/Http/Controllers/TicketCommentStoreController.php b/Modules/Helpdesk/Http/Controllers/TicketCommentStoreController.php index 75556d36..dda2b90c 100644 --- a/Modules/Helpdesk/Http/Controllers/TicketCommentStoreController.php +++ b/Modules/Helpdesk/Http/Controllers/TicketCommentStoreController.php @@ -74,6 +74,7 @@ final class TicketCommentStoreController extends Controller $uploadedFiles = $request->file('attachments', []); $uploadedFiles = is_array($uploadedFiles) ? $uploadedFiles : [$uploadedFiles]; + $uploadedFiles = array_values(array_filter($uploadedFiles, static fn ($file): bool => $file instanceof \Illuminate\Http\UploadedFile && $file->isValid())); $tickets->comment( ticket: $ticket, diff --git a/Modules/Helpdesk/Livewire/KanbanBoard.php b/Modules/Helpdesk/Livewire/KanbanBoard.php index d12fc052..5a035276 100644 --- a/Modules/Helpdesk/Livewire/KanbanBoard.php +++ b/Modules/Helpdesk/Livewire/KanbanBoard.php @@ -21,6 +21,8 @@ final class KanbanBoard extends Component public int $refreshNonce = 0; + public bool $isMovingTicket = false; + public function mount(): void { $this->board_id = KanbanBoardModel::query() @@ -34,36 +36,48 @@ final class KanbanBoard extends Component */ public function moveTicket(int|string $ticketId, int|string $statusId, WorkflowNotificationService $workflow): void { - $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); + if ($this->isMovingTicket) { + return; } - $ticket->forceFill([ - 'board_id' => $board?->id, - 'status_id' => $status->id, - 'completed_at' => $status->is_closed ? now() : null, - 'last_activity_at' => now(), - ])->save(); + $this->isMovingTicket = true; - $workflow->dispatch( - $ticket->fresh(['status', 'priority', 'category', 'project', 'team']) ?? $ticket, - $status->is_closed ? 'ticket.closed' : 'ticket.updated', - Auth::user() - ); + 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'; - $message = 'Ticket '.$ticket->reference.' moved from '.$oldStatusName.' to '.$status->name.'.'; - session()->flash('success', $message); + $this->authorize('update', $ticket); - $this->refreshBoard(); - $this->dispatch('helpdesk-kanban-refresh'); - $this->dispatch('wms:toast', type: 'success', title: 'Ticket moved', message: $message); + 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(['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('wms:toast', type: 'success', title: 'Ticket moved', message: $message); + } finally { + $this->isMovingTicket = false; + } } /** @@ -102,7 +116,7 @@ final class KanbanBoard extends Component session()->flash('success', $message); $this->refreshBoard(); - $this->dispatch('helpdesk-kanban-refresh'); + $this->dispatch('helpdesk-kanban-moved', ticketId: (int) $fresh->id, statusId: (int) $fresh->status_id); $this->dispatch('wms:toast', type: 'success', title: 'Board changed', message: $message); } @@ -113,6 +127,7 @@ final class KanbanBoard extends Component public function refreshBoard(): void { $this->refreshNonce++; + unset($this->ticketsByStatus); } /** diff --git a/Modules/Helpdesk/Resources/views/livewire/kanban-board.blade.php b/Modules/Helpdesk/Resources/views/livewire/kanban-board.blade.php index 15625934..acef2684 100644 --- a/Modules/Helpdesk/Resources/views/livewire/kanban-board.blade.php +++ b/Modules/Helpdesk/Resources/views/livewire/kanban-board.blade.php @@ -263,16 +263,18 @@

{{ $status->name }}

-

{{ $tickets->count() }} tickets

+

{{ $tickets->count() }} tickets

@forelse ($tickets as $ticket)
const initialiseHelpdeskKanban = function () { let draggedTicketId = null; + let draggedCard = null; + + const updateColumnCounts = function () { + document.querySelectorAll('[data-helpdesk-kanban-count]').forEach(function (counter) { + const statusId = counter.dataset.statusId; + const column = document.querySelector('[data-helpdesk-kanban-column][data-status-id="' + statusId + '"]'); + + if (!column) { + return; + } + + counter.textContent = column.querySelectorAll('[data-helpdesk-kanban-ticket]').length.toString(); + }); + }; + + const forceLivewireRefresh = function () { + if (typeof $wire !== 'undefined' && typeof $wire.$refresh === 'function') { + return Promise.resolve($wire.$refresh()); + } + + if (typeof Livewire !== 'undefined' && typeof Livewire.find === 'function') { + const root = document.querySelector('[wire\\:id]'); + const component = root ? Livewire.find(root.getAttribute('wire:id')) : null; + + if (component && typeof component.$refresh === 'function') { + return Promise.resolve(component.$refresh()); + } + } + + window.location.reload(); + + return Promise.resolve(); + }; document.querySelectorAll('[data-helpdesk-kanban-ticket]').forEach(function (card) { if (card.dataset.nativeDragReady === '1') return; @@ -372,8 +407,10 @@ card.addEventListener('dragstart', function (event) { draggedTicketId = card.dataset.ticketId; + draggedCard = card; event.dataTransfer.effectAllowed = 'move'; event.dataTransfer.setData('text/plain', draggedTicketId); + event.dataTransfer.setData('application/x-helpdesk-ticket-id', draggedTicketId); card.classList.add('opacity-60'); }); @@ -381,6 +418,7 @@ card.classList.remove('opacity-60'); document.querySelectorAll('[data-helpdesk-kanban-column]').forEach(function (column) { column.dataset.dragOver = 'false'; + column.dataset.refreshing = 'false'; }); }); }); @@ -405,26 +443,43 @@ event.preventDefault(); column.dataset.dragOver = 'false'; - const ticketId = event.dataTransfer.getData('text/plain') || draggedTicketId; + const ticketId = event.dataTransfer.getData('application/x-helpdesk-ticket-id') || event.dataTransfer.getData('text/plain') || draggedTicketId; const statusId = column.dataset.statusId; + const card = draggedCard || document.querySelector('[data-helpdesk-kanban-ticket][data-ticket-id="' + ticketId + '"]'); - if (!ticketId || !statusId) return; + if (!ticketId || !statusId || !card) return; - Promise.resolve($wire.moveTicket(ticketId, statusId)).then(function () { - setTimeout(function () { - if (typeof $wire.$refresh === 'function') { - $wire.$refresh(); - } - initialiseHelpdeskKanban(); - }, 100); - }); + column.dataset.refreshing = 'true'; + column.appendChild(card); + updateColumnCounts(); + + Promise.resolve($wire.moveTicket(ticketId, statusId)) + .then(forceLivewireRefresh) + .finally(function () { + column.dataset.refreshing = 'false'; + draggedTicketId = null; + draggedCard = null; + updateColumnCounts(); + setTimeout(initialiseHelpdeskKanban, 50); + }); }); }); + + updateColumnCounts(); }; initialiseHelpdeskKanban(); document.addEventListener('livewire:navigated', initialiseHelpdeskKanban); document.addEventListener('livewire:updated', initialiseHelpdeskKanban); + document.addEventListener('helpdesk-kanban-moved', function () { + setTimeout(function () { + initialiseHelpdeskKanban(); + + if (typeof $wire !== 'undefined' && typeof $wire.$refresh === 'function') { + $wire.$refresh(); + } + }, 50); + }); @endscript
diff --git a/Modules/Helpdesk/Resources/views/livewire/ticket-show.blade.php b/Modules/Helpdesk/Resources/views/livewire/ticket-show.blade.php index 87260523..273a48ed 100644 --- a/Modules/Helpdesk/Resources/views/livewire/ticket-show.blade.php +++ b/Modules/Helpdesk/Resources/views/livewire/ticket-show.blade.php @@ -233,10 +233,10 @@ align-items: center !important; justify-content: center !important; gap: 12px !important; - width: 190px !important; - min-width: 190px !important; - height: 56px !important; - min-height: 56px !important; + width: 210px !important; + min-width: 210px !important; + height: 58px !important; + min-height: 58px !important; padding: 0 22px !important; border: 1px solid rgba(134, 239, 172, 0.65) !important; border-radius: 18px !important; @@ -723,15 +723,16 @@ const fileInput = event.target.closest('[data-helpdesk-file-input]'); if (fileInput) { - const panel = fileInput.closest('details') || fileInput.closest('div'); + const panel = fileInput.closest('[data-helpdesk-ticket-attachment-form], [data-helpdesk-comment-form], details, form'); const label = panel?.querySelector('[data-helpdesk-file-label]'); const count = fileInput.files?.length || 0; - const text = count === 0 ? 'No files selected' : `${count} file${count === 1 ? '' : 's'} selected`; + const names = Array.from(fileInput.files || []).map((file) => file.name); if (label) { - label.textContent = text; + label.textContent = count === 0 + ? 'No files selected' + : `${count} file${count === 1 ? '' : 's'} selected${names.length ? ': ' + names.join(', ') : ''}`; } - } }); diff --git a/Modules/Helpdesk/Services/AttachmentService.php b/Modules/Helpdesk/Services/AttachmentService.php index 29f446d1..404a87f5 100644 --- a/Modules/Helpdesk/Services/AttachmentService.php +++ b/Modules/Helpdesk/Services/AttachmentService.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Modules\Helpdesk\Services; +use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\Storage; use Modules\Helpdesk\Models\Attachment; use Modules\Helpdesk\Models\Comment; @@ -14,6 +15,9 @@ final class AttachmentService public function storeMany(Ticket $ticket, array $files, int $userId, ?Comment $comment = null): void { foreach ($files as $file) { + if (! $file instanceof UploadedFile || ! $file->isValid()) { + continue; + } $disk = (string) config('helpdesk.disk', 'public'); $originalName = $file->getClientOriginalName(); diff --git a/Modules/Helpdesk/changelog.yaml b/Modules/Helpdesk/changelog.yaml index 4ee517fa..8ead24dc 100644 --- a/Modules/Helpdesk/changelog.yaml +++ b/Modules/Helpdesk/changelog.yaml @@ -355,3 +355,19 @@ versions: - 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. diff --git a/Modules/Helpdesk/module.json b/Modules/Helpdesk/module.json index 8ca6d28c..e483c90c 100644 --- a/Modules/Helpdesk/module.json +++ b/Modules/Helpdesk/module.json @@ -22,5 +22,5 @@ "Modules\\Helpdesk\\Providers\\HelpdeskServiceProvider" ], "files": [], - "version": "1.9.21" + "version": "1.9.23" }