From de458cc010cd19c1f413c502fd79bb349bd0d25e Mon Sep 17 00:00:00 2001 From: Mark Gregory Date: Sun, 14 Jun 2026 14:11:31 +0100 Subject: [PATCH] Updated the helpdesk module --- Modules/Helpdesk/Livewire/SettingsManager.php | 130 +++++----- Modules/Helpdesk/Policies/TicketPolicy.php | 25 +- .../views/livewire/kanban-board.blade.php | 235 ++++++++---------- .../views/livewire/settings-manager.blade.php | 34 +-- .../views/livewire/ticket-create.blade.php | 75 +++--- .../views/livewire/ticket-edit.blade.php | 111 +++++---- .../views/partials/select2-loader.blade.php | 129 ++++++++-- Modules/Helpdesk/changelog.yaml | 15 +- Modules/Helpdesk/module.json | 2 +- 9 files changed, 423 insertions(+), 333 deletions(-) mode change 100755 => 100644 Modules/Helpdesk/changelog.yaml diff --git a/Modules/Helpdesk/Livewire/SettingsManager.php b/Modules/Helpdesk/Livewire/SettingsManager.php index ce97b14c..09d73f6b 100644 --- a/Modules/Helpdesk/Livewire/SettingsManager.php +++ b/Modules/Helpdesk/Livewire/SettingsManager.php @@ -37,15 +37,15 @@ final class SettingsManager extends Component /** @var array */ public array $projectForm = ['id' => null, 'company_id' => null, 'team_id' => null, 'title' => '', 'slug' => '', 'color' => 'blue', 'description' => '', 'archived_at' => null]; + /** @var array */ + public array $boardForm = ['id' => null, 'name' => '', 'slug' => '', 'description' => '', 'is_default' => false, 'sort_order' => 0, 'status_ids' => []]; + /** @var array */ public array $repositoryForm = ['id' => null, 'company_id' => null, 'project_id' => null, 'name' => '', 'provider' => 'git', 'remote_url' => '', 'default_branch' => 'main', 'webhook_secret' => '']; /** @var array */ public array $workflowForm = ['id' => null, 'name' => '', 'event' => 'ticket.created', 'subject' => '', 'html_body' => '', 'text_body' => '', 'to_role' => 'requester', 'cc' => '', 'bcc' => '', 'is_active' => true]; - /** @var array */ - public array $boardForm = ['id' => null, 'name' => '', 'slug' => '', 'description' => '', 'is_default' => false, 'sort_order' => 0, 'status_ids' => []]; - public function saveStatus(): void { $data = $this->validate([ @@ -211,6 +211,69 @@ final class SettingsManager extends Component $this->projectForm = ['id' => null, 'company_id' => null, 'team_id' => null, 'title' => '', 'slug' => '', 'color' => 'blue', 'description' => '', 'archived_at' => null]; } + + public function saveBoard(): void + { + $data = $this->validate([ + 'boardForm.name' => ['required', 'string', 'max:160'], + 'boardForm.slug' => ['nullable', 'string', 'max:180'], + 'boardForm.description' => ['nullable', 'string'], + 'boardForm.is_default' => ['boolean'], + 'boardForm.sort_order' => ['required', 'integer', 'min:0'], + 'boardForm.status_ids' => ['array'], + 'boardForm.status_ids.*' => ['integer', 'exists:helpdesk_statuses,id'], + ])['boardForm']; + + $statusIds = collect($data['status_ids'] ?? [])->map(static fn ($id): int => (int) $id)->filter()->values(); + unset($data['status_ids']); + + $data['slug'] = $this->slug($data['slug'] ?: $data['name']); + + if ((bool) ($data['is_default'] ?? false)) { + KanbanBoard::query()->whereKeyNot($this->boardForm['id'])->update(['is_default' => false]); + } + + $board = KanbanBoard::query()->updateOrCreate(['id' => $this->boardForm['id']], $data); + + $sync = []; + foreach ($statusIds as $index => $statusId) { + $sync[$statusId] = ['sort_order' => ($index + 1) * 10]; + } + + $board->statuses()->sync($sync); + $this->resetBoardForm(); + $this->section = 'boards'; + session()->flash('success', 'Kanban board saved.'); + } + + public function editBoard(int $id): void + { + $board = KanbanBoard::query()->with('statuses')->findOrFail($id); + + $this->boardForm = [ + 'id' => $board->id, + 'name' => $board->name, + 'slug' => $board->slug, + 'description' => $board->description, + 'is_default' => $board->is_default, + 'sort_order' => $board->sort_order, + 'status_ids' => $board->statuses->pluck('id')->map(static fn ($id): int => (int) $id)->all(), + ]; + + $this->section = 'boards'; + } + + public function deleteBoard(int $id): void + { + KanbanBoard::query()->findOrFail($id)->delete(); + session()->flash('success', 'Kanban board deleted.'); + } + + public function resetBoardForm(): void + { + $this->boardForm = ['id' => null, 'name' => '', 'slug' => '', 'description' => '', 'is_default' => false, 'sort_order' => 0, 'status_ids' => []]; + } + public function saveRepository(): void { $data = $this->validate([ @@ -245,65 +308,6 @@ final class SettingsManager extends Component $this->repositoryForm = ['id' => null, 'company_id' => null, 'project_id' => null, 'name' => '', 'provider' => 'git', 'remote_url' => '', 'default_branch' => 'main', 'webhook_secret' => '']; } - - public function saveBoard(): void - { - $data = $this->validate([ - 'boardForm.name' => ['required', 'string', 'max:160'], - 'boardForm.slug' => ['nullable', 'string', 'max:180'], - 'boardForm.description' => ['nullable', 'string'], - 'boardForm.is_default' => ['boolean'], - 'boardForm.sort_order' => ['required', 'integer', 'min:0'], - 'boardForm.status_ids' => ['required', 'array', 'min:1'], - 'boardForm.status_ids.*' => ['integer', 'exists:helpdesk_statuses,id'], - ])['boardForm']; - - $statusIds = collect($data['status_ids']) - ->map(static fn (mixed $id): int => (int) $id) - ->filter() - ->unique() - ->values(); - - unset($data['status_ids']); - $data['slug'] = $this->slug($data['slug'] ?: $data['name']); - - if ((bool) $data['is_default']) { - KanbanBoard::query()->where('is_default', true)->update(['is_default' => false]); - } - - $board = KanbanBoard::query()->updateOrCreate(['id' => $this->boardForm['id']], $data); - - $sync = []; - foreach ($statusIds as $index => $statusId) { - $sync[$statusId] = ['sort_order' => ($index + 1) * 10]; - } - - $board->statuses()->sync($sync); - $this->resetBoardForm(); - $this->section = 'boards'; - session()->flash('success', 'Kanban board saved.'); - } - - public function editBoard(int $id): void - { - $board = KanbanBoard::query()->with('statuses')->findOrFail($id); - - $this->boardForm = $board->only(['id', 'name', 'slug', 'description', 'is_default', 'sort_order']); - $this->boardForm['status_ids'] = $board->statuses->pluck('id')->map(static fn (mixed $id): int => (int) $id)->all(); - $this->section = 'boards'; - } - - public function deleteBoard(int $id): void - { - KanbanBoard::query()->findOrFail($id)->delete(); - session()->flash('success', 'Kanban board deleted.'); - } - - public function resetBoardForm(): void - { - $this->boardForm = ['id' => null, 'name' => '', 'slug' => '', 'description' => '', 'is_default' => false, 'sort_order' => 0, 'status_ids' => []]; - } - public function saveWorkflowTemplate(): void { $data = $this->validate([ @@ -349,8 +353,8 @@ final class SettingsManager extends Component 'categories' => Category::query()->withCount('tickets')->orderBy('name')->get(), 'teams' => Team::query()->withCount(['projects', 'tickets'])->orderBy('name')->get(), 'projects' => Project::query()->with(['team'])->withCount('tickets')->orderBy('title')->get(), + 'boards' => KanbanBoard::query()->with('statuses')->orderBy('sort_order')->orderBy('name')->get(), 'repositories' => GitRepository::query()->with(['project'])->withCount('commits')->orderBy('name')->get(), - 'boards' => KanbanBoard::query()->with('statuses')->withCount('statuses')->orderBy('sort_order')->orderBy('name')->get(), 'workflowTemplates' => WorkflowTemplate::query()->orderBy('event')->orderBy('name')->get(), ])->layout('layouts.app'); } diff --git a/Modules/Helpdesk/Policies/TicketPolicy.php b/Modules/Helpdesk/Policies/TicketPolicy.php index 7314c065..ad140d06 100644 --- a/Modules/Helpdesk/Policies/TicketPolicy.php +++ b/Modules/Helpdesk/Policies/TicketPolicy.php @@ -16,8 +16,8 @@ final class TicketPolicy public function view(User $user, Ticket $ticket): bool { - return (bool) ($user->helpdesk_admin ?? false) - || (bool) ($user->helpdesk_agent ?? false) && ((int) $ticket->agent_id === (int) $user->id || (int) $ticket->user_id === (int) $user->id) + return $this->isPrivileged($user) + || ((bool) ($user->helpdesk_agent ?? false) && ((int) $ticket->agent_id === (int) $user->id || (int) $ticket->user_id === (int) $user->id)) || (int) $ticket->user_id === (int) $user->id; } @@ -28,9 +28,9 @@ final class TicketPolicy public function update(User $user, Ticket $ticket): bool { - return (bool) ($user->helpdesk_admin ?? false) - || (bool) ($user->helpdesk_agent ?? false) - || (int) $ticket->agent_id === (int) $user->id; + return $this->isPrivileged($user) + || (int) $ticket->agent_id === (int) $user->id + || (int) $ticket->user_id === (int) $user->id; } public function comment(User $user, Ticket $ticket): bool @@ -40,6 +40,19 @@ final class TicketPolicy public function delete(User $user, Ticket $ticket): bool { - return (bool) ($user->helpdesk_admin ?? false); + return $this->isPrivileged($user); + } + + private function isPrivileged(User $user): bool + { + if ((bool) ($user->helpdesk_admin ?? false)) { + return true; + } + + if (method_exists($user, 'isMpbaPlatformAdmin') && $user->isMpbaPlatformAdmin()) { + return true; + } + + return false; } } diff --git a/Modules/Helpdesk/Resources/views/livewire/kanban-board.blade.php b/Modules/Helpdesk/Resources/views/livewire/kanban-board.blade.php index 6d44aaa9..f2a50bc1 100644 --- a/Modules/Helpdesk/Resources/views/livewire/kanban-board.blade.php +++ b/Modules/Helpdesk/Resources/views/livewire/kanban-board.blade.php @@ -1,11 +1,11 @@ -
+

Helpdesk workflow

Kanban board

-

Drag tickets between configured board columns. Boards and columns are managed in Helpdesk Settings.

+

Drag tickets between board columns. Boards and columns are configured in Helpdesk Settings.

@@ -23,168 +23,151 @@
@endif -
-
-

Active board

-

{{ $board?->description ?: 'Use settings to configure which statuses appear on each board.' }}

+ @if ($boards->isEmpty()) +
+ No Kanban boards exist yet. Open Helpdesk Settings and create a board with at least one status column. +
+ @else +
+
+

Active board

+

{{ $board?->description ?: 'Use settings to configure which statuses appear as draggable columns.' }}

+
+ +
- -
+
+ @foreach ($statuses as $status) + @php($tickets = $ticketsByStatus[$status->id] ?? collect()) -
- @foreach ($statuses as $status) - @php($tickets = $ticketsByStatus[$status->id] ?? collect()) - -
-
-
-
-

{{ $status->name }}

-

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

+
+
+
+
+

{{ $status->name }}

+

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

+
+
- -
-
+ -
- @forelse ($tickets as $ticket) -
-
-
-
{{ $ticket->reference }}
-

{{ $ticket->subject }}

+
+ @forelse ($tickets as $ticket) +
+
+
+
{{ $ticket->reference }}
+

{{ $ticket->subject }}

+
+ + @if ($ticket->priority) + + {{ $ticket->priority->name }} + + @endif
- @if ($ticket->priority) - - {{ $ticket->priority->name }} - - @endif -
+
+
{{ $ticket->owner?->name ?? 'Unknown' }}
+
{{ $ticket->agent?->name ?? 'Unassigned' }}
+ @if ($ticket->project) +
{{ $ticket->project->title }}
+ @endif + @if ($ticket->team) +
{{ $ticket->team->name }}
+ @endif +
-
-
{{ $ticket->owner?->name ?? 'Unknown' }}
-
{{ $ticket->agent?->name ?? 'Unassigned' }}
- @if ($ticket->project) -
{{ $ticket->project->title }}
- @endif - @if ($ticket->team) -
{{ $ticket->team->name }}
- @endif +
+ Open + @can('update', $ticket) + Edit + @endcan +
+
+ @empty +
+ Drop tickets here for {{ $status->name }}.
- -
- Open - @can('update', $ticket) - Edit - @endcan -
- - @empty -
- Drop tickets here for {{ $status->name }}. -
- @endforelse -
-
- @endforeach -
+ @endforelse +
+ + @endforeach +
+ @endif @script @endscript
diff --git a/Modules/Helpdesk/Resources/views/livewire/settings-manager.blade.php b/Modules/Helpdesk/Resources/views/livewire/settings-manager.blade.php index dad093e3..d329a19f 100644 --- a/Modules/Helpdesk/Resources/views/livewire/settings-manager.blade.php +++ b/Modules/Helpdesk/Resources/views/livewire/settings-manager.blade.php @@ -20,7 +20,7 @@ {{ session('success') }} @endif -
+
@foreach ([ 'statuses' => ['label' => 'Statuses', 'count' => $statuses->count()], 'priorities' => ['label' => 'Priorities', 'count' => $priorities->count()], @@ -31,7 +31,7 @@ 'git' => ['label' => 'Git', 'count' => $repositories->count()], 'workflow' => ['label' => 'Workflow', 'count' => $workflowTemplates->count()], ] as $key => $item) - @@ -129,21 +129,20 @@ -
- +
+
- -
+ +
@foreach ($statuses as $status) -
- @error('boardForm.status_ids')

{{ $message }}

@enderror
Save board @@ -157,7 +156,6 @@ Board - Slug Columns Default Actions @@ -166,23 +164,25 @@ @forelse ($boards as $boardItem) - {{ $boardItem->name }} - {{ $boardItem->slug }} - + +
{{ $boardItem->name }}
+
{{ $boardItem->slug }}
+ +
@foreach ($boardItem->statuses as $status) - {{ $status->name }} + {{ $status->name }} @endforeach
{{ $boardItem->is_default ? 'Default' : 'No' }} Edit - Delete + Delete @empty - No Kanban boards yet. + No Kanban boards yet. @endforelse @@ -279,7 +279,7 @@ {{ $template->to_role }} {{ $template->is_active ? 'Active' : 'Inactive' }} - Edit + Edit Delete diff --git a/Modules/Helpdesk/Resources/views/livewire/ticket-create.blade.php b/Modules/Helpdesk/Resources/views/livewire/ticket-create.blade.php index 0dd94471..4f53a553 100644 --- a/Modules/Helpdesk/Resources/views/livewire/ticket-create.blade.php +++ b/Modules/Helpdesk/Resources/views/livewire/ticket-create.blade.php @@ -6,98 +6,101 @@ ])
-
-
-

Ticket details

-

Mandatory fields are marked with *. Rich details and attachments are saved by Livewire.

+
+
+

Ticket details

+

Mandatory fields are marked with *. Use the searchable lists to keep the ticket correctly routed.

-
-
- - - @error('subject')

{{ $message }}

@enderror +
+
+ + + @error('subject')

{{ $message }}

@enderror
@if($companies->isNotEmpty())
- - @foreach ($companies as $company) @endforeach - @error('company_id')

{{ $message }}

@enderror + @error('company_id')

{{ $message }}

@enderror
@endif
- - @foreach ($priorities as $priority) @endforeach - @error('priority_id')

{{ $message }}

@enderror + @error('priority_id')

{{ $message }}

@enderror
- - @foreach ($categories as $category) @endforeach - @error('category_id')

{{ $message }}

@enderror + @error('category_id')

{{ $message }}

@enderror
- - @foreach ($projects as $project) @endforeach - @error('project_id')

{{ $message }}

@enderror + @error('project_id')

{{ $message }}

@enderror
- - @foreach ($teams as $team) @endforeach - @error('team_id')

{{ $message }}

@enderror + @error('team_id')

{{ $message }}

@enderror
- - - @error('due_at')

{{ $message }}

@enderror + + + @error('due_at')

{{ $message }}

@enderror
-
+
- @error('body')

{{ $message }}

@enderror + @error('body')

{{ $message }}

@enderror
-
- - - @error('attachments')

{{ $message }}

@enderror - @error('attachments.*')

{{ $message }}

@enderror +
+ + + @error('attachments')

{{ $message }}

@enderror + @error('attachments.*')

{{ $message }}

@enderror
-
+
Cancel - +
diff --git a/Modules/Helpdesk/Resources/views/livewire/ticket-edit.blade.php b/Modules/Helpdesk/Resources/views/livewire/ticket-edit.blade.php index 0b6ce785..30924488 100644 --- a/Modules/Helpdesk/Resources/views/livewire/ticket-edit.blade.php +++ b/Modules/Helpdesk/Resources/views/livewire/ticket-edit.blade.php @@ -1,126 +1,127 @@ -
+
@include('helpdesk::partials.page-header', [ 'eyebrow' => 'Helpdesk', 'title' => 'Edit Ticket', - 'subtitle' => 'Update assignment, status, priority, category, team, project, due date and ticket details.', + 'subtitle' => 'Update ownership, assignment, workflow status, project, team, category, priority and ticket details.', ])
-
-
+
+
-

{{ $ticket->reference }}

-

Fields marked * are mandatory.

-
-
- - Back - +

{{ $ticket->reference }} · Ticket controls

+

Mandatory fields are marked with *. Assignment and status updates trigger configured workflow messages.

+ Back to ticket
-
-
- - - @error('subject')

{{ $message }}

@enderror +
+
+ + + @error('subject')

{{ $message }}

@enderror
@if($companies->isNotEmpty())
- - + @foreach ($companies as $company) @endforeach - @error('company_id')

{{ $message }}

@enderror + @error('company_id')

{{ $message }}

@enderror
@endif
- - - @error('agent_id')

{{ $message }}

@enderror -
- -
- - @foreach ($statuses as $status) @endforeach - @error('status_id')

{{ $message }}

@enderror + @error('status_id')

{{ $message }}

@enderror
- - @foreach ($priorities as $priority) @endforeach - @error('priority_id')

{{ $message }}

@enderror + @error('priority_id')

{{ $message }}

@enderror
- - + + @foreach ($agents as $agent) + + @endforeach + + @error('agent_id')

{{ $message }}

@enderror +
+ +
+ + - @error('category_id')

{{ $message }}

@enderror + @error('category_id')

{{ $message }}

@enderror
- - @foreach ($projects as $project) - + @endforeach - @error('project_id')

{{ $message }}

@enderror + @error('project_id')

{{ $message }}

@enderror
- - @foreach ($teams as $team) @endforeach - @error('team_id')

{{ $message }}

@enderror + @error('team_id')

{{ $message }}

@enderror
- - - @error('due_at')

{{ $message }}

@enderror + + + @error('due_at')

{{ $message }}

@enderror
-
- +
+ + @error('body')

{{ $message }}

@enderror
-
- Cancel - +
+ Cancel +
+ @include('helpdesk::partials.select2-loader')
diff --git a/Modules/Helpdesk/Resources/views/partials/select2-loader.blade.php b/Modules/Helpdesk/Resources/views/partials/select2-loader.blade.php index fb828c1f..f3ac0950 100644 --- a/Modules/Helpdesk/Resources/views/partials/select2-loader.blade.php +++ b/Modules/Helpdesk/Resources/views/partials/select2-loader.blade.php @@ -1,30 +1,117 @@ -@script - -@endscript + loadScript('https://code.jquery.com/jquery-3.7.1.min.js', bootSelect2); + } + + document.addEventListener('DOMContentLoaded', bootHelpdeskSelects); + document.addEventListener('livewire:navigated', bootHelpdeskSelects); + document.addEventListener('livewire:updated', bootHelpdeskSelects); + bootHelpdeskSelects(); + })(); + diff --git a/Modules/Helpdesk/changelog.yaml b/Modules/Helpdesk/changelog.yaml old mode 100755 new mode 100644 index 066ab89b..f3e2e0c9 --- a/Modules/Helpdesk/changelog.yaml +++ b/Modules/Helpdesk/changelog.yaml @@ -54,13 +54,12 @@ versions: - Marked mandatory ticket fields and enhanced create/edit ticket forms with Select2-capable dropdowns. - Kept module.json version in sync with changelog.yaml. - "1.1.2": + "1.1.3": changed: - - Restored ticket editing with a full Livewire edit view for assignment, status, priority, category, project, team, due date and details. - - Replaced the Kanban board drag behaviour with dependency-free native HTML5 drag and drop so cards move without external SortableJS. - - Added Kanban board CRUD to Helpdesk settings with status-column selection and default board support. - - Made Helpdesk settings edit actions blue and improved the statuses selector card styling. - - Added reusable Select2 progressive enhancement for Helpdesk dropdowns while preserving native Livewire behaviour when Select2 is unavailable. - - Made ticket reply buttons green and reset reply editors after posting. - - Rebuilt ticket comment loading as an unlimited nested tree with eager-loaded users and attachments. + - Fixed ticket update permissions so platform admins, helpdesk admins, assigned agents and ticket owners can edit tickets where appropriate. + - Added the missing ticket edit Blade view with assignment, status, priority, category, project, team, due date and details controls. + - Replaced the Kanban drag dependency path with native browser drag-and-drop to prevent blocked scripts from breaking ticket movement. + - Added Kanban Boards CRUD to Helpdesk settings with configurable status columns. + - Improved ticket create and edit form layout, mandatory markers, dark footer styling and searchable Select2-capable dropdown loading. - Kept module.json version in sync with changelog.yaml. + diff --git a/Modules/Helpdesk/module.json b/Modules/Helpdesk/module.json index 0bbf6138..f988ba09 100644 --- a/Modules/Helpdesk/module.json +++ b/Modules/Helpdesk/module.json @@ -13,5 +13,5 @@ "Modules\\Helpdesk\\Providers\\HelpdeskServiceProvider" ], "files": [], - "version": "1.1.2" + "version": "1.1.3" }