intial ticket fix partial delete fix
This commit is contained in:
@@ -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]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -22,5 +22,5 @@
|
||||
"Modules\\Helpdesk\\Providers\\HelpdeskServiceProvider"
|
||||
],
|
||||
"files": [],
|
||||
"version": "1.9.27"
|
||||
"version": "1.9.28"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user