workign kaban board
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -263,16 +263,18 @@
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<h2 class="text-sm font-bold uppercase tracking-wide text-zinc-900 dark:text-zinc-100">{{ $status->name }}</h2>
|
||||
<p class="mt-1 text-xs text-zinc-500">{{ $tickets->count() }} tickets</p>
|
||||
<p class="mt-1 text-xs text-zinc-500"><span data-helpdesk-kanban-count data-status-id="{{ $status->id }}">{{ $tickets->count() }}</span> tickets</p>
|
||||
</div>
|
||||
<span class="inline-flex h-4 w-4 rounded-full ring-4 ring-zinc-100 dark:ring-zinc-800" style="background-color: {{ \Modules\Helpdesk\Support\Color::css($status->color) }}"></span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div
|
||||
id="helpdesk-kanban-column-{{ $status->id }}"
|
||||
class="helpdesk-kanban-column min-h-[560px] flex-1 space-y-3 p-3 transition data-[drag-over=true]:bg-blue-50 dark:data-[drag-over=true]:bg-blue-950/30"
|
||||
data-helpdesk-kanban-column
|
||||
data-status-id="{{ $status->id }}"
|
||||
wire:key="kanban-dropzone-{{ $board?->id ?? 0 }}-{{ $status->id }}-{{ $refreshNonce }}"
|
||||
>
|
||||
@forelse ($tickets as $ticket)
|
||||
<article
|
||||
@@ -365,6 +367,39 @@
|
||||
<script>
|
||||
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);
|
||||
});
|
||||
</script>
|
||||
@endscript
|
||||
</div>
|
||||
|
||||
@@ -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(', ') : ''}`;
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -22,5 +22,5 @@
|
||||
"Modules\\Helpdesk\\Providers\\HelpdeskServiceProvider"
|
||||
],
|
||||
"files": [],
|
||||
"version": "1.9.21"
|
||||
"version": "1.9.23"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user