From bcd133e27af654956f1070fbf4b07706fa0c6f24 Mon Sep 17 00:00:00 2001 From: Mark Gregory Date: Tue, 16 Jun 2026 18:25:54 +0100 Subject: [PATCH] fixe duploads and attachments --- Modules/Helpdesk/Livewire/TicketEdit.php | 157 ++++++++++++++++-- Modules/Helpdesk/Livewire/TicketShow.php | 60 ++++++- .../views/livewire/ticket-edit.blade.php | 50 +++++- .../views/livewire/ticket-show.blade.php | 42 +++-- 4 files changed, 276 insertions(+), 33 deletions(-) diff --git a/Modules/Helpdesk/Livewire/TicketEdit.php b/Modules/Helpdesk/Livewire/TicketEdit.php index a433be58..907cf827 100644 --- a/Modules/Helpdesk/Livewire/TicketEdit.php +++ b/Modules/Helpdesk/Livewire/TicketEdit.php @@ -6,11 +6,15 @@ namespace Modules\Helpdesk\Livewire; use App\Models\User; use Illuminate\Contracts\View\View; +use Illuminate\Database\Eloquent\Collection as EloquentCollection; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; use Livewire\Attributes\Validate; use Livewire\Component; +use Livewire\Features\SupportFileUploads\TemporaryUploadedFile; +use Livewire\WithFileUploads; use Modules\Helpdesk\Models\Category; use Modules\Helpdesk\Models\KanbanBoard; use Modules\Helpdesk\Models\Priority; @@ -25,6 +29,8 @@ use Modules\Helpdesk\Services\WorkflowNotificationService; final class TicketEdit extends Component { + use WithFileUploads; + public Ticket $ticket; #[Validate('nullable|integer')] @@ -63,16 +69,22 @@ final class TicketEdit extends Component #[Validate('nullable|date')] public ?string $due_at = null; - /** @var array */ #[Validate('array')] public array $tag_ids = []; + /** @var array */ + #[Validate('array')] + public array $attachments = []; + + /** + * Load the editable ticket state, including existing attachments and assignment. + */ public function mount(Ticket $ticket): void { $this->authorize('update', $ticket); - $this->ticket = $ticket->fresh(['status', 'priority', 'category', 'project', 'team', 'agent', 'tags']) ?? $ticket; + $this->ticket = $this->freshTicket($ticket); $this->company_id = $this->ticket->company_id; $this->subject = $this->ticket->subject; $this->body = $this->ticket->body; @@ -88,12 +100,16 @@ final class TicketEdit extends Component $this->tag_ids = $this->ticket->tags->pluck('id')->map(fn ($id): int => (int) $id)->all(); } + /** + * Save ticket edits, assignment and any newly uploaded ticket-level attachments. + */ public function save(WorkflowNotificationService $workflow, SlaService $sla): mixed { $this->authorize('update', $this->ticket); - $this->validate(); + $this->validate(); $this->validate([ + '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', ]); @@ -117,19 +133,36 @@ final class TicketEdit extends Component ])->save(); $this->ticket->tags()->sync(array_values(array_unique(array_map('intval', $this->tag_ids)))); + $this->storeTicketAttachments(); if ($status->is_closed) { $sla->recordClosure($this->ticket); } - $fresh = $this->ticket->fresh(['owner', 'agent', 'status', 'priority', 'category', 'project', 'team', 'product', 'tags']) ?? $this->ticket; + $fresh = $this->freshTicket($this->ticket); $workflow->dispatch($fresh, $status->is_closed ? 'ticket.closed' : 'ticket.updated', Auth::user()); - session()->flash('success', 'Ticket ' . $fresh->reference . ' has been updated.'); + session()->flash('success', 'Ticket '.$fresh->reference.' has been updated.'); return redirect()->route('helpdesk.tickets.show', $fresh); } + /** + * Remove an existing ticket-level attachment from storage and the database. + */ + public function removeAttachment(int $attachmentId): void + { + $this->authorize('update', $this->ticket); + + $attachment = $this->ticket->attachments()->whereKey($attachmentId)->firstOrFail(); + + if (method_exists($attachment, 'deleteFile')) { + $attachment->deleteFile(); + } + + $attachment->delete(); + $this->ticket = $this->freshTicket($this->ticket); + } /** * Clear the edit form, including Select2 and Flatpickr browser widgets. @@ -153,6 +186,7 @@ final class TicketEdit extends Component $this->agent_id = null; $this->due_at = null; $this->tag_ids = []; + $this->attachments = []; $this->dispatch('helpdesk-form-cleared'); $this->dispatch('helpdesk-editor-reset', model: 'body'); @@ -170,12 +204,113 @@ final class TicketEdit extends Component 'projects' => Project::query()->whereNull('archived_at')->orderBy('title')->get(), 'products' => Product::query()->where('is_active', true)->orderBy('name')->get(), 'tags' => Tag::query()->orderBy('name')->get(), - 'agents' => User::query() - ->where(static function ($query): void { - $query->where('helpdesk_agent', true)->orWhere('helpdesk_admin', true); - }) - ->orderBy('name') - ->get(['id', 'name', 'email']), + 'agents' => $this->agentOptions(), ])->layout('layouts.app'); } + + /** + * Return assignable users. Falls back to all tenant users when helpdesk flags/roles are absent. + * + * @return EloquentCollection + */ + private function agentOptions(): EloquentCollection + { + return User::query() + ->select(['id', 'name', 'email', 'company_id']) + ->when($this->company_id !== null && Schema::hasColumn('users', 'company_id'), function ($query): void { + $query->where(function ($query): void { + $query->where('company_id', $this->company_id) + ->orWhereNull('company_id'); + }); + }) + ->when(Schema::hasColumn('users', 'helpdesk_agent') || Schema::hasColumn('users', 'helpdesk_admin'), function ($query): void { + $query->where(function ($query): void { + if (Schema::hasColumn('users', 'helpdesk_agent')) { + $query->orWhere('helpdesk_agent', true); + } + + if (Schema::hasColumn('users', 'helpdesk_admin')) { + $query->orWhere('helpdesk_admin', true); + } + + if ($this->agent_id !== null) { + $query->orWhereKey($this->agent_id); + } + }); + }) + ->orderBy('name') + ->get(); + } + + private function freshTicket(Ticket $ticket): Ticket + { + return $ticket->fresh([ + 'owner', + 'agent', + 'status', + 'priority', + 'category', + 'project', + 'team', + 'product', + 'tags', + 'attachments.user', + ]) ?? $ticket; + } + + /** + * Store ticket-level attachments in a schema-tolerant way. + */ + private function storeTicketAttachments(): void + { + if ($this->attachments === []) { + return; + } + + foreach ($this->attachments as $upload) { + if (! $upload instanceof TemporaryUploadedFile) { + continue; + } + + $path = $upload->store('helpdesk/tickets/'.$this->ticket->id, 'public'); + $this->ticket->attachments()->create($this->attachmentPayload($upload, $path)); + } + + $this->attachments = []; + $this->ticket = $this->freshTicket($this->ticket); + } + + /** + * Build attachment attributes using the columns available in the project migration. + * + * @return array + */ + private function attachmentPayload(TemporaryUploadedFile $upload, string $path): array + { + $table = 'helpdesk_attachments'; + $payload = []; + $columns = [ + 'user_id' => Auth::id(), + 'uploaded_by_id' => Auth::id(), + 'disk' => 'public', + 'path' => $path, + 'file_path' => $path, + 'filename' => basename($path), + 'file_name' => basename($path), + 'original_name' => $upload->getClientOriginalName(), + 'original_filename' => $upload->getClientOriginalName(), + 'mime_type' => $upload->getMimeType(), + 'mime' => $upload->getMimeType(), + 'size' => $upload->getSize(), + 'size_bytes' => $upload->getSize(), + ]; + + foreach ($columns as $column => $value) { + if (Schema::hasColumn($table, $column)) { + $payload[$column] = $value; + } + } + + return $payload; + } } diff --git a/Modules/Helpdesk/Livewire/TicketShow.php b/Modules/Helpdesk/Livewire/TicketShow.php index ea84c274..2a8c351c 100644 --- a/Modules/Helpdesk/Livewire/TicketShow.php +++ b/Modules/Helpdesk/Livewire/TicketShow.php @@ -6,8 +6,10 @@ namespace Modules\Helpdesk\Livewire; use App\Models\User; use Illuminate\Contracts\View\View; +use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Schema; use Livewire\Attributes\Validate; use Livewire\Component; use Livewire\Features\SupportFileUploads\TemporaryUploadedFile; @@ -41,6 +43,8 @@ final class TicketShow extends Component public ?int $priority_id = null; + public ?int $agent_id = null; + public ?int $reply_parent_id = null; public string $replyBody = ''; @@ -53,6 +57,7 @@ final class TicketShow extends Component $this->ticket = $this->freshTicket($ticket); $this->status_id = $this->ticket->status_id; $this->priority_id = $this->ticket->priority_id; + $this->agent_id = $this->ticket->agent_id; } public function startReply(int $commentId): void @@ -73,7 +78,7 @@ final class TicketShow extends Component $this->validate([ 'commentBody' => ['required', 'string', 'min:2'], - 'attachments.*' => 'file|max:'.config('helpdesk.max_upload_kb').'|mimes:'.implode(',', config('helpdesk.allowed_mimes')), + 'attachments.*' => 'file|max:'.config('helpdesk.max_upload_kb', 10240).'|mimes:'.implode(',', config('helpdesk.allowed_mimes', ['jpg', 'jpeg', 'png', 'pdf', 'txt', 'log', 'zip'])), ]); $tickets->comment($this->ticket, Auth::user(), $this->commentBody, $this->internal, $this->attachments); @@ -101,7 +106,6 @@ final class TicketShow extends Component $this->dispatch('helpdesk-reply-added'); } - public function applyCannedResponse(CannedResponseRenderer $renderer): void { $this->authorize('comment', $this->ticket); @@ -167,11 +171,27 @@ final class TicketShow extends Component $this->dispatch('helpdesk-priority-updated'); } + public function updateAgent(WorkflowNotificationService $workflow): void + { + $this->authorize('update', $this->ticket); + $this->validate(['agent_id' => ['nullable', 'integer', 'exists:users,id']]); + + $this->ticket->forceFill([ + 'agent_id' => $this->agent_id, + 'last_activity_at' => now(), + ])->save(); + + $this->ticket = $this->freshTicket($this->ticket); + $workflow->dispatch($this->ticket, 'ticket.updated', Auth::user()); + $this->dispatch('helpdesk-agent-updated'); + } + public function render(): View { return view('helpdesk::livewire.ticket-show', [ 'statuses' => Status::query()->orderBy('sort_order')->get(), 'priorities' => Priority::query()->orderBy('sort_order')->get(), + 'agents' => $this->agentOptions(), 'cannedResponses' => CannedResponse::query()->where('is_active', true)->orderBy('sort_order')->orderBy('name')->get(), ])->layout('layouts.app'); } @@ -207,10 +227,44 @@ final class TicketShow extends Component return $ticket; } + /** + * Return assignable users. Falls back to all tenant users when helpdesk flags/roles are absent. + * + * @return EloquentCollection + */ + private function agentOptions(): EloquentCollection + { + return User::query() + ->select(['id', 'name', 'email', 'company_id']) + ->when($this->ticket->company_id !== null && Schema::hasColumn('users', 'company_id'), function ($query): void { + $query->where(function ($query): void { + $query->where('company_id', $this->ticket->company_id) + ->orWhereNull('company_id'); + }); + }) + ->when(Schema::hasColumn('users', 'helpdesk_agent') || Schema::hasColumn('users', 'helpdesk_admin'), function ($query): void { + $query->where(function ($query): void { + if (Schema::hasColumn('users', 'helpdesk_agent')) { + $query->orWhere('helpdesk_agent', true); + } + + if (Schema::hasColumn('users', 'helpdesk_admin')) { + $query->orWhere('helpdesk_admin', true); + } + + if ($this->agent_id !== null) { + $query->orWhereKey($this->agent_id); + } + }); + }) + ->orderBy('name') + ->get(); + } + /** * Build an unlimited nested comment tree from a flat eager-loaded collection. * - * @param Collection> $grouped + * @param Collection> $grouped * @return Collection */ private function buildCommentTree(Collection $grouped, ?int $parentId = null): Collection diff --git a/Modules/Helpdesk/Resources/views/livewire/ticket-edit.blade.php b/Modules/Helpdesk/Resources/views/livewire/ticket-edit.blade.php index c555af85..314fab42 100644 --- a/Modules/Helpdesk/Resources/views/livewire/ticket-edit.blade.php +++ b/Modules/Helpdesk/Resources/views/livewire/ticket-edit.blade.php @@ -170,19 +170,19 @@
-
+
@@ -374,6 +374,42 @@ Uploading attachments...
+
+

Existing attachments

+ + @forelse ($ticket->attachments as $attachment) + + @empty +
+ No ticket-level attachments are currently saved. +
+ @endforelse +
+ + @if ($attachments !== []) +
+ {{ count($attachments) }} new attachment(s) selected. Save the ticket to attach them. +
+ @endif + @error('attachments')

{{ $message }}

@enderror diff --git a/Modules/Helpdesk/Resources/views/livewire/ticket-show.blade.php b/Modules/Helpdesk/Resources/views/livewire/ticket-show.blade.php index 37a4322b..bb7b2b06 100644 --- a/Modules/Helpdesk/Resources/views/livewire/ticket-show.blade.php +++ b/Modules/Helpdesk/Resources/views/livewire/ticket-show.blade.php @@ -37,19 +37,21 @@
- @if ($ticket->attachments->isNotEmpty()) -
-

Ticket attachments

-
- @foreach ($ticket->attachments as $attachment) - -
{{ $attachment->original_name }}
-
{{ Number::fileSize($attachment->size) }}
-
- @endforeach -
+

Conversation

@@ -184,6 +186,22 @@ @can('update', $ticket)

Agent actions

+ + + Update assigned agent + @foreach ($statuses as $status)