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"
}