ui updates for ticket layout now with multipel attachments

This commit is contained in:
Mark Gregory
2026-06-17 13:23:52 +01:00
parent 4bf1604542
commit a3ac516138
8 changed files with 187 additions and 102 deletions
@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Modules\Helpdesk\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Gate;
use Modules\Helpdesk\Models\Ticket;
use Modules\Helpdesk\Services\AttachmentService;
final class TicketAttachmentStoreController extends Controller
{
/**
* Store one or more ticket-level supporting files for an existing ticket.
*/
public function __invoke(Request $request, Ticket $ticket, AttachmentService $attachments): RedirectResponse
{
Gate::authorize('update', $ticket);
$maxUploadKilobytes = (int) config('helpdesk.max_upload_kb', 10240);
$allowedMimes = (array) config('helpdesk.allowed_mimes', ['jpg', 'jpeg', 'png', 'pdf', 'txt', 'log', 'zip']);
$request->validate([
'attachments' => ['required', 'array', 'min:1'],
'attachments.*' => ['required', 'file', 'max:'.$maxUploadKilobytes, 'mimes:'.implode(',', $allowedMimes)],
], [
'attachments.required' => 'Choose at least one supporting file.',
'attachments.*.file' => 'Each supporting file must be a valid uploaded file.',
]);
$user = Auth::user();
abort_if($user === null, 403);
$uploadedFiles = $request->file('attachments', []);
$uploadedFiles = is_array($uploadedFiles) ? array_values($uploadedFiles) : [$uploadedFiles];
$attachments->storeMany($ticket, $uploadedFiles, (int) $user->id);
$ticket->forceFill(['last_activity_at' => now()])->save();
return redirect()
->route('helpdesk.tickets.show', $ticket)
->with('ticket-attachments-added', count($uploadedFiles).' supporting file'.(count($uploadedFiles) === 1 ? '' : 's').' attached.');
}
}
+19 -3
View File
@@ -8,6 +8,7 @@ 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;
@@ -55,12 +56,13 @@ final class KanbanBoard extends Component
Auth::user()
);
$this->board_id = $board?->id;
$this->skipRender();
$message = 'Ticket '.$ticket->reference.' moved from '.$oldStatusName.' to '.$status->name.'.';
session()->flash('success', $message);
$this->dispatch('wms:toast', type: 'success', title: 'Ticket moved', message: $message);
$this->dispatch('helpdesk-kanban-ticket-moved', ticketId: (int) $ticket->id, statusId: (int) $status->id);
$this->dispatch('helpdesk-kanban-refresh');
$this->dispatch('helpdesk-kanban-ticket-moved', ticketId: $ticket->id, statusId: $status->id);
}
/**
@@ -95,10 +97,13 @@ final class KanbanBoard extends Component
$fresh = $ticket->fresh(['status', 'priority', 'category', 'project', 'team', 'board']) ?? $ticket;
$workflow->dispatch($fresh, 'ticket.updated', Auth::user());
$this->skipRender();
$message = 'Ticket '.$fresh->reference.' moved from '.$oldBoardName.' to '.$board->name.'.';
session()->flash('success', $message);
$this->dispatch('wms:toast', type: 'success', title: 'Board changed', message: $message);
$this->dispatch('helpdesk-kanban-ticket-moved', ticketId: (int) $fresh->id, statusId: (int) $fresh->status_id);
$this->dispatch('helpdesk-kanban-refresh');
$this->dispatch('helpdesk-kanban-ticket-moved', ticketId: $fresh->id, boardId: $board->id);
}
/**
@@ -109,6 +114,17 @@ final class KanbanBoard extends Component
$this->board_id = is_numeric($value) ? (int) $value : null;
}
/**
* Refresh the board after a drag/drop move, board change or external ticket update.
*/
#[On('helpdesk-kanban-refresh')]
#[On('ticket-updated')]
#[On('ticket-moved')]
public function refreshBoard(): void
{
// Livewire will re-run render(), which reloads all board columns from the database.
}
/**
* Render the Kanban board.
*/
+6 -4
View File
@@ -108,6 +108,7 @@ final class TicketEdit extends Component
$this->validate();
$this->validate([
'attachments' => 'array',
'attachments.*' => 'file|max:'.config('helpdesk.max_upload_kb', 10240).'|mimes:'.implode(',', config('helpdesk.allowed_mimes', ['jpg', 'jpeg', 'png', 'pdf', 'txt', 'log', 'zip'])),
'tag_ids.*' => 'integer|exists:helpdesk_tags,id',
]);
@@ -274,8 +275,9 @@ final class TicketEdit extends Component
continue;
}
$path = $upload->store('helpdesk/tickets/'.$this->ticket->id, 'public');
$this->ticket->attachments()->create($this->attachmentPayload($upload, $path));
$disk = (string) config('helpdesk.disk', 'local');
$path = $upload->store('tickets/'.$this->ticket->id, $disk);
$this->ticket->attachments()->create($this->attachmentPayload($upload, $path, $disk));
}
$this->attachments = [];
@@ -287,14 +289,14 @@ final class TicketEdit extends Component
*
* @return array<string, mixed>
*/
private function attachmentPayload(TemporaryUploadedFile $upload, string $path): array
private function attachmentPayload(TemporaryUploadedFile $upload, string $path, string $disk): array
{
$table = 'helpdesk_attachments';
$payload = [];
$columns = [
'user_id' => Auth::id(),
'uploaded_by_id' => Auth::id(),
'disk' => 'public',
'disk' => $disk,
'path' => $path,
'file_path' => $path,
'filename' => basename($path),
@@ -199,7 +199,7 @@
}
</style>
<div class="hd-helpdesk-surface mx-auto max-w-[1800px] space-y-6" dusk="helpdesk-kanban-board">
<div class="hd-helpdesk-surface mx-auto max-w-[1800px] space-y-6" dusk="helpdesk-kanban-board" wire:poll.15s="refreshBoard">
<div class="overflow-hidden rounded-3xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
<div class="bg-gradient-to-br from-slate-950 via-slate-900 to-blue-950 px-6 py-7 text-white">
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
@@ -254,11 +254,11 @@
This board is being initialised with the default workflow statuses. Refresh the page if the columns do not appear immediately.
</div>
@else
<div class="flex gap-4 overflow-x-auto pb-4" data-helpdesk-kanban-board wire:key="helpdesk-kanban-board-{{ $board?->id ?? 'none' }}">
<div class="flex gap-4 overflow-x-auto pb-4" data-helpdesk-kanban-board wire:key="helpdesk-kanban-board-{{ $board?->id }}-{{ now()->timestamp }}">
@foreach ($statuses as $status)
@php($tickets = $ticketsByStatus[$status->id] ?? collect())
<section wire:key="helpdesk-kanban-column-{{ $board?->id }}-{{ $status->id }}" class="flex min-h-[640px] w-[330px] shrink-0 flex-col overflow-hidden rounded-2xl border border-zinc-200 bg-zinc-100 shadow-sm dark:border-zinc-700 dark:bg-zinc-950" dusk="kanban-column-{{ $status->slug }}">
<section class="flex min-h-[640px] w-[330px] shrink-0 flex-col overflow-hidden rounded-2xl border border-zinc-200 bg-zinc-100 shadow-sm dark:border-zinc-700 dark:bg-zinc-950" dusk="kanban-column-{{ $status->slug }}" wire:key="helpdesk-kanban-column-{{ $board?->id }}-{{ $status->id }}">
<header class="border-b border-zinc-200 bg-white px-4 py-4 dark:border-zinc-800 dark:bg-zinc-900">
<div class="flex items-start justify-between gap-3">
<div>
@@ -276,12 +276,12 @@
>
@forelse ($tickets as $ticket)
<article
wire:key="helpdesk-kanban-ticket-{{ $ticket->id }}-{{ $ticket->status_id }}-{{ $ticket->updated_at?->timestamp }}"
class="group cursor-grab rounded-2xl border border-zinc-200 bg-white p-4 shadow-sm transition hover:-translate-y-0.5 hover:shadow-md active:cursor-grabbing dark:border-zinc-800 dark:bg-zinc-900"
draggable="true"
data-helpdesk-kanban-ticket
data-ticket-id="{{ $ticket->id }}"
dusk="kanban-ticket-{{ $ticket->id }}"
wire:key="helpdesk-kanban-ticket-{{ $ticket->id }}-{{ $ticket->status_id }}-{{ optional($ticket->updated_at)->timestamp ?? 0 }}"
>
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
@@ -411,7 +411,7 @@
if (!ticketId || !statusId) return;
$wire.moveTicket(ticketId, statusId).then(function () {
initialiseHelpdeskKanban();
$wire.dispatch('helpdesk-kanban-refresh');
});
});
});
@@ -420,8 +420,10 @@
initialiseHelpdeskKanban();
document.addEventListener('livewire:navigated', initialiseHelpdeskKanban);
document.addEventListener('livewire:updated', initialiseHelpdeskKanban);
document.addEventListener('livewire:update', initialiseHelpdeskKanban);
document.addEventListener('helpdesk-kanban-ticket-moved', initialiseHelpdeskKanban);
document.addEventListener('helpdesk-kanban-ticket-moved', function () {
$wire.dispatch('helpdesk-kanban-refresh');
setTimeout(initialiseHelpdeskKanban, 150);
});
</script>
@endscript
</div>
@@ -3,18 +3,13 @@
'resources/css/app.css',
'resources/js/app.js',
])
@vite([
'Resources/assets/css/app.css',
'Resources/assets/js/app.js',
], 'build/helpdesk')
@include('helpdesk::partials.page-header', [
'eyebrow' => 'Helpdesk',
'title' => 'Edit Ticket',
'subtitle' => 'Update ownership, assignment, workflow status, product/service, project, team, category, priority and ticket details.',
])
<form wire:submit.prevent="save" class="space-y-6">
<form wire:submit.prevent="save" enctype="multipart/form-data" class="space-y-6">
<div class="overflow-hidden rounded-2xl border border-zinc-700 bg-zinc-900 shadow-sm">
<div class="border-b border-zinc-700 bg-zinc-800/80 px-6 py-4">
<div class="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
@@ -307,106 +307,80 @@
</div>
</article>
<section class="overflow-hidden rounded-[2rem] border border-zinc-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-900" data-helpdesk-supporting-files-card>
<div class="bg-gradient-to-r from-slate-950 via-blue-950 to-slate-900 px-7 py-7 text-white sm:px-9">
<div class="flex flex-col gap-5 xl:flex-row xl:items-center xl:justify-between">
<div class="flex min-w-0 items-start gap-5">
<span class="inline-flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl bg-blue-500/25 text-white ring-1 ring-white/20 shadow-lg shadow-black/20">
<i class="fa-solid fa-paperclip text-xl" aria-hidden="true"></i>
</span>
<div class="min-w-0 space-y-2">
<p class="text-xs font-black uppercase tracking-[0.22em] text-blue-200">Ticket attachments</p>
<h2 class="text-2xl font-extrabold tracking-tight sm:text-3xl">Supporting files</h2>
<p class="text-base font-medium leading-7 text-blue-50/85">Attach multiple screenshots, documents, logs and evidence directly to this existing ticket.</p>
</div>
</div>
<span class="inline-flex w-fit shrink-0 items-center gap-3 rounded-2xl bg-white/15 px-5 py-3 text-base font-extrabold ring-1 ring-white/20 shadow-sm">
<i class="fa-solid fa-paperclip text-sm" aria-hidden="true"></i>
{{ $ticket->attachments->count() }} file{{ $ticket->attachments->count() === 1 ? '' : 's' }}
<details open class="overflow-hidden rounded-2xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-900" data-helpdesk-supporting-files-card>
<summary class="flex cursor-pointer list-none items-center justify-between gap-5 bg-gradient-to-r from-slate-950 via-blue-950 to-slate-900 px-7 py-7 text-white marker:hidden sm:px-9 [&::-webkit-details-marker]:hidden">
<div class="flex min-w-0 items-start gap-5">
<span class="inline-flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl bg-blue-500/25 text-white ring-1 ring-white/20 shadow-lg shadow-black/20">
<i class="fa-solid fa-paperclip text-xl" aria-hidden="true"></i>
</span>
<div class="min-w-0 space-y-2">
<p class="text-xs font-black uppercase tracking-[0.22em] text-blue-200">Ticket attachments</p>
<h2 class="text-2xl font-extrabold tracking-tight sm:text-3xl">Supporting files</h2>
<p class="text-base font-medium leading-7 text-blue-50/85">Attach screenshots, documents, logs and evidence directly to this existing ticket.</p>
</div>
</div>
</div>
<div class="space-y-5 p-6 sm:p-7">
@if (session('ticket-supporting-files-added'))
<div class="rounded-2xl border border-emerald-300 bg-emerald-50 px-4 py-3 text-sm font-bold text-emerald-800 dark:border-emerald-800 dark:bg-emerald-950 dark:text-emerald-100">
{{ session('ticket-supporting-files-added') }}
<span class="inline-flex shrink-0 items-center gap-3 rounded-2xl bg-white/15 px-5 py-3 text-base font-extrabold ring-1 ring-white/20 shadow-sm">
<i class="fa-solid fa-paperclip text-sm" aria-hidden="true"></i>
{{ $ticket->attachments->count() }} file{{ $ticket->attachments->count() === 1 ? '' : 's' }}
<i class="fa-solid fa-chevron-right transition" aria-hidden="true"></i>
</span>
</summary>
<div class="space-y-5 p-6">
@if (session('ticket-attachments-added'))
<div class="rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm font-bold text-emerald-800 dark:border-emerald-800 dark:bg-emerald-950 dark:text-emerald-100">
{{ session('ticket-attachments-added') }}
</div>
@endif
@can('comment', $ticket)
<div class="rounded-[1.5rem] border border-blue-200 bg-blue-50/80 p-5 shadow-sm dark:border-blue-900/60 dark:bg-blue-950/30" data-helpdesk-attachment-shell>
<div class="flex min-h-[5.25rem] flex-col gap-4 sm:flex-row sm:items-center sm:justify-between" data-helpdesk-attachment-picker-row>
@can('update', $ticket)
<form method="POST" action="{{ route('helpdesk.tickets.attachments.store', $ticket) }}" enctype="multipart/form-data" class="rounded-2xl border border-zinc-200 bg-zinc-50/80 p-5 shadow-sm dark:border-zinc-800 dark:bg-zinc-950/70" data-helpdesk-supporting-files-form>
@csrf
<div class="flex min-h-[5.25rem] flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div class="flex min-w-0 items-center gap-4">
<span class="inline-flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl border border-blue-200 bg-white text-blue-700 shadow-sm dark:border-blue-800 dark:bg-zinc-950 dark:text-blue-200">
<i class="fa-solid fa-file-arrow-up text-base" aria-hidden="true"></i>
<span class="inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-xl border border-zinc-300 bg-white text-zinc-700 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-200">
<i class="fa-solid fa-file-arrow-up text-sm" aria-hidden="true"></i>
</span>
<span class="min-w-0">
<span class="block text-sm font-black text-zinc-900 dark:text-zinc-100">Attach supporting evidence</span>
<span class="block truncate text-xs font-semibold text-zinc-600 dark:text-zinc-300" data-helpdesk-supporting-file-label>
{{ count($supportingFiles) === 0 ? 'No files selected' : count($supportingFiles).' file'.(count($supportingFiles) === 1 ? '' : 's').' selected' }}
</span>
<span class="block truncate text-xs font-semibold text-zinc-500 dark:text-zinc-400" data-helpdesk-file-label>No files selected</span>
</span>
</div>
<div class="flex flex-wrap items-center justify-end gap-3">
<label data-helpdesk-file-button>
<div class="flex shrink-0 flex-wrap items-center justify-center gap-3 sm:justify-end">
<label data-helpdesk-file-button class="inline-flex cursor-pointer items-center justify-center gap-2 rounded-xl bg-blue-600 px-4 py-2.5 text-sm font-black text-white shadow-sm transition hover:bg-blue-700 focus-within:outline focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-blue-500">
<i class="fa-solid fa-paperclip" aria-hidden="true"></i>
<span>Choose files</span>
<input id="helpdesk-supporting-files" type="file" multiple class="sr-only" wire:model="supportingFiles" data-helpdesk-supporting-file-input>
<input name="attachments[]" type="file" multiple class="sr-only" data-helpdesk-file-input>
</label>
<button
type="button"
wire:click="saveSupportingFiles"
wire:loading.attr="disabled"
wire:target="supportingFiles,saveSupportingFiles"
@disabled(count($supportingFiles) === 0)
class="inline-flex min-h-12 items-center justify-center gap-2 rounded-2xl bg-emerald-600 px-5 py-3 text-sm font-black text-white shadow-sm transition hover:bg-emerald-700 disabled:cursor-not-allowed disabled:opacity-50"
>
<i class="fa-solid fa-cloud-arrow-up" aria-hidden="true"></i>
Save files
<button type="submit" class="inline-flex items-center justify-center gap-2 rounded-xl bg-emerald-600 px-4 py-2.5 text-sm font-black text-white shadow-sm transition hover:bg-emerald-700 focus:outline-none focus:ring-2 focus:ring-emerald-400/70">
<i class="fa-solid fa-upload" aria-hidden="true"></i>
Upload
</button>
</div>
</div>
<div wire:loading wire:target="supportingFiles" class="mt-4 rounded-xl border border-blue-200 bg-white px-4 py-3 text-sm font-bold text-blue-800 dark:border-blue-800 dark:bg-zinc-950 dark:text-blue-100">
Preparing selected files…
</div>
@if (count($supportingFiles) > 0)
<div class="mt-4 rounded-2xl border border-zinc-200 bg-white p-4 dark:border-zinc-800 dark:bg-zinc-950">
<p class="mb-3 text-xs font-black uppercase tracking-[0.2em] text-zinc-500 dark:text-zinc-400">Ready to attach</p>
<ul class="space-y-2">
@foreach ($supportingFiles as $file)
<li class="flex items-center justify-between gap-3 rounded-xl border border-zinc-200 bg-zinc-50 px-3 py-2 text-sm font-semibold text-zinc-800 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100">
<span class="truncate">{{ $file->getClientOriginalName() }}</span>
<span class="shrink-0 text-xs text-zinc-500">{{ Number::fileSize((int) $file->getSize()) }}</span>
</li>
@endforeach
</ul>
</div>
@endif
@error('supportingFiles') <div class="mt-2 text-sm font-bold text-red-600">{{ $message }}</div> @enderror
@error('supportingFiles.*') <div class="mt-2 text-sm font-bold text-red-600">{{ $message }}</div> @enderror
</div>
@error('attachments') <div class="mt-3 text-sm font-semibold text-red-600">{{ $message }}</div> @enderror
@error('attachments.*') <div class="mt-3 text-sm font-semibold text-red-600">{{ $message }}</div> @enderror
</form>
@endcan
@if ($ticket->attachments->isEmpty())
<div class="rounded-2xl border border-dashed border-zinc-300 bg-zinc-50 px-5 py-8 text-center text-sm font-semibold text-zinc-600 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-300">
No supporting files have been attached yet.
<div class="rounded-2xl border border-dashed border-zinc-300 bg-zinc-50 p-8 text-center dark:border-zinc-700 dark:bg-zinc-950">
<p class="text-sm font-black text-zinc-800 dark:text-zinc-100">No supporting files yet.</p>
<p class="mt-1 text-sm font-semibold text-zinc-500 dark:text-zinc-400">Use Choose files above to attach multiple files to this existing ticket.</p>
</div>
@else
<div class="grid gap-3 sm:grid-cols-2">
@foreach ($ticket->attachments as $attachment)
<a href="{{ route('helpdesk.attachments.download', $attachment->id) }}" target="_blank" class="group flex items-center justify-between gap-4 rounded-2xl border border-zinc-200 bg-white p-4 text-sm shadow-sm transition hover:-translate-y-0.5 hover:bg-zinc-50 hover:shadow-md dark:border-zinc-700 dark:bg-zinc-950 dark:hover:bg-zinc-900">
<a href="{{ route('helpdesk.attachments.download', $attachment->id) }}" target="_blank" class="group flex items-center justify-between gap-4 rounded-xl border border-zinc-200 p-4 text-sm transition hover:bg-zinc-50 dark:border-zinc-700 dark:hover:bg-zinc-800">
<span class="min-w-0">
<span class="block truncate font-black text-zinc-900 dark:text-zinc-100">{{ $attachment->original_name ?? $attachment->original_filename ?? $attachment->filename ?? $attachment->file_name ?? basename($attachment->path ?? $attachment->file_path ?? '') }}</span>
<span class="block text-xs font-semibold text-zinc-500">{{ Number::fileSize($attachment->size ?? $attachment->size_bytes ?? 0) }}</span>
<span class="block truncate font-bold text-zinc-900 dark:text-zinc-100">{{ $attachment->original_name ?? $attachment->original_filename ?? $attachment->filename ?? $attachment->file_name ?? basename($attachment->path ?? $attachment->file_path ?? '') }}</span>
<span class="block text-xs text-zinc-500">{{ Number::fileSize($attachment->size ?? $attachment->size_bytes ?? 0) }}</span>
</span>
<span class="inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl bg-blue-600 text-white shadow-sm transition group-hover:bg-blue-700">
<span class="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-zinc-100 text-zinc-700 transition group-hover:bg-blue-600 group-hover:text-white dark:bg-zinc-800 dark:text-zinc-200">
<i class="fa-solid fa-download" aria-hidden="true"></i>
</span>
</a>
@@ -414,7 +388,7 @@
</div>
@endif
</div>
</section>
</details>
<section class="space-y-4">
<details class="overflow-hidden rounded-2xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-900" data-helpdesk-conversation-section>
@@ -577,6 +551,50 @@
@endif
@endcan
<details class="mt-5 overflow-hidden rounded-[1.5rem] border border-zinc-200 bg-white/80 shadow-sm dark:border-zinc-800 dark:bg-zinc-950/50">
<summary class="group flex cursor-pointer list-none items-center justify-between gap-4 bg-gradient-to-r from-slate-950 via-blue-950 to-slate-900 px-6 py-5 text-white transition hover:from-slate-900 hover:via-blue-900 hover:to-slate-900 [&::-webkit-details-marker]:hidden">
<span class="flex min-w-0 items-center gap-4">
<span class="inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl border border-blue-400/30 bg-blue-500/15 text-blue-200 shadow-inner">
<i class="fa-solid fa-paperclip text-sm" aria-hidden="true"></i>
</span>
<span class="min-w-0">
<span class="block text-xs font-black uppercase tracking-[0.28em] text-blue-200">Attachments</span>
<span class="block text-xl font-black tracking-tight text-white">Supporting files</span>
<span class="block truncate text-sm font-semibold text-zinc-300">Pin screenshots, logs and supporting documents.</span>
</span>
</span>
<span class="flex shrink-0 items-center gap-3">
<span class="rounded-full border border-white/10 bg-white/10 px-3 py-1 text-xs font-black text-white" data-helpdesk-file-count>0 files</span>
<i class="fa-solid fa-chevron-down text-xs text-blue-200 transition group-open:rotate-180" aria-hidden="true"></i>
</span>
</summary>
<div class="border-t border-zinc-200 p-6 dark:border-zinc-800">
<div class="rounded-2xl border border-zinc-200 bg-zinc-50/80 p-5 shadow-sm dark:border-zinc-800 dark:bg-zinc-950/70">
<div class="flex min-h-[5.25rem] flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div class="flex min-w-0 items-center gap-4">
<span class="inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-xl border border-zinc-300 bg-white text-zinc-700 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-200">
<i class="fa-solid fa-file-arrow-up text-sm" aria-hidden="true"></i>
</span>
<span class="min-w-0">
<span class="block text-sm font-black text-zinc-900 dark:text-zinc-100">Attach supporting evidence</span>
<span class="block truncate text-xs font-semibold text-zinc-500 dark:text-zinc-400" data-helpdesk-file-label>No files attached</span>
</span>
</div>
<div class="flex shrink-0 items-center justify-center sm:min-w-[190px]">
<label data-helpdesk-file-button>
<i class="fa-solid fa-paperclip" aria-hidden="true"></i>
<span>Attach files</span>
<input name="attachments[]" type="file" multiple class="sr-only" data-helpdesk-file-input>
</label>
</div>
</div>
</div>
@error('attachments.*') <div class="mt-2 text-sm text-red-600">{{ $message }}</div> @enderror
</div>
</details>
<div class="mt-6 flex justify-end">
<button dusk="post-comment" type="submit" class="inline-flex items-center justify-center rounded-xl bg-emerald-600 px-5 py-3 text-sm font-bold text-white shadow-sm transition hover:bg-emerald-700 focus:outline-none focus:ring-2 focus:ring-emerald-400/70">
<i class="fa-solid fa-comments mr-2" aria-hidden="true"></i>
@@ -705,14 +723,17 @@
});
}
const fileInput = event.target.closest('[data-helpdesk-file-input], [data-helpdesk-supporting-file-input]');
const fileInput = event.target.closest('[data-helpdesk-file-input]');
if (fileInput) {
const panel = fileInput.closest('details') || fileInput.closest('div');
const label = panel?.querySelector('[data-helpdesk-file-label], [data-helpdesk-supporting-file-label]');
const panel = fileInput.closest('[data-helpdesk-supporting-files-form]') || fileInput.closest('details') || fileInput.closest('div');
const label = panel?.querySelector('[data-helpdesk-file-label]');
const badge = panel?.querySelector('[data-helpdesk-file-count]');
const count = fileInput.files?.length || 0;
const text = count === 0 ? 'No files attached' : `${count} file${count === 1 ? '' : 's'} selected`;
const files = Array.from(fileInput.files || []);
const count = files.length;
const text = count === 0
? 'No files selected'
: `${count} file${count === 1 ? '' : 's'} selected: ${files.map((file) => file.name).join(', ')}`;
if (label) {
label.textContent = text;
+2
View File
@@ -7,6 +7,7 @@ use Modules\Helpdesk\Http\Controllers\AttachmentDownloadController;
use Modules\Helpdesk\Http\Controllers\HelpdeskController;
use Modules\Helpdesk\Http\Controllers\TicketControlsController;
use Modules\Helpdesk\Http\Controllers\TicketCommentDeleteController;
use Modules\Helpdesk\Http\Controllers\TicketAttachmentStoreController;
use Modules\Helpdesk\Http\Controllers\TicketCommentStoreController;
use Modules\Helpdesk\Http\Controllers\TicketStoreController;
use Modules\Helpdesk\Http\Controllers\WorkflowOpenController;
@@ -31,6 +32,7 @@ Route::get('/tickets/{ticket}/edit', TicketEdit::class)->name('tickets.edit');
Route::post('/tickets/{ticket}/agent', [TicketControlsController::class, 'updateAgent'])->name('tickets.agent.update');
Route::post('/tickets/{ticket}/status', [TicketControlsController::class, 'updateStatus'])->name('tickets.status.update');
Route::post('/tickets/{ticket}/priority', [TicketControlsController::class, 'updatePriority'])->name('tickets.priority.update');
Route::post('/tickets/{ticket}/attachments', TicketAttachmentStoreController::class)->name('tickets.attachments.store');
Route::post('/tickets/{ticket}/comments', TicketCommentStoreController::class)->name('tickets.comments.store');
Route::delete('/tickets/{ticket}/comments/{comment}', TicketCommentDeleteController::class)->name('tickets.comments.destroy');
Route::get('/tickets/{ticket}', TicketShow::class)->name('tickets.show');
+4 -6
View File
@@ -348,10 +348,8 @@ versions:
- version: 1.9.20
date: 2026-06-17
changes:
- Moved existing-ticket attachment upload into the Supporting files card instead of the response form.
- Added Livewire-backed multi-file uploads directly against existing tickets without requiring a new response.
- Preserved normal response posting while removing the misplaced response attachment chooser from the ticket detail form.
- Improved the Supporting files attachment button styling and selected-file status display.
- Added Kanban board re-render keys and moved-ticket browser events so drag/drop status moves refresh reliably.
- Added feature coverage for existing-ticket multiple supporting file uploads and Kanban moved-ticket dispatch.
- Removed the missing Helpdesk Vite manifest call from ticket edit to stop the build/helpdesk manifest server error.
- Added direct multi-file Supporting files upload for existing tickets without requiring a reply/comment.
- Moved the ticket-level attachment button into the Supporting files card and restored a compact professional action style.
- Added Kanban drag/drop refresh dispatching, polling and stable Livewire keys so moved tickets refresh automatically.
- Kept module.json version in sync with changelog.yaml.