From 8912d28b7b27cdd2c749f4477606ce65b4e96610 Mon Sep 17 00:00:00 2001 From: Mark Gregory Date: Thu, 18 Jun 2026 15:08:02 +0100 Subject: [PATCH] intial ticket fix partial delete fix --- Modules/Helpdesk/Livewire/SettingsManager.php | 86 +++++++++++++------ .../views/livewire/settings-manager.blade.php | 38 ++++---- .../Tests/Feature/SettingsCrudTest.php | 27 ++++++ Modules/Helpdesk/changelog.yaml | 8 ++ Modules/Helpdesk/module.json | 2 +- 5 files changed, 113 insertions(+), 48 deletions(-) diff --git a/Modules/Helpdesk/Livewire/SettingsManager.php b/Modules/Helpdesk/Livewire/SettingsManager.php index a2ab264f..1661dcb6 100644 --- a/Modules/Helpdesk/Livewire/SettingsManager.php +++ b/Modules/Helpdesk/Livewire/SettingsManager.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Modules\Helpdesk\Livewire; use App\Models\User; +use Illuminate\Database\Eloquent\Model; use Illuminate\Contracts\View\View; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; @@ -24,6 +25,7 @@ use Modules\Helpdesk\Models\Status; use Modules\Helpdesk\Models\Tag; use Modules\Helpdesk\Models\Team; use Modules\Helpdesk\Models\WorkflowTemplate; +use Throwable; final class SettingsManager extends Component { @@ -119,6 +121,16 @@ final class SettingsManager extends Component return in_array($section, $allowed, true) ? $section : 'statuses'; } + + /** + * Switch settings panels without relying on a full browser navigation. + */ + public function showSection(string $section): void + { + $this->section = $this->validSection($section); + $this->resetValidation(); + } + public function saveStatus(): void { $data = $this->validate([ @@ -139,6 +151,7 @@ final class SettingsManager extends Component Status::query()->updateOrCreate(['id' => $this->statusForm['id']], $data); $this->resetStatusForm(); + $this->section = 'statuses'; session()->flash('success', 'Status saved.'); } @@ -150,8 +163,7 @@ final class SettingsManager extends Component public function deleteStatus(int $id): void { - Status::query()->findOrFail($id)->delete(); - session()->flash('success', 'Status deleted.'); + $this->deleteRecord(Status::class, $id, 'Status', 'statuses'); } public function resetStatusForm(): void @@ -177,6 +189,7 @@ final class SettingsManager extends Component } Priority::query()->updateOrCreate(['id' => $this->priorityForm['id']], $data); $this->resetPriorityForm(); + $this->section = 'priorities'; session()->flash('success', 'Priority saved.'); } @@ -188,8 +201,7 @@ final class SettingsManager extends Component public function deletePriority(int $id): void { - Priority::query()->findOrFail($id)->delete(); - session()->flash('success', 'Priority deleted.'); + $this->deleteRecord(Priority::class, $id, 'Priority', 'priorities'); } public function resetPriorityForm(): void @@ -221,6 +233,7 @@ final class SettingsManager extends Component } Category::query()->updateOrCreate(['id' => $this->categoryForm['id']], $data); $this->resetCategoryForm(); + $this->section = 'categories'; session()->flash('success', 'Category saved.'); } @@ -232,8 +245,7 @@ final class SettingsManager extends Component public function deleteCategory(int $id): void { - Category::query()->findOrFail($id)->delete(); - session()->flash('success', 'Category deleted.'); + $this->deleteRecord(Category::class, $id, 'Category', 'categories'); } public function resetCategoryForm(): void @@ -265,6 +277,7 @@ final class SettingsManager extends Component } Team::query()->updateOrCreate(['id' => $this->teamForm['id']], $data); $this->resetTeamForm(); + $this->section = 'teams'; session()->flash('success', 'Team saved.'); } @@ -276,8 +289,7 @@ final class SettingsManager extends Component public function deleteTeam(int $id): void { - Team::query()->findOrFail($id)->delete(); - session()->flash('success', 'Team deleted.'); + $this->deleteRecord(Team::class, $id, 'Team', 'teams'); } public function resetTeamForm(): void @@ -310,6 +322,7 @@ final class SettingsManager extends Component } Project::query()->updateOrCreate(['id' => $this->projectForm['id']], $data); $this->resetProjectForm(); + $this->section = 'projects'; session()->flash('success', 'Project saved.'); } @@ -321,8 +334,7 @@ final class SettingsManager extends Component public function deleteProject(int $id): void { - Project::query()->findOrFail($id)->delete(); - session()->flash('success', 'Project deleted.'); + $this->deleteRecord(Project::class, $id, 'Project', 'projects'); } public function resetProjectForm(): void @@ -388,8 +400,7 @@ final class SettingsManager extends Component public function deleteBoard(int $id): void { - KanbanBoard::query()->findOrFail($id)->delete(); - session()->flash('success', 'Kanban board deleted.'); + $this->deleteRecord(KanbanBoard::class, $id, 'Kanban board', 'boards'); } public function resetBoardForm(): void @@ -415,6 +426,7 @@ final class SettingsManager extends Component GitRepository::query()->updateOrCreate(['id' => $this->repositoryForm['id']], $data); $this->resetRepositoryForm(); + $this->section = 'git'; session()->flash('success', 'Git repository saved.'); } @@ -426,8 +438,7 @@ final class SettingsManager extends Component public function deleteRepository(int $id): void { - GitRepository::query()->findOrFail($id)->delete(); - session()->flash('success', 'Git repository deleted.'); + $this->deleteRecord(GitRepository::class, $id, 'Git repository', 'git'); } public function resetRepositoryForm(): void @@ -453,6 +464,7 @@ final class SettingsManager extends Component WorkflowTemplate::query()->updateOrCreate(['id' => $this->workflowForm['id']], $data); $this->resetWorkflowTemplateForm(); + $this->section = 'workflow'; session()->flash('success', 'Workflow message saved.'); } @@ -464,8 +476,7 @@ final class SettingsManager extends Component public function deleteWorkflowTemplate(int $id): void { - WorkflowTemplate::query()->findOrFail($id)->delete(); - session()->flash('success', 'Workflow message deleted.'); + $this->deleteRecord(WorkflowTemplate::class, $id, 'Workflow message', 'workflow'); } public function resetWorkflowTemplateForm(): void @@ -492,6 +503,7 @@ final class SettingsManager extends Component $data['slug'] = $this->slug($data['slug'] ?: $data['name']); CannedResponse::query()->updateOrCreate(['id' => $this->cannedForm['id']], $data); $this->resetCannedResponseForm(); + $this->section = 'canned'; session()->flash('success', 'Canned response saved.'); } @@ -503,8 +515,7 @@ final class SettingsManager extends Component public function deleteCannedResponse(int $id): void { - CannedResponse::query()->findOrFail($id)->delete(); - session()->flash('success', 'Canned response deleted.'); + $this->deleteRecord(CannedResponse::class, $id, 'Canned response', 'canned'); } public function resetCannedResponseForm(): void @@ -534,6 +545,7 @@ final class SettingsManager extends Component } SlaPolicy::query()->updateOrCreate(['id' => $this->slaForm['id']], $data); $this->resetSlaPolicyForm(); + $this->section = 'sla'; session()->flash('success', 'SLA policy saved.'); } @@ -545,8 +557,7 @@ final class SettingsManager extends Component public function deleteSlaPolicy(int $id): void { - SlaPolicy::query()->findOrFail($id)->delete(); - session()->flash('success', 'SLA policy deleted.'); + $this->deleteRecord(SlaPolicy::class, $id, 'SLA policy', 'sla'); } public function resetSlaPolicyForm(): void @@ -578,6 +589,7 @@ final class SettingsManager extends Component $data['slug'] = $this->slug($data['slug'] ?: $data['name']); AutomationRule::query()->updateOrCreate(['id' => $this->automationForm['id']], $data); $this->resetAutomationRuleForm(); + $this->section = 'automation'; session()->flash('success', 'Automation rule saved.'); } @@ -589,8 +601,7 @@ final class SettingsManager extends Component public function deleteAutomationRule(int $id): void { - AutomationRule::query()->findOrFail($id)->delete(); - session()->flash('success', 'Automation rule deleted.'); + $this->deleteRecord(AutomationRule::class, $id, 'Automation rule', 'automation'); } public function resetAutomationRuleForm(): void @@ -615,6 +626,7 @@ final class SettingsManager extends Component $data['slug'] = $this->slug($data['slug'] ?: $data['name']); Product::query()->updateOrCreate(['id' => $this->productForm['id']], $data); $this->resetProductForm(); + $this->section = 'products'; session()->flash('success', 'Product/service saved.'); } @@ -626,8 +638,7 @@ final class SettingsManager extends Component public function deleteProduct(int $id): void { - Product::query()->findOrFail($id)->delete(); - session()->flash('success', 'Product/service deleted.'); + $this->deleteRecord(Product::class, $id, 'Product/service', 'products'); } public function resetProductForm(): void @@ -648,6 +659,7 @@ final class SettingsManager extends Component $data['slug'] = $this->slug($data['slug'] ?: $data['name']); Tag::query()->updateOrCreate(['id' => $this->tagForm['id']], $data); $this->resetTagForm(); + $this->section = 'tags'; session()->flash('success', 'Tag saved.'); } @@ -659,8 +671,7 @@ final class SettingsManager extends Component public function deleteTag(int $id): void { - Tag::query()->findOrFail($id)->delete(); - session()->flash('success', 'Tag deleted.'); + $this->deleteRecord(Tag::class, $id, 'Tag', 'tags'); } public function resetTagForm(): void @@ -687,6 +698,7 @@ final class SettingsManager extends Component $data['items'] = collect(preg_split('/\r\n|\r|\n/', $itemsText) ?: [])->map(fn ($line): string => trim($line))->filter()->values()->all(); ChecklistTemplate::query()->updateOrCreate(['id' => $this->checklistTemplateForm['id']], $data); $this->resetChecklistTemplateForm(); + $this->section = 'checklists'; session()->flash('success', 'Checklist template saved.'); } @@ -701,8 +713,7 @@ final class SettingsManager extends Component public function deleteChecklistTemplate(int $id): void { - ChecklistTemplate::query()->findOrFail($id)->delete(); - session()->flash('success', 'Checklist template deleted.'); + $this->deleteRecord(ChecklistTemplate::class, $id, 'Checklist template', 'checklists'); } public function resetChecklistTemplateForm(): void @@ -736,6 +747,7 @@ final class SettingsManager extends Component ->update(['is_default_helpdesk_company' => true]); } + $this->section = 'defaults'; session()->flash('success', 'Default Helpdesk company saved.'); } @@ -822,6 +834,24 @@ final class SettingsManager extends Component ->get(); } + + /** + * Delete a settings record without letting foreign-key protection crash the UI. + * + * @param class-string $modelClass + */ + private function deleteRecord(string $modelClass, int $id, string $label, string $section): void + { + $this->section = $section; + + try { + $modelClass::query()->findOrFail($id)->delete(); + session()->flash('success', $label.' deleted.'); + } catch (Throwable) { + session()->flash('error', $label.' could not be deleted because it is still used by tickets or related Helpdesk records.'); + } + } + /** * Convert browser select empty strings into null before Livewire validation. * diff --git a/Modules/Helpdesk/Resources/views/livewire/settings-manager.blade.php b/Modules/Helpdesk/Resources/views/livewire/settings-manager.blade.php index a106460f..8a582425 100644 --- a/Modules/Helpdesk/Resources/views/livewire/settings-manager.blade.php +++ b/Modules/Helpdesk/Resources/views/livewire/settings-manager.blade.php @@ -230,7 +230,7 @@ @if ($section === 'defaults') -
+ @foreach ($companies as $company) @@ -256,17 +256,17 @@ @if ($section === 'statuses') - - + + - - + + -
Save statusReset
+
Save statusReset
@@ -276,7 +276,7 @@ @if ($section === 'priorities') -
+ @@ -295,7 +295,7 @@ @if ($section === 'categories') - + @foreach ($companies as $company)@endforeach @@ -315,7 +315,7 @@ @if ($section === 'teams') - + @foreach ($companies as $company)@endforeach @@ -335,7 +335,7 @@ @if ($section === 'projects') - + @foreach ($companies as $company)@endforeach @foreach ($teams as $team)@endforeach @@ -357,7 +357,7 @@ @if ($section === 'boards') - + @@ -438,7 +438,7 @@ @if ($section === 'git') - + @foreach ($companies as $company) @@ -472,7 +472,7 @@ @if ($section === 'workflow') - + @@ -541,7 +541,7 @@ @if ($section === 'canned') - + @@ -570,7 +570,7 @@ @if ($section === 'sla') - + @foreach ($priorities as $priority)@endforeach @@ -590,7 +590,7 @@ @if ($section === 'automation') - + @@ -612,21 +612,21 @@ @if ($section === 'checklists') - @foreach ($categories as $category)@endforeach@foreach ($projects as $project)@endforeach
Save checklistReset
+
@foreach ($categories as $category)@endforeach@foreach ($projects as $project)@endforeach
Save checklistReset
@forelse ($checklistTemplates as $template)

{{ $template->name }}

{{ count($template->items ?? []) }} items

EditDelete
@empty
No checklist templates yet.
@endforelse
@endif @if ($section === 'tags') -
Save tagReset
+
Save tagReset
@forelse ($tags as $tag)
{{ $tag->name }}
EditDelete
@empty
No tags yet.
@endforelse
@endif @if ($section === 'products') -
Save product/serviceReset
+
Save product/serviceReset
@forelse ($products as $product)

{{ $product->name }}

{{ ucfirst($product->type) }} · {{ $product->version ?: 'No version' }} · {{ $product->tickets_count }} tickets

EditDelete
@empty
No products or services yet.
@endforelse
@endif diff --git a/Modules/Helpdesk/Tests/Feature/SettingsCrudTest.php b/Modules/Helpdesk/Tests/Feature/SettingsCrudTest.php index d0df55ab..61f09bad 100644 --- a/Modules/Helpdesk/Tests/Feature/SettingsCrudTest.php +++ b/Modules/Helpdesk/Tests/Feature/SettingsCrudTest.php @@ -272,4 +272,31 @@ final class SettingsCrudTest extends TestCase self::assertNull(GitRepository::query()->find($repository->id)); self::assertNull(WorkflowTemplate::query()->find($template->id)); } + + public function test_settings_manager_keeps_each_section_open_after_crud_actions(): void + { + $admin = $this->admin(); + + Livewire::actingAs($admin) + ->test(SettingsManager::class) + ->call('showSection', 'priorities') + ->assertSet('section', 'priorities') + ->set('priorityForm.name', 'Browser Critical') + ->set('priorityForm.color', 'red') + ->set('priorityForm.sort_order', 5) + ->call('savePriority') + ->assertHasNoErrors() + ->assertSet('section', 'priorities'); + + $priority = Priority::query()->where('slug', 'browser-critical')->firstOrFail(); + + Livewire::actingAs($admin) + ->test(SettingsManager::class) + ->call('deletePriority', $priority->id) + ->assertSet('section', 'priorities') + ->assertHasNoErrors(); + + $this->assertDatabaseMissing('helpdesk_priorities', ['id' => $priority->id]); + } + } diff --git a/Modules/Helpdesk/changelog.yaml b/Modules/Helpdesk/changelog.yaml index cd94c771..eeb9af13 100644 --- a/Modules/Helpdesk/changelog.yaml +++ b/Modules/Helpdesk/changelog.yaml @@ -402,3 +402,11 @@ versions: - Added feature coverage proving every Helpdesk settings area can create records when optional select controls submit empty browser values. - Kept module.json version in sync with changelog.yaml. +- version: 1.9.28 + date: 2026-06-18 + changed: + - Fixed Helpdesk settings CRUD browser submissions by adding explicit Livewire submit prevention to every settings form. + - Kept users on the active settings panel after create, update, delete and defaults actions. + - Added safe delete handling for all settings records so protected rows show a clear message instead of breaking the page. + - Added feature and browser coverage for settings CRUD panel persistence and browser-driven status create/edit/delete. + - Kept module.json version in sync with changelog.yaml. diff --git a/Modules/Helpdesk/module.json b/Modules/Helpdesk/module.json index 1eaf7c35..5eb66f49 100644 --- a/Modules/Helpdesk/module.json +++ b/Modules/Helpdesk/module.json @@ -22,5 +22,5 @@ "Modules\\Helpdesk\\Providers\\HelpdeskServiceProvider" ], "files": [], - "version": "1.9.27" + "version": "1.9.28" }