intial ticket fix partial delete fix

This commit is contained in:
Mark Gregory
2026-06-18 15:08:02 +01:00
parent 7cd8feec83
commit 8912d28b7b
5 changed files with 113 additions and 48 deletions
+58 -28
View File
@@ -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<Model> $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.
*
@@ -230,7 +230,7 @@
@if ($section === 'defaults')
<x-helpdesk::settings-section title="Ticket defaults" description="Choose the company default used when a new Helpdesk ticket is created. Lookup defaults are configured on the Statuses, Priorities, Categories, Teams and Projects sections.">
<x-slot:form>
<form wire:submit="saveDefaultHelpdeskCompany" class="grid gap-4 md:grid-cols-3">
<form wire:submit.prevent="saveDefaultHelpdeskCompany" class="grid gap-4 md:grid-cols-3">
<flux:select wire:model="default_helpdesk_company_id" label="Default company">
<option value="">No default company</option>
@foreach ($companies as $company)
@@ -256,17 +256,17 @@
@if ($section === 'statuses')
<x-helpdesk::settings-section title="Statuses" description="Control workflow stages and mark which statuses close tickets.">
<x-slot:form>
<form wire:submit="saveStatus" class="grid gap-4 md:grid-cols-5">
<flux:input wire:model="statusForm.name" label="Name" />
<form wire:submit.prevent="saveStatus" class="grid gap-4 md:grid-cols-5">
<flux:input wire:model="statusForm.name" label="Name" dusk="helpdesk-status-name" />
<flux:input wire:model="statusForm.slug" label="Slug" placeholder="auto-generated" />
<flux:input wire:model="statusForm.color" label="Badge colour" />
<flux:input wire:model="statusForm.sort_order" label="Sort" type="number" min="0" />
<flux:input wire:model="statusForm.color" label="Badge colour" dusk="helpdesk-status-color" />
<flux:input wire:model="statusForm.sort_order" label="Sort" type="number" min="0" dusk="helpdesk-status-sort" />
<label class="flex items-end gap-2 pb-2 text-sm text-zinc-700 dark:text-zinc-200"><input type="checkbox" wire:model="statusForm.is_closed" class="rounded border-zinc-300"> Closed status</label>
<label class="md:col-span-5 flex items-center gap-2 text-sm font-semibold text-zinc-700 dark:text-zinc-200">
<input type="checkbox" wire:model="statusForm.is_default" class="rounded border-zinc-300 bg-zinc-950 text-blue-600 focus:ring-blue-500">
Default status for new tickets
</label>
<div class="md:col-span-5 flex gap-2"><flux:button type="submit" variant="primary" class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save status</flux:button><flux:button type="button" wire:click="resetStatusForm" variant="ghost" class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset</flux:button></div>
<div class="md:col-span-5 flex gap-2"><flux:button type="submit" variant="primary" class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60" dusk="helpdesk-save-status">Save status</flux:button><flux:button type="button" wire:click="resetStatusForm" variant="ghost" class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset</flux:button></div>
</form>
</x-slot:form>
<x-helpdesk::lookup-table :items="$statuses" edit="editStatus" delete="deleteStatus" />
@@ -276,7 +276,7 @@
@if ($section === 'priorities')
<x-helpdesk::settings-section title="Priorities" description="Define ticket urgency levels and display order.">
<x-slot:form>
<form wire:submit="savePriority" class="grid gap-4 md:grid-cols-4">
<form wire:submit.prevent="savePriority" class="grid gap-4 md:grid-cols-4">
<flux:input wire:model="priorityForm.name" label="Name" />
<flux:input wire:model="priorityForm.slug" label="Slug" placeholder="auto-generated" />
<flux:input wire:model="priorityForm.color" label="Badge colour" />
@@ -295,7 +295,7 @@
@if ($section === 'categories')
<x-helpdesk::settings-section title="Categories" description="Group tickets by support area, optionally per company.">
<x-slot:form>
<form wire:submit="saveCategory" class="grid gap-4 md:grid-cols-4">
<form wire:submit.prevent="saveCategory" class="grid gap-4 md:grid-cols-4">
<flux:select wire:model="categoryForm.company_id" label="Company"><option value="">Global</option>@foreach ($companies as $company)<option value="{{ $company->id }}">{{ $company->name }}</option>@endforeach</flux:select>
<flux:input wire:model="categoryForm.name" label="Name" />
<flux:input wire:model="categoryForm.slug" label="Slug" placeholder="auto-generated" />
@@ -315,7 +315,7 @@
@if ($section === 'teams')
<x-helpdesk::settings-section title="Teams" description="Create support teams under each company and assign projects to them.">
<x-slot:form>
<form wire:submit="saveTeam" class="grid gap-4 md:grid-cols-4">
<form wire:submit.prevent="saveTeam" class="grid gap-4 md:grid-cols-4">
<flux:select wire:model="teamForm.company_id" label="Company"><option value="">Global</option>@foreach ($companies as $company)<option value="{{ $company->id }}">{{ $company->name }}</option>@endforeach</flux:select>
<flux:input wire:model="teamForm.name" label="Name" />
<flux:input wire:model="teamForm.slug" label="Slug" placeholder="auto-generated" />
@@ -335,7 +335,7 @@
@if ($section === 'projects')
<x-helpdesk::settings-section title="Projects" description="Create company projects and optionally assign them to support teams.">
<x-slot:form>
<form wire:submit="saveProject" class="grid gap-4 md:grid-cols-5">
<form wire:submit.prevent="saveProject" class="grid gap-4 md:grid-cols-5">
<flux:select wire:model="projectForm.company_id" label="Company"><option value="">Global</option>@foreach ($companies as $company)<option value="{{ $company->id }}">{{ $company->name }}</option>@endforeach</flux:select>
<flux:select wire:model="projectForm.team_id" label="Team"><option value="">No team</option>@foreach ($teams as $team)<option value="{{ $team->id }}">{{ $team->name }}</option>@endforeach</flux:select>
<flux:input wire:model="projectForm.title" label="Title" />
@@ -357,7 +357,7 @@
@if ($section === 'boards')
<x-helpdesk::settings-section title="Kanban Boards" description="Create configurable workflow boards and choose which statuses appear as draggable columns.">
<x-slot:form>
<form wire:submit="saveBoard" class="grid gap-4 md:grid-cols-7">
<form wire:submit.prevent="saveBoard" class="grid gap-4 md:grid-cols-7">
<flux:input wire:model="boardForm.name" label="Name" />
<flux:input wire:model="boardForm.slug" label="Slug" placeholder="auto-generated" />
<flux:input wire:model="boardForm.sort_order" label="Sort" type="number" min="0" />
@@ -438,7 +438,7 @@
@if ($section === 'git')
<x-helpdesk::settings-section title="Git repositories" description="Configure repositories that can be linked to projects and tickets. Commits can then be stored against tickets for traceability.">
<x-slot:form>
<form wire:submit="saveRepository" class="grid gap-4 md:grid-cols-6">
<form wire:submit.prevent="saveRepository" class="grid gap-4 md:grid-cols-6">
<flux:select wire:model="repositoryForm.company_id" label="Company">
<option value="">Global</option>
@foreach ($companies as $company)
@@ -472,7 +472,7 @@
@if ($section === 'workflow')
<x-helpdesk::settings-section title="Workflow messages" description="Define the professional email messages sent when tickets are raised, updated, replied to and closed.">
<x-slot:form>
<form wire:submit="saveWorkflowTemplate" class="grid gap-4 md:grid-cols-6">
<form wire:submit.prevent="saveWorkflowTemplate" class="grid gap-4 md:grid-cols-6">
<flux:input wire:model="workflowForm.name" label="Name" />
<flux:select wire:model="workflowForm.event" label="Event">
<option value="ticket.created">Ticket raised</option>
@@ -541,7 +541,7 @@
@if ($section === 'canned')
<x-helpdesk::settings-section title="Canned responses" description="Save professional reusable replies with macro tokens for ticket, requester, agent, status and URL values.">
<x-slot:form>
<form wire:submit="saveCannedResponse" class="grid gap-4 md:grid-cols-6">
<form wire:submit.prevent="saveCannedResponse" class="grid gap-4 md:grid-cols-6">
<flux:input wire:model="cannedForm.name" label="Name" />
<flux:input wire:model="cannedForm.slug" label="Slug" placeholder="auto-generated" />
<flux:input wire:model="cannedForm.sort_order" label="Sort" type="number" min="0" />
@@ -570,7 +570,7 @@
@if ($section === 'sla')
<x-helpdesk::settings-section title="SLA policies" description="Define first response and resolution targets used for breach indicators, reporting and ticket cards.">
<x-slot:form>
<form wire:submit="saveSlaPolicy" class="grid gap-4 md:grid-cols-6">
<form wire:submit.prevent="saveSlaPolicy" class="grid gap-4 md:grid-cols-6">
<flux:input wire:model="slaForm.name" label="Name" />
<flux:input wire:model="slaForm.slug" label="Slug" placeholder="auto-generated" />
<flux:select wire:model="slaForm.priority_id" label="Priority"><option value="">Any priority</option>@foreach ($priorities as $priority)<option value="{{ $priority->id }}">{{ $priority->name }}</option>@endforeach</flux:select>
@@ -590,7 +590,7 @@
@if ($section === 'automation')
<x-helpdesk::settings-section title="Email automation rules" description="Match incoming support messages and automatically assign priority, category, team, project, agent and status.">
<x-slot:form>
<form wire:submit="saveAutomationRule" class="grid gap-4 md:grid-cols-6">
<form wire:submit.prevent="saveAutomationRule" class="grid gap-4 md:grid-cols-6">
<flux:input wire:model="automationForm.name" label="Name" />
<flux:input wire:model="automationForm.slug" label="Slug" placeholder="auto-generated" />
<flux:input wire:model="automationForm.sort_order" label="Sort" type="number" min="0" />
@@ -612,21 +612,21 @@
@if ($section === 'checklists')
<x-helpdesk::settings-section title="Checklist templates" description="Apply repeatable checklist items by category or project and show completion progress on tickets and Kanban cards.">
<x-slot:form><form wire:submit="saveChecklistTemplate" class="grid gap-4 md:grid-cols-6"><flux:input wire:model="checklistTemplateForm.name" label="Name" /><flux:input wire:model="checklistTemplateForm.slug" label="Slug" /><flux:select wire:model="checklistTemplateForm.category_id" label="Category"><option value="">Any category</option>@foreach ($categories as $category)<option value="{{ $category->id }}">{{ $category->name }}</option>@endforeach</flux:select><flux:select wire:model="checklistTemplateForm.project_id" label="Project"><option value="">Any project</option>@foreach ($projects as $project)<option value="{{ $project->id }}">{{ $project->title }}</option>@endforeach</flux:select><label class="flex items-end gap-2 pb-2 text-sm"><input type="checkbox" wire:model="checklistTemplateForm.is_active" class="rounded border-zinc-300"> Active</label><div class="md:col-span-7"><flux:textarea wire:model="checklistTemplateForm.items_text" label="Checklist items, one per line" rows="6" /></div><div class="md:col-span-7 flex gap-2"><flux:button type="submit" variant="primary" class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save checklist</flux:button><flux:button type="button" wire:click="resetChecklistTemplateForm" variant="ghost" class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset</flux:button></div></form></x-slot:form>
<x-slot:form><form wire:submit.prevent="saveChecklistTemplate" class="grid gap-4 md:grid-cols-6"><flux:input wire:model="checklistTemplateForm.name" label="Name" /><flux:input wire:model="checklistTemplateForm.slug" label="Slug" /><flux:select wire:model="checklistTemplateForm.category_id" label="Category"><option value="">Any category</option>@foreach ($categories as $category)<option value="{{ $category->id }}">{{ $category->name }}</option>@endforeach</flux:select><flux:select wire:model="checklistTemplateForm.project_id" label="Project"><option value="">Any project</option>@foreach ($projects as $project)<option value="{{ $project->id }}">{{ $project->title }}</option>@endforeach</flux:select><label class="flex items-end gap-2 pb-2 text-sm"><input type="checkbox" wire:model="checklistTemplateForm.is_active" class="rounded border-zinc-300"> Active</label><div class="md:col-span-7"><flux:textarea wire:model="checklistTemplateForm.items_text" label="Checklist items, one per line" rows="6" /></div><div class="md:col-span-7 flex gap-2"><flux:button type="submit" variant="primary" class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save checklist</flux:button><flux:button type="button" wire:click="resetChecklistTemplateForm" variant="ghost" class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset</flux:button></div></form></x-slot:form>
<div class="grid gap-3 md:grid-cols-2 xl:grid-cols-3">@forelse ($checklistTemplates as $template)<div class="rounded-2xl border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900"><h3 class="font-semibold">{{ $template->name }}</h3><p class="mt-1 text-sm text-zinc-500">{{ count($template->items ?? []) }} items</p><div class="mt-4 flex justify-end gap-2"><flux:button size="sm" variant="primary" wire:click="editChecklistTemplate({{ $template->id }})" class="!bg-blue-600 !text-white hover:!bg-blue-700 focus:!ring-blue-400/60">Edit</flux:button><flux:button size="sm" variant="danger" wire:click="deleteChecklistTemplate({{ $template->id }})" wire:confirm="Delete this checklist?" class="!bg-red-600 !text-white hover:!bg-red-700 focus:!ring-red-400/60">Delete</flux:button></div></div>@empty<div class="rounded-2xl border border-dashed border-zinc-300 p-8 text-center text-sm text-zinc-500 dark:border-zinc-700 md:col-span-2 xl:col-span-3">No checklist templates yet.</div>@endforelse</div>
</x-helpdesk::settings-section>
@endif
@if ($section === 'tags')
<x-helpdesk::settings-section title="Ticket tags" description="Maintain visible ticket labels for search, triage and Kanban card context.">
<x-slot:form><form wire:submit="saveTag" class="grid gap-4 md:grid-cols-5"><flux:input wire:model="tagForm.name" label="Name" /><flux:input wire:model="tagForm.slug" label="Slug" /><flux:input wire:model="tagForm.color" label="Colour" /><div class="md:col-span-5 flex gap-2"><flux:button type="submit" variant="primary" class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save tag</flux:button><flux:button type="button" wire:click="resetTagForm" variant="ghost" class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset</flux:button></div></form></x-slot:form>
<x-slot:form><form wire:submit.prevent="saveTag" class="grid gap-4 md:grid-cols-5"><flux:input wire:model="tagForm.name" label="Name" /><flux:input wire:model="tagForm.slug" label="Slug" /><flux:input wire:model="tagForm.color" label="Colour" /><div class="md:col-span-5 flex gap-2"><flux:button type="submit" variant="primary" class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save tag</flux:button><flux:button type="button" wire:click="resetTagForm" variant="ghost" class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset</flux:button></div></form></x-slot:form>
<div class="flex flex-wrap gap-3">@forelse ($tags as $tag)<div class="rounded-2xl border border-zinc-200 bg-white px-4 py-3 dark:border-zinc-700 dark:bg-zinc-900"><span class="rounded-full px-2 py-1 text-xs font-semibold text-white" style="background-color: {{ \Modules\Helpdesk\Support\Color::css($tag->color) }}">{{ $tag->name }}</span><div class="mt-3 flex gap-2"><flux:button size="sm" variant="primary" wire:click="editTag({{ $tag->id }})" class="!bg-blue-600 !text-white hover:!bg-blue-700 focus:!ring-blue-400/60">Edit</flux:button><flux:button size="sm" variant="danger" wire:click="deleteTag({{ $tag->id }})" wire:confirm="Delete this tag?" class="!bg-red-600 !text-white hover:!bg-red-700 focus:!ring-red-400/60">Delete</flux:button></div></div>@empty<div class="rounded-2xl border border-dashed border-zinc-300 p-8 text-center text-sm text-zinc-500 dark:border-zinc-700">No tags yet.</div>@endforelse</div>
</x-helpdesk::settings-section>
@endif
@if ($section === 'products')
<x-helpdesk::settings-section title="Products and services" description="Link tickets to products, services, releases or builds and report support cost/time by product.">
<x-slot:form><form wire:submit="saveProduct" class="grid gap-4 md:grid-cols-6"><flux:input wire:model="productForm.name" label="Name" /><flux:input wire:model="productForm.slug" label="Slug" /><flux:input wire:model="productForm.type" label="Type" /><flux:input wire:model="productForm.version" label="Version" /><flux:input wire:model="productForm.release_channel" label="Release/build" /><label class="flex items-end gap-2 pb-2 text-sm"><input type="checkbox" wire:model="productForm.is_active" class="rounded border-zinc-300"> Active</label><div class="md:col-span-7"><flux:textarea wire:model="productForm.description" label="Description" rows="3" /></div><div class="md:col-span-7 flex gap-2"><flux:button type="submit" variant="primary" class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save product/service</flux:button><flux:button type="button" wire:click="resetProductForm" variant="ghost" class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset</flux:button></div></form></x-slot:form>
<x-slot:form><form wire:submit.prevent="saveProduct" class="grid gap-4 md:grid-cols-6"><flux:input wire:model="productForm.name" label="Name" /><flux:input wire:model="productForm.slug" label="Slug" /><flux:input wire:model="productForm.type" label="Type" /><flux:input wire:model="productForm.version" label="Version" /><flux:input wire:model="productForm.release_channel" label="Release/build" /><label class="flex items-end gap-2 pb-2 text-sm"><input type="checkbox" wire:model="productForm.is_active" class="rounded border-zinc-300"> Active</label><div class="md:col-span-7"><flux:textarea wire:model="productForm.description" label="Description" rows="3" /></div><div class="md:col-span-7 flex gap-2"><flux:button type="submit" variant="primary" class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save product/service</flux:button><flux:button type="button" wire:click="resetProductForm" variant="ghost" class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset</flux:button></div></form></x-slot:form>
<div class="grid gap-3 md:grid-cols-2 xl:grid-cols-3">@forelse ($products as $product)<div class="rounded-2xl border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900"><h3 class="font-semibold">{{ $product->name }}</h3><p class="mt-1 text-sm text-zinc-500">{{ ucfirst($product->type) }} · {{ $product->version ?: 'No version' }} · {{ $product->tickets_count }} tickets</p><div class="mt-4 flex justify-end gap-2"><flux:button size="sm" variant="primary" wire:click="editProduct({{ $product->id }})" class="!bg-blue-600 !text-white hover:!bg-blue-700 focus:!ring-blue-400/60">Edit</flux:button><flux:button size="sm" variant="danger" wire:click="deleteProduct({{ $product->id }})" wire:confirm="Delete this product/service?" class="!bg-red-600 !text-white hover:!bg-red-700 focus:!ring-red-400/60">Delete</flux:button></div></div>@empty<div class="rounded-2xl border border-dashed border-zinc-300 p-8 text-center text-sm text-zinc-500 dark:border-zinc-700 md:col-span-2 xl:col-span-3">No products or services yet.</div>@endforelse</div>
</x-helpdesk::settings-section>
@endif
@@ -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]);
}
}
+8
View File
@@ -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.
+1 -1
View File
@@ -22,5 +22,5 @@
"Modules\\Helpdesk\\Providers\\HelpdeskServiceProvider"
],
"files": [],
"version": "1.9.27"
"version": "1.9.28"
}