structure updates

This commit is contained in:
Mark Gregory
2026-08-26 11:30:36 +01:00
parent 8404c6e40f
commit 66fd8b0e8a
5 changed files with 786 additions and 155 deletions
+282 -14
View File
@@ -12,6 +12,7 @@ use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Livewire\Component;
use Livewire\WithPagination;
use Modules\Helpdesk\Models\AutomationRule;
use Modules\Helpdesk\Models\CannedResponse;
use Modules\Helpdesk\Models\Category;
@@ -30,10 +31,26 @@ use Modules\Helpdesk\Support\Color;
final class SettingsManager extends Component
{
use WithPagination;
public string $section = 'statuses';
public bool $teamModalOpen = false;
public bool $statusModalOpen = false;
public bool $priorityModalOpen = false;
public bool $categoryModalOpen = false;
public bool $projectModalOpen = false;
public bool $boardModalOpen = false;
public bool $repositoryModalOpen = false;
public bool $workflowModalOpen = false;
public bool $cannedModalOpen = false;
public bool $slaModalOpen = false;
public bool $automationModalOpen = false;
public bool $checklistModalOpen = false;
public bool $tagModalOpen = false;
public bool $productModalOpen = false;
/** @var array<string, mixed> */
public array $statusForm = ['id' => null, 'name' => '', 'slug' => '', 'color' => 'gray', 'sort_order' => 0, 'is_closed' => false, 'is_default' => false];
@@ -141,6 +158,7 @@ final class SettingsManager extends Component
$this->persistSettingModel(Status::class, $this->statusForm['id'], $data);
$this->resetStatusForm();
$this->statusModalOpen = false;
session()->flash('success', 'Status saved.');
}
@@ -149,6 +167,7 @@ final class SettingsManager extends Component
$this->statusForm = Status::query()->findOrFail($id)->only(['id', 'name', 'slug', 'color', 'sort_order', 'is_closed', 'is_default']);
$this->statusForm['color'] = Color::normalise($this->statusForm['color'] ?? 'gray');
$this->section = 'statuses';
$this->statusModalOpen = true;
}
public function deleteStatus(int $id): void
@@ -157,6 +176,21 @@ final class SettingsManager extends Component
session()->flash('success', 'Status deleted.');
}
public function openCreateStatusModal(): void
{
$this->resetValidation();
$this->resetStatusForm();
$this->section = 'statuses';
$this->statusModalOpen = true;
}
public function closeStatusModal(): void
{
$this->resetValidation();
$this->resetStatusForm();
$this->statusModalOpen = false;
}
public function resetStatusForm(): void
{
$this->statusForm = ['id' => null, 'name' => '', 'slug' => '', 'color' => 'gray', 'sort_order' => 0, 'is_closed' => false, 'is_default' => false];
@@ -180,6 +214,7 @@ final class SettingsManager extends Component
}
$this->persistSettingModel(Priority::class, $this->priorityForm['id'], $data);
$this->resetPriorityForm();
$this->priorityModalOpen = false;
session()->flash('success', 'Priority saved.');
}
@@ -188,6 +223,7 @@ final class SettingsManager extends Component
$this->priorityForm = Priority::query()->findOrFail($id)->only(['id', 'name', 'slug', 'color', 'sort_order', 'is_default']);
$this->priorityForm['color'] = Color::normalise($this->priorityForm['color'] ?? 'gray');
$this->section = 'priorities';
$this->priorityModalOpen = true;
}
public function deletePriority(int $id): void
@@ -196,6 +232,21 @@ final class SettingsManager extends Component
session()->flash('success', 'Priority deleted.');
}
public function openCreatePriorityModal(): void
{
$this->resetValidation();
$this->resetPriorityForm();
$this->section = 'priorities';
$this->priorityModalOpen = true;
}
public function closePriorityModal(): void
{
$this->resetValidation();
$this->resetPriorityForm();
$this->priorityModalOpen = false;
}
public function resetPriorityForm(): void
{
$this->priorityForm = ['id' => null, 'name' => '', 'slug' => '', 'color' => 'gray', 'sort_order' => 0, 'is_default' => false];
@@ -221,6 +272,7 @@ final class SettingsManager extends Component
}
$this->persistSettingModel(Category::class, $this->categoryForm['id'], $data);
$this->resetCategoryForm();
$this->categoryModalOpen = false;
session()->flash('success', 'Category saved.');
}
@@ -229,6 +281,7 @@ final class SettingsManager extends Component
$this->categoryForm = Category::query()->findOrFail($id)->only(['id', 'name', 'slug', 'color', 'description', 'is_default']);
$this->categoryForm['color'] = Color::normalise($this->categoryForm['color'] ?? 'blue');
$this->section = 'categories';
$this->categoryModalOpen = true;
}
public function deleteCategory(int $id): void
@@ -237,6 +290,21 @@ final class SettingsManager extends Component
session()->flash('success', 'Category deleted.');
}
public function openCreateCategoryModal(): void
{
$this->resetValidation();
$this->resetCategoryForm();
$this->section = 'categories';
$this->categoryModalOpen = true;
}
public function closeCategoryModal(): void
{
$this->resetValidation();
$this->resetCategoryForm();
$this->categoryModalOpen = false;
}
public function resetCategoryForm(): void
{
$this->categoryForm = ['id' => null, 'name' => '', 'slug' => '', 'color' => 'blue', 'description' => '', 'is_default' => false];
@@ -401,6 +469,7 @@ final class SettingsManager extends Component
}
$this->persistSettingModel(Project::class, $this->projectForm['id'], $data);
$this->resetProjectForm();
$this->projectModalOpen = false;
session()->flash('success', 'Project saved.');
}
@@ -409,6 +478,7 @@ final class SettingsManager extends Component
$this->projectForm = Project::query()->findOrFail($id)->only(['id', 'team_id', 'title', 'slug', 'color', 'description', 'is_default', 'archived_at']);
$this->projectForm['color'] = Color::normalise($this->projectForm['color'] ?? 'blue');
$this->section = 'projects';
$this->projectModalOpen = true;
}
public function deleteProject(int $id): void
@@ -417,6 +487,21 @@ final class SettingsManager extends Component
session()->flash('success', 'Project deleted.');
}
public function openCreateProjectModal(): void
{
$this->resetValidation();
$this->resetProjectForm();
$this->section = 'projects';
$this->projectModalOpen = true;
}
public function closeProjectModal(): void
{
$this->resetValidation();
$this->resetProjectForm();
$this->projectModalOpen = false;
}
public function resetProjectForm(): void
{
$this->projectForm = ['id' => null, 'team_id' => null, 'title' => '', 'slug' => '', 'color' => 'blue', 'description' => '', 'is_default' => false, 'archived_at' => null];
@@ -456,6 +541,7 @@ final class SettingsManager extends Component
$board->statuses()->sync($sync);
$this->resetBoardForm();
$this->boardModalOpen = false;
$this->section = 'boards';
session()->flash('success', 'Kanban board saved.');
}
@@ -476,6 +562,7 @@ final class SettingsManager extends Component
];
$this->section = 'boards';
$this->boardModalOpen = true;
}
public function deleteBoard(int $id): void
@@ -484,6 +571,21 @@ final class SettingsManager extends Component
session()->flash('success', 'Kanban board deleted.');
}
public function openCreateBoardModal(): void
{
$this->resetValidation();
$this->resetBoardForm();
$this->section = 'boards';
$this->boardModalOpen = true;
}
public function closeBoardModal(): void
{
$this->resetValidation();
$this->resetBoardForm();
$this->boardModalOpen = false;
}
public function resetBoardForm(): void
{
$this->boardForm = ['id' => null, 'name' => '', 'slug' => '', 'description' => '', 'owner_id' => null, 'is_default' => false, 'sort_order' => 0, 'status_ids' => []];
@@ -506,6 +608,7 @@ final class SettingsManager extends Component
$this->persistSettingModel(GitRepository::class, $this->repositoryForm['id'], $data);
$this->resetRepositoryForm();
$this->repositoryModalOpen = false;
session()->flash('success', 'Git repository saved.');
}
@@ -513,6 +616,7 @@ final class SettingsManager extends Component
{
$this->repositoryForm = GitRepository::query()->findOrFail($id)->only(['id', 'project_id', 'name', 'provider', 'remote_url', 'default_branch', 'webhook_secret']);
$this->section = 'git';
$this->repositoryModalOpen = true;
}
public function deleteRepository(int $id): void
@@ -521,6 +625,21 @@ final class SettingsManager extends Component
session()->flash('success', 'Git repository deleted.');
}
public function openCreateRepositoryModal(): void
{
$this->resetValidation();
$this->resetRepositoryForm();
$this->section = 'git';
$this->repositoryModalOpen = true;
}
public function closeRepositoryModal(): void
{
$this->resetValidation();
$this->resetRepositoryForm();
$this->repositoryModalOpen = false;
}
public function resetRepositoryForm(): void
{
$this->repositoryForm = ['id' => null, 'project_id' => null, 'name' => '', 'provider' => 'git', 'remote_url' => '', 'default_branch' => 'main', 'webhook_secret' => ''];
@@ -544,6 +663,7 @@ final class SettingsManager extends Component
$this->persistSettingModel(WorkflowTemplate::class, $this->workflowForm['id'], $data);
$this->resetWorkflowTemplateForm();
$this->workflowModalOpen = false;
session()->flash('success', 'Workflow message saved.');
}
@@ -551,6 +671,7 @@ final class SettingsManager extends Component
{
$this->workflowForm = WorkflowTemplate::query()->findOrFail($id)->only(['id', 'name', 'event', 'subject', 'html_body', 'text_body', 'to_role', 'cc', 'bcc', 'is_active']);
$this->section = 'workflow';
$this->workflowModalOpen = true;
}
public function deleteWorkflowTemplate(int $id): void
@@ -559,6 +680,21 @@ final class SettingsManager extends Component
session()->flash('success', 'Workflow message deleted.');
}
public function openCreateWorkflowTemplateModal(): void
{
$this->resetValidation();
$this->resetWorkflowTemplateForm();
$this->section = 'workflow';
$this->workflowModalOpen = true;
}
public function closeWorkflowTemplateModal(): void
{
$this->resetValidation();
$this->resetWorkflowTemplateForm();
$this->workflowModalOpen = false;
}
public function resetWorkflowTemplateForm(): void
{
$this->workflowForm = ['id' => null, 'name' => '', 'event' => 'ticket.created', 'subject' => '', 'html_body' => '', 'text_body' => '', 'to_role' => 'requester', 'cc' => '', 'bcc' => '', 'is_active' => true];
@@ -582,6 +718,7 @@ final class SettingsManager extends Component
$data['slug'] = $this->slug($data['slug'] ?: $data['name']);
$this->persistSettingModel(CannedResponse::class, $this->cannedForm['id'], $data);
$this->resetCannedResponseForm();
$this->cannedModalOpen = false;
session()->flash('success', 'Canned response saved.');
}
@@ -589,6 +726,7 @@ final class SettingsManager extends Component
{
$this->cannedForm = CannedResponse::query()->findOrFail($id)->only(['id', 'category_id', 'team_id', 'name', 'slug', 'visibility', 'subject', 'body', 'is_active', 'sort_order']);
$this->section = 'canned';
$this->cannedModalOpen = true;
}
public function deleteCannedResponse(int $id): void
@@ -597,6 +735,21 @@ final class SettingsManager extends Component
session()->flash('success', 'Canned response deleted.');
}
public function openCreateCannedResponseModal(): void
{
$this->resetValidation();
$this->resetCannedResponseForm();
$this->section = 'canned';
$this->cannedModalOpen = true;
}
public function closeCannedResponseModal(): void
{
$this->resetValidation();
$this->resetCannedResponseForm();
$this->cannedModalOpen = false;
}
public function resetCannedResponseForm(): void
{
$this->cannedForm = ['id' => null, 'category_id' => null, 'team_id' => null, 'name' => '', 'slug' => '', 'visibility' => 'agents', 'subject' => '', 'body' => '', 'is_active' => true, 'sort_order' => 0];
@@ -623,6 +776,7 @@ final class SettingsManager extends Component
}
$this->persistSettingModel(SlaPolicy::class, $this->slaForm['id'], $data);
$this->resetSlaPolicyForm();
$this->slaModalOpen = false;
session()->flash('success', 'SLA policy saved.');
}
@@ -630,6 +784,7 @@ final class SettingsManager extends Component
{
$this->slaForm = SlaPolicy::query()->findOrFail($id)->only(['id', 'priority_id', 'category_id', 'name', 'slug', 'first_response_minutes', 'resolution_minutes', 'business_hours_only', 'is_default', 'is_active']);
$this->section = 'sla';
$this->slaModalOpen = true;
}
public function deleteSlaPolicy(int $id): void
@@ -638,6 +793,21 @@ final class SettingsManager extends Component
session()->flash('success', 'SLA policy deleted.');
}
public function openCreateSlaPolicyModal(): void
{
$this->resetValidation();
$this->resetSlaPolicyForm();
$this->section = 'sla';
$this->slaModalOpen = true;
}
public function closeSlaPolicyModal(): void
{
$this->resetValidation();
$this->resetSlaPolicyForm();
$this->slaModalOpen = false;
}
public function resetSlaPolicyForm(): void
{
$this->slaForm = ['id' => null, 'priority_id' => null, 'category_id' => null, 'name' => '', 'slug' => '', 'first_response_minutes' => 240, 'resolution_minutes' => 2880, 'business_hours_only' => false, 'is_default' => false, 'is_active' => true];
@@ -666,6 +836,7 @@ final class SettingsManager extends Component
$data['slug'] = $this->slug($data['slug'] ?: $data['name']);
$this->persistSettingModel(AutomationRule::class, $this->automationForm['id'], $data);
$this->resetAutomationRuleForm();
$this->automationModalOpen = false;
session()->flash('success', 'Automation rule saved.');
}
@@ -673,6 +844,7 @@ final class SettingsManager extends Component
{
$this->automationForm = AutomationRule::query()->findOrFail($id)->only(['id', 'name', 'slug', 'match_sender', 'match_subject', 'match_mailbox', 'match_category_id', 'set_priority_id', 'set_category_id', 'set_team_id', 'set_project_id', 'set_status_id', 'set_agent_id', 'is_active', 'sort_order']);
$this->section = 'automation';
$this->automationModalOpen = true;
}
public function deleteAutomationRule(int $id): void
@@ -681,6 +853,21 @@ final class SettingsManager extends Component
session()->flash('success', 'Automation rule deleted.');
}
public function openCreateAutomationRuleModal(): void
{
$this->resetValidation();
$this->resetAutomationRuleForm();
$this->section = 'automation';
$this->automationModalOpen = true;
}
public function closeAutomationRuleModal(): void
{
$this->resetValidation();
$this->resetAutomationRuleForm();
$this->automationModalOpen = false;
}
public function resetAutomationRuleForm(): void
{
$this->automationForm = ['id' => null, 'name' => '', 'slug' => '', 'match_sender' => '', 'match_subject' => '', 'match_mailbox' => '', 'match_category_id' => null, 'set_priority_id' => null, 'set_category_id' => null, 'set_team_id' => null, 'set_project_id' => null, 'set_status_id' => null, 'set_agent_id' => null, 'is_active' => true, 'sort_order' => 0];
@@ -702,6 +889,7 @@ final class SettingsManager extends Component
$data['slug'] = $this->slug($data['slug'] ?: $data['name']);
$this->persistSettingModel(Product::class, $this->productForm['id'], $data);
$this->resetProductForm();
$this->productModalOpen = false;
session()->flash('success', 'Product/service saved.');
}
@@ -709,6 +897,7 @@ final class SettingsManager extends Component
{
$this->productForm = Product::query()->findOrFail($id)->only(['id', 'name', 'slug', 'type', 'version', 'release_channel', 'description', 'is_active']);
$this->section = 'products';
$this->productModalOpen = true;
}
public function deleteProduct(int $id): void
@@ -717,6 +906,21 @@ final class SettingsManager extends Component
session()->flash('success', 'Product/service deleted.');
}
public function openCreateProductModal(): void
{
$this->resetValidation();
$this->resetProductForm();
$this->section = 'products';
$this->productModalOpen = true;
}
public function closeProductModal(): void
{
$this->resetValidation();
$this->resetProductForm();
$this->productModalOpen = false;
}
public function resetProductForm(): void
{
$this->productForm = ['id' => null, 'name' => '', 'slug' => '', 'type' => 'service', 'version' => '', 'release_channel' => '', 'description' => '', 'is_active' => true];
@@ -734,6 +938,7 @@ final class SettingsManager extends Component
$data['slug'] = $this->slug($data['slug'] ?: $data['name']);
$this->persistSettingModel(Tag::class, $this->tagForm['id'], $data);
$this->resetTagForm();
$this->tagModalOpen = false;
session()->flash('success', 'Tag saved.');
}
@@ -742,6 +947,7 @@ final class SettingsManager extends Component
$this->tagForm = Tag::query()->findOrFail($id)->only(['id', 'name', 'slug', 'color']);
$this->tagForm['color'] = Color::normalise($this->tagForm['color'] ?? 'gray');
$this->section = 'tags';
$this->tagModalOpen = true;
}
public function deleteTag(int $id): void
@@ -750,6 +956,21 @@ final class SettingsManager extends Component
session()->flash('success', 'Tag deleted.');
}
public function openCreateTagModal(): void
{
$this->resetValidation();
$this->resetTagForm();
$this->section = 'tags';
$this->tagModalOpen = true;
}
public function closeTagModal(): void
{
$this->resetValidation();
$this->resetTagForm();
$this->tagModalOpen = false;
}
public function resetTagForm(): void
{
$this->tagForm = ['id' => null, 'name' => '', 'slug' => '', 'color' => 'gray'];
@@ -773,6 +994,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();
$this->persistSettingModel(ChecklistTemplate::class, $this->checklistTemplateForm['id'], $data);
$this->resetChecklistTemplateForm();
$this->checklistModalOpen = false;
session()->flash('success', 'Checklist template saved.');
}
@@ -783,6 +1005,7 @@ final class SettingsManager extends Component
'items_text' => collect($template->items ?? [])->map(fn ($item): string => is_array($item) ? (string) ($item['title'] ?? '') : (string) $item)->implode("\n"),
];
$this->section = 'checklists';
$this->checklistModalOpen = true;
}
public function deleteChecklistTemplate(int $id): void
@@ -791,6 +1014,21 @@ final class SettingsManager extends Component
session()->flash('success', 'Checklist template deleted.');
}
public function openCreateChecklistTemplateModal(): void
{
$this->resetValidation();
$this->resetChecklistTemplateForm();
$this->section = 'checklists';
$this->checklistModalOpen = true;
}
public function closeChecklistTemplateModal(): void
{
$this->resetValidation();
$this->resetChecklistTemplateForm();
$this->checklistModalOpen = false;
}
public function resetChecklistTemplateForm(): void
{
$this->checklistTemplateForm = ['id' => null, 'category_id' => null, 'project_id' => null, 'name' => '', 'slug' => '', 'items_text' => '', 'is_active' => true];
@@ -810,15 +1048,45 @@ final class SettingsManager extends Component
'boardOwners' => User::query()->where(static function ($query): void {
$query->where('helpdesk_agent', true)->orWhere('helpdesk_admin', true);
})->orderBy('name')->get(['id', 'name', 'email']),
'boards' => KanbanBoard::query()->with(['statuses', 'owner:id,name,email'])->withCount('tickets')->orderBy('sort_order')->orderBy('name')->get(),
'repositories' => GitRepository::query()->with(['project'])->withCount(['commits', 'webhookLogs'])->orderBy('name')->get(),
'workflowTemplates' => WorkflowTemplate::query()->orderBy('event')->orderBy('name')->get(),
'cannedResponses' => CannedResponse::query()->with(['category', 'team'])->orderBy('sort_order')->orderBy('name')->get(),
'slaPolicies' => SlaPolicy::query()->with(['priority', 'category'])->orderByDesc('is_default')->orderBy('name')->get(),
'automationRules' => AutomationRule::query()->orderBy('sort_order')->orderBy('name')->get(),
'products' => Product::query()->withCount('tickets')->orderBy('name')->get(),
'tags' => Tag::query()->orderBy('name')->get(),
'checklistTemplates' => ChecklistTemplate::query()->orderBy('name')->get(),
'boards' => KanbanBoard::query()
->with(['statuses', 'owner:id,name,email'])
->withCount('tickets')
->orderBy('sort_order')
->orderBy('name')
->paginate(10, ['*'], 'boardsPage'),
'repositories' => GitRepository::query()
->with(['project'])
->withCount(['commits', 'webhookLogs'])
->orderBy('name')
->paginate(10, ['*'], 'gitPage'),
'workflowTemplates' => WorkflowTemplate::query()
->orderBy('event')
->orderBy('name')
->paginate(10, ['*'], 'workflowPage'),
'cannedResponses' => CannedResponse::query()
->with(['category', 'team'])
->orderBy('sort_order')
->orderBy('name')
->paginate(10, ['*'], 'cannedPage'),
'slaPolicies' => SlaPolicy::query()
->with(['priority', 'category'])
->orderByDesc('is_default')
->orderBy('name')
->paginate(10, ['*'], 'slaPage'),
'automationRules' => AutomationRule::query()
->orderBy('sort_order')
->orderBy('name')
->paginate(10, ['*'], 'rulesPage'),
'products' => Product::query()
->withCount('tickets')
->orderBy('name')
->paginate(10, ['*'], 'productsPage'),
'tags' => Tag::query()
->orderBy('name')
->paginate(10, ['*'], 'tagsPage'),
'checklistTemplates' => ChecklistTemplate::query()
->orderBy('name')
->paginate(10, ['*'], 'checksPage'),
'colourOptions' => Color::options(),
])->layout('layouts.app');
}
@@ -830,7 +1098,7 @@ final class SettingsManager extends Component
->when($this->hasDefaultColumn('helpdesk_statuses'), fn ($query) => $query->orderByDesc('is_default'))
->orderBy('sort_order')
->orderBy('name')
->get();
->paginate(10, ['*'], 'statusesPage');
}
private function orderedPriorities(): mixed
@@ -840,7 +1108,7 @@ final class SettingsManager extends Component
->when($this->hasDefaultColumn('helpdesk_priorities'), fn ($query) => $query->orderByDesc('is_default'))
->orderBy('sort_order')
->orderBy('name')
->get();
->paginate(10, ['*'], 'prioritiesPage');
}
private function orderedCategories(): mixed
@@ -849,7 +1117,7 @@ final class SettingsManager extends Component
->withCount('tickets')
->when($this->hasDefaultColumn('helpdesk_categories'), fn ($query) => $query->orderByDesc('is_default'))
->orderBy('name')
->get();
->paginate(10, ['*'], 'categoriesPage');
}
private function orderedTeams(): mixed
@@ -859,7 +1127,7 @@ final class SettingsManager extends Component
->withCount(['projects', 'tickets', 'users'])
->when($this->hasDefaultColumn('helpdesk_teams'), fn ($query) => $query->orderByDesc('is_default'))
->orderBy('name')
->get();
->paginate(10, ['*'], 'teamsPage');
}
private function orderedProjects(): mixed
@@ -869,7 +1137,7 @@ final class SettingsManager extends Component
->withCount('tickets')
->when($this->hasDefaultColumn('helpdesk_projects'), fn ($query) => $query->orderByDesc('is_default'))
->orderBy('title')
->get();
->paginate(10, ['*'], 'projectsPage');
}
/**
@@ -982,3 +982,38 @@ span.flatpickr-weekday {
box-shadow: 0 0 0 1000px #09090b inset !important;
transition: background-color 9999s ease-in-out 0s;
}
/* Helpdesk settings modal presentation */
.hd-settings-modal {
text-align: left;
}
.hd-settings-modal form {
width: 100%;
text-align: left;
}
.hd-settings-modal form > * {
min-width: 0;
}
.hd-settings-modal input:not([type="checkbox"]):not([type="radio"]):not([type="color"]),
.hd-settings-modal select,
.hd-settings-modal textarea {
width: 100%;
}
.hd-settings-modal textarea {
line-height: 1.5;
}
.hd-settings-modal label {
text-align: left;
}
@media (min-width: 768px) {
.hd-settings-modal {
padding-inline: 0.25rem;
}
}
@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Modules\Helpdesk\Tests\Feature;
use Modules\Helpdesk\Livewire\SettingsManager;
use ReflectionClass;
use Tests\TestCase;
final class SettingsModalStructureTest extends TestCase
{
public function test_each_settings_editor_exposes_a_modal_state_property(): void
{
$reflection = new ReflectionClass(SettingsManager::class);
foreach ([
'statusModalOpen', 'priorityModalOpen', 'categoryModalOpen',
'projectModalOpen', 'boardModalOpen', 'repositoryModalOpen',
'workflowModalOpen', 'cannedModalOpen', 'slaModalOpen',
'automationModalOpen', 'checklistModalOpen', 'tagModalOpen',
'productModalOpen',
] as $property) {
self::assertTrue($reflection->hasProperty($property), $property);
}
}
}
@@ -0,0 +1,28 @@
@props(['paginator'])
@if ($paginator instanceof \Illuminate\Contracts\Pagination\Paginator)
<div class="flex flex-col gap-3 border-t border-zinc-200 bg-zinc-50/70 px-4 py-4 dark:border-zinc-800 dark:bg-zinc-950/35 sm:flex-row sm:items-center sm:justify-between">
<div class="text-xs font-medium text-zinc-500 sm:text-sm">
@if (method_exists($paginator, 'total'))
Showing
<span class="font-semibold text-zinc-800 dark:text-zinc-200">{{ $paginator->firstItem() ?? 0 }}</span>
–
<span class="font-semibold text-zinc-800 dark:text-zinc-200">{{ $paginator->lastItem() ?? 0 }}</span>
of
<span class="font-semibold text-zinc-800 dark:text-zinc-200">{{ $paginator->total() }}</span>
@else
Page {{ $paginator->currentPage() }}
@endif
</div>
@if ($paginator->hasPages())
<div class="min-w-0">
{{ $paginator->onEachSide(1)->links() }}
</div>
@else
<div class="rounded-lg border border-zinc-200 bg-white px-3 py-1.5 text-xs font-semibold text-zinc-500 dark:border-zinc-700 dark:bg-zinc-900">
Page 1
</div>
@endif
</div>
@endif
@@ -3,20 +3,20 @@
* Keep the settings view safe during rolling module upgrades or Livewire navigate
* hydration where the compiled Blade view may be newer than the loaded component.
*/
$statuses = collect($statuses ?? []);
$priorities = collect($priorities ?? []);
$categories = collect($categories ?? []);
$teams = collect($teams ?? []);
$projects = collect($projects ?? []);
$boards = collect($boards ?? []);
$repositories = collect($repositories ?? []);
$workflowTemplates = collect($workflowTemplates ?? []);
$cannedResponses = collect($cannedResponses ?? []);
$slaPolicies = collect($slaPolicies ?? []);
$automationRules = collect($automationRules ?? []);
$checklistTemplates = collect($checklistTemplates ?? []);
$tags = collect($tags ?? []);
$products = collect($products ?? []);
$statuses = $statuses ?? collect();
$priorities = $priorities ?? collect();
$categories = $categories ?? collect();
$teams = $teams ?? collect();
$projects = $projects ?? collect();
$boards = $boards ?? collect();
$repositories = $repositories ?? collect();
$workflowTemplates = $workflowTemplates ?? collect();
$cannedResponses = $cannedResponses ?? collect();
$slaPolicies = $slaPolicies ?? collect();
$automationRules = $automationRules ?? collect();
$checklistTemplates = $checklistTemplates ?? collect();
$tags = $tags ?? collect();
$products = $products ?? collect();
@endphp
<div class="hd-helpdesk-surface hd-mobile-ready hd-core-settings space-y-4" dusk="helpdesk-settings-manager">
@@ -43,20 +43,20 @@
</div>
<div class="grid gap-2 md:grid-cols-4 xl:grid-cols-7">
@foreach ([
'statuses' => ['label' => 'Statuses', 'count' => $statuses->count()],
'priorities' => ['label' => 'Priorities', 'count' => $priorities->count()],
'categories' => ['label' => 'Categories', 'count' => $categories->count()],
'teams' => ['label' => 'Teams', 'count' => $teams->count()],
'projects' => ['label' => 'Projects', 'count' => $projects->count()],
'boards' => ['label' => 'Boards', 'count' => $boards->count()],
'git' => ['label' => 'Git', 'count' => $repositories->count()],
'workflow' => ['label' => 'Workflow', 'count' => $workflowTemplates->count()],
'canned' => ['label' => 'Canned', 'count' => $cannedResponses->count()],
'sla' => ['label' => 'SLA', 'count' => $slaPolicies->count()],
'automation' => ['label' => 'Rules', 'count' => $automationRules->count()],
'checklists' => ['label' => 'Checks', 'count' => $checklistTemplates->count()],
'tags' => ['label' => 'Tags', 'count' => $tags->count()],
'products' => ['label' => 'Products', 'count' => $products->count()],
'statuses' => ['label' => 'Statuses', 'count' => method_exists($statuses, 'total') ? $statuses->total() : $statuses->count()],
'priorities' => ['label' => 'Priorities', 'count' => method_exists($priorities, 'total') ? $priorities->total() : $priorities->count()],
'categories' => ['label' => 'Categories', 'count' => method_exists($categories, 'total') ? $categories->total() : $categories->count()],
'teams' => ['label' => 'Teams', 'count' => method_exists($teams, 'total') ? $teams->total() : $teams->count()],
'projects' => ['label' => 'Projects', 'count' => method_exists($projects, 'total') ? $projects->total() : $projects->count()],
'boards' => ['label' => 'Boards', 'count' => method_exists($boards, 'total') ? $boards->total() : $boards->count()],
'git' => ['label' => 'Git', 'count' => method_exists($repositories, 'total') ? $repositories->total() : $repositories->count()],
'workflow' => ['label' => 'Workflow', 'count' => method_exists($workflowTemplates, 'total') ? $workflowTemplates->total() : $workflowTemplates->count()],
'canned' => ['label' => 'Canned', 'count' => method_exists($cannedResponses, 'total') ? $cannedResponses->total() : $cannedResponses->count()],
'sla' => ['label' => 'SLA', 'count' => method_exists($slaPolicies, 'total') ? $slaPolicies->total() : $slaPolicies->count()],
'automation' => ['label' => 'Rules', 'count' => method_exists($automationRules, 'total') ? $automationRules->total() : $automationRules->count()],
'checklists' => ['label' => 'Checks', 'count' => method_exists($checklistTemplates, 'total') ? $checklistTemplates->total() : $checklistTemplates->count()],
'tags' => ['label' => 'Tags', 'count' => method_exists($tags, 'total') ? $tags->total() : $tags->count()],
'products' => ['label' => 'Products', 'count' => method_exists($products, 'total') ? $products->total() : $products->count()],
] as $key => $item)
@php
$sectionPalette = match ($key) {
@@ -103,12 +103,34 @@
@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.prevent="saveStatus" class="grid gap-3 xl:grid-cols-12">
<div class="xl:col-span-3">
<x-slot:actions>
<flux:button
size="sm"
type="button"
wire:click="openCreateStatusModal"
variant="primary"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60"
>
<i class="fa-solid fa-plus mr-1" aria-hidden="true"></i>
Create status
</flux:button>
</x-slot:actions>
<flux:modal
name="helpdesk-statuses-editor"
wire:model="statusModalOpen"
class="hd-settings-modal w-full max-w-4xl"
@close="closeStatusModal"
>
<div class="mb-5 border-b border-zinc-200 pb-4 dark:border-zinc-700">
<flux:heading size="lg">{{ filled($statusForm['id'] ?? null) ? 'Edit status' : 'Create status' }}</flux:heading>
<flux:subheading class="mt-1">Create or edit this status without leaving the settings list.</flux:subheading>
</div>
<form wire:submit="saveStatus" class="grid items-start gap-x-4 gap-y-5 xl:grid-cols-12">
<div class="xl:col-span-4">
<flux:input wire:model="statusForm.name" label="Name" />
</div>
<div class="xl:col-span-3">
<div class="xl:col-span-4">
<flux:input wire:model="statusForm.slug" label="Slug" placeholder="auto-generated" />
</div>
<x-helpdesk.colour-select class="xl:col-span-12" model="statusForm.color" label="Badge colour" :options="$colourOptions" />
@@ -124,41 +146,52 @@
class="rounded border-zinc-300 bg-zinc-950 text-blue-600 focus:ring-blue-500">
Default status for new tickets
</label>
@error('teamForm.name')
<div class="xl:col-span-12 rounded-lg border border-red-500/30 bg-red-500/10 px-3 py-2 text-sm text-red-300">{{ $message }}</div>
@enderror
@error('teamForm.slug')
<div class="xl:col-span-12 rounded-lg border border-red-500/30 bg-red-500/10 px-3 py-2 text-sm text-red-300">{{ $message }}</div>
@enderror
@error('teamForm.color')
<div class="xl:col-span-12 rounded-lg border border-red-500/30 bg-red-500/10 px-3 py-2 text-sm text-red-300">{{ $message }}</div>
@enderror
@error('teamForm.user_ids.*')
<div class="xl:col-span-12 rounded-lg border border-red-500/30 bg-red-500/10 px-3 py-2 text-sm text-red-300">{{ $message }}</div>
@enderror
<div class="flex flex-wrap items-center gap-2 xl:col-span-12">
<flux:button size="sm" type="submit" variant="primary"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save status
</flux:button>
<flux:button size="sm" type="button" wire:click="resetStatusForm" variant="ghost"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset
<flux:button size="sm" type="button" wire:click="closeStatusModal" variant="ghost"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Cancel
</flux:button>
</div>
</form>
</x-slot:form>
</flux:modal>
<x-helpdesk.lookup-table :items="$statuses" edit="editStatus" delete="deleteStatus" />
</x-helpdesk.settings-section>
<x-helpdesk.settings-pagination :paginator="$statuses" />
</x-helpdesk.settings-section>
@endif
@if ($section === 'priorities')
<x-helpdesk.settings-section title="Priorities" description="Define ticket urgency levels and display order.">
<x-slot:form>
<form wire:submit.prevent="savePriority" class="grid gap-3 xl:grid-cols-12">
<div class="xl:col-span-3">
<x-slot:actions>
<flux:button
size="sm"
type="button"
wire:click="openCreatePriorityModal"
variant="primary"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60"
>
<i class="fa-solid fa-plus mr-1" aria-hidden="true"></i>
Create priority
</flux:button>
</x-slot:actions>
<flux:modal
name="helpdesk-priorities-editor"
wire:model="priorityModalOpen"
class="hd-settings-modal w-full max-w-4xl"
@close="closePriorityModal"
>
<div class="mb-5 border-b border-zinc-200 pb-4 dark:border-zinc-700">
<flux:heading size="lg">{{ filled($priorityForm['id'] ?? null) ? 'Edit priority' : 'Create priority' }}</flux:heading>
<flux:subheading class="mt-1">Create or edit this priority without leaving the settings list.</flux:subheading>
</div>
<form wire:submit="savePriority" class="grid items-start gap-x-4 gap-y-5 xl:grid-cols-12">
<div class="xl:col-span-4">
<flux:input wire:model="priorityForm.name" label="Name" />
</div>
<div class="xl:col-span-3">
<div class="xl:col-span-4">
<flux:input wire:model="priorityForm.slug" label="Slug" placeholder="auto-generated" />
</div>
<x-helpdesk.colour-select class="xl:col-span-12" model="priorityForm.color" label="Badge colour" :options="$colourOptions" />
@@ -174,27 +207,48 @@
<flux:button size="sm" type="submit" variant="primary"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save priority
</flux:button>
<flux:button size="sm" type="button" wire:click="resetPriorityForm" variant="ghost"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset
<flux:button size="sm" type="button" wire:click="closePriorityModal" variant="ghost"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Cancel
</flux:button>
</div>
</form>
</x-slot:form>
</flux:modal>
<x-helpdesk.lookup-table :items="$priorities" edit="editPriority" delete="deletePriority" />
</x-helpdesk.settings-section>
<x-helpdesk.settings-pagination :paginator="$priorities" />
</x-helpdesk.settings-section>
@endif
@if ($section === 'categories')
<x-helpdesk.settings-section title="Categories"
description="Group tickets by support area.">
<x-slot:form>
<form wire:submit.prevent="saveCategory" class="grid gap-3 xl:grid-cols-12">
<div class="xl:col-span-3">
</div>
<div class="xl:col-span-3">
<x-slot:actions>
<flux:button
size="sm"
type="button"
wire:click="openCreateCategoryModal"
variant="primary"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60"
>
<i class="fa-solid fa-plus mr-1" aria-hidden="true"></i>
Create category
</flux:button>
</x-slot:actions>
<flux:modal
name="helpdesk-categories-editor"
wire:model="categoryModalOpen"
class="hd-settings-modal w-full max-w-4xl"
@close="closeCategoryModal"
>
<div class="mb-5 border-b border-zinc-200 pb-4 dark:border-zinc-700">
<flux:heading size="lg">{{ filled($categoryForm['id'] ?? null) ? 'Edit category' : 'Create category' }}</flux:heading>
<flux:subheading class="mt-1">Create or edit this category without leaving the settings list.</flux:subheading>
</div>
<form wire:submit="saveCategory" class="grid items-start gap-x-4 gap-y-5 xl:grid-cols-12">
<div class="xl:col-span-4">
<flux:input wire:model="categoryForm.name" label="Name" />
</div>
<div class="xl:col-span-3">
<div class="xl:col-span-4">
<flux:input wire:model="categoryForm.slug" label="Slug" placeholder="auto-generated" />
</div>
<x-helpdesk.colour-select class="xl:col-span-12" model="categoryForm.color" label="Badge colour" :options="$colourOptions" />
@@ -210,14 +264,15 @@
<flux:button size="sm" type="submit" variant="primary"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save
category</flux:button>
<flux:button size="sm" type="button" wire:click="resetCategoryForm" variant="ghost"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset
<flux:button size="sm" type="button" wire:click="closeCategoryModal" variant="ghost"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Cancel
</flux:button>
</div>
</form>
</x-slot:form>
</flux:modal>
<x-helpdesk.lookup-table :items="$categories" edit="editCategory" delete="deleteCategory" />
</x-helpdesk.settings-section>
<x-helpdesk.settings-pagination :paginator="$categories" />
</x-helpdesk.settings-section>
@endif
@if ($section === 'teams')
@@ -241,7 +296,7 @@
<flux:modal
name="helpdesk-team-editor"
wire:model="teamModalOpen"
class="w-full max-w-5xl"
class="hd-settings-modal w-full max-w-5xl"
@close="closeTeamModal"
>
<form wire:submit="saveTeam" class="space-y-5">
@@ -307,19 +362,6 @@
Default team for new tickets
</label>
@error('teamForm.name')
<div class="rounded-lg border border-red-500/30 bg-red-500/10 px-3 py-2 text-sm text-red-600 dark:text-red-300">{{ $message }}</div>
@enderror
@error('teamForm.slug')
<div class="rounded-lg border border-red-500/30 bg-red-500/10 px-3 py-2 text-sm text-red-600 dark:text-red-300">{{ $message }}</div>
@enderror
@error('teamForm.color')
<div class="rounded-lg border border-red-500/30 bg-red-500/10 px-3 py-2 text-sm text-red-600 dark:text-red-300">{{ $message }}</div>
@enderror
@error('teamForm.user_ids.*')
<div class="rounded-lg border border-red-500/30 bg-red-500/10 px-3 py-2 text-sm text-red-600 dark:text-red-300">{{ $message }}</div>
@enderror
<div class="flex flex-wrap justify-end gap-2 border-t border-zinc-200 pt-4 dark:border-zinc-700">
<flux:button type="button" variant="ghost" wire:click="closeTeamModal">
Cancel
@@ -331,14 +373,37 @@
</div>
</form>
</flux:modal>
</x-helpdesk.settings-section>
<x-helpdesk.settings-pagination :paginator="$teams" />
</x-helpdesk.settings-section>
@endif
@if ($section === 'projects')
<x-helpdesk.settings-section title="Projects"
description="Create projects and optionally assign them to support teams.">
<x-slot:form>
<form wire:submit.prevent="saveProject" class="grid gap-3 xl:grid-cols-12">
<x-slot:actions>
<flux:button
size="sm"
type="button"
wire:click="openCreateProjectModal"
variant="primary"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60"
>
<i class="fa-solid fa-plus mr-1" aria-hidden="true"></i>
Create project
</flux:button>
</x-slot:actions>
<flux:modal
name="helpdesk-projects-editor"
wire:model="projectModalOpen"
class="hd-settings-modal w-full max-w-5xl"
@close="closeProjectModal"
>
<div class="mb-5 border-b border-zinc-200 pb-4 dark:border-zinc-700">
<flux:heading size="lg">{{ filled($projectForm['id'] ?? null) ? 'Edit project' : 'Create project' }}</flux:heading>
<flux:subheading class="mt-1">Create or edit this project without leaving the settings list.</flux:subheading>
</div>
<form wire:submit="saveProject" class="grid items-start gap-x-4 gap-y-5 xl:grid-cols-12">
<div class="xl:col-span-2">
</div>
<div class="xl:col-span-2">
@@ -368,22 +433,45 @@
<flux:button size="sm" type="submit" variant="primary"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save project
</flux:button>
<flux:button size="sm" type="button" wire:click="resetProjectForm" variant="ghost"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset
<flux:button size="sm" type="button" wire:click="closeProjectModal" variant="ghost"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Cancel
</flux:button>
</div>
</form>
</x-slot:form>
</flux:modal>
<x-helpdesk.project-table :items="$projects" />
</x-helpdesk.settings-section>
<x-helpdesk.settings-pagination :paginator="$projects" />
</x-helpdesk.settings-section>
@endif
@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.prevent="saveBoard" class="grid gap-3 md:grid-cols-7">
<x-slot:actions>
<flux:button
size="sm"
type="button"
wire:click="openCreateBoardModal"
variant="primary"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60"
>
<i class="fa-solid fa-plus mr-1" aria-hidden="true"></i>
Create board
</flux:button>
</x-slot:actions>
<flux:modal
name="helpdesk-boards-editor"
wire:model="boardModalOpen"
class="hd-settings-modal w-full max-w-6xl"
@close="closeBoardModal"
>
<div class="mb-5 border-b border-zinc-200 pb-4 dark:border-zinc-700">
<flux:heading size="lg">{{ filled($boardForm['id'] ?? null) ? 'Edit board' : 'Create board' }}</flux:heading>
<flux:subheading class="mt-1">Create or edit this board without leaving the settings list.</flux:subheading>
</div>
<form wire:submit="saveBoard" class="grid items-start gap-x-4 gap-y-5 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" />
@@ -420,12 +508,12 @@
<flux:button size="sm" type="submit" variant="primary"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save board
</flux:button>
<flux:button size="sm" type="button" wire:click="resetBoardForm" variant="ghost"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset
<flux:button size="sm" type="button" wire:click="closeBoardModal" variant="ghost"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Cancel
</flux:button>
</div>
</form>
</x-slot:form>
</flux:modal>
<div
class="overflow-hidden rounded-2xl border border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900">
@@ -485,14 +573,37 @@
</tbody>
</table>
</div>
</x-helpdesk.settings-section>
<x-helpdesk.settings-pagination :paginator="$boards" />
</x-helpdesk.settings-section>
@endif
@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.prevent="saveRepository" class="grid gap-3 md:grid-cols-6">
<x-slot:actions>
<flux:button
size="sm"
type="button"
wire:click="openCreateRepositoryModal"
variant="primary"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60"
>
<i class="fa-solid fa-plus mr-1" aria-hidden="true"></i>
Create repository
</flux:button>
</x-slot:actions>
<flux:modal
name="helpdesk-git-editor"
wire:model="repositoryModalOpen"
class="hd-settings-modal w-full max-w-5xl"
@close="closeRepositoryModal"
>
<div class="mb-5 border-b border-zinc-200 pb-4 dark:border-zinc-700">
<flux:heading size="lg">{{ filled($repositoryForm['id'] ?? null) ? 'Edit repository' : 'Create repository' }}</flux:heading>
<flux:subheading class="mt-1">Create or edit this repository without leaving the settings list.</flux:subheading>
</div>
<form wire:submit="saveRepository" class="grid items-start gap-x-4 gap-y-5 md:grid-cols-6">
<flux:select wire:model="repositoryForm.project_id" label="Project">
<option value="">No project</option>
@foreach ($projects as $project)
@@ -512,22 +623,45 @@
<flux:button size="sm" type="submit" variant="primary"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save
repository</flux:button>
<flux:button size="sm" type="button" wire:click="resetRepositoryForm" variant="ghost"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset
<flux:button size="sm" type="button" wire:click="closeRepositoryModal" variant="ghost"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Cancel
</flux:button>
</div>
</form>
</x-slot:form>
</flux:modal>
<x-helpdesk.repository-table :items="$repositories" />
</x-helpdesk.settings-section>
<x-helpdesk.settings-pagination :paginator="$repositories" />
</x-helpdesk.settings-section>
@endif
@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.prevent="saveWorkflowTemplate" class="grid gap-3 md:grid-cols-6">
<x-slot:actions>
<flux:button
size="sm"
type="button"
wire:click="openCreateWorkflowTemplateModal"
variant="primary"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60"
>
<i class="fa-solid fa-plus mr-1" aria-hidden="true"></i>
Create workflow message
</flux:button>
</x-slot:actions>
<flux:modal
name="helpdesk-workflow-editor"
wire:model="workflowModalOpen"
class="hd-settings-modal w-full max-w-6xl"
@close="closeWorkflowTemplateModal"
>
<div class="mb-5 border-b border-zinc-200 pb-4 dark:border-zinc-700">
<flux:heading size="lg">{{ filled($workflowForm['id'] ?? null) ? 'Edit workflow message' : 'Create workflow message' }}</flux:heading>
<flux:subheading class="mt-1">Create or edit this workflow message without leaving the settings list.</flux:subheading>
</div>
<form wire:submit="saveWorkflowTemplate" class="grid items-start gap-x-4 gap-y-5 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>
@@ -564,12 +698,12 @@
<flux:button size="sm" type="submit" variant="primary"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save
workflow message</flux:button>
<flux:button size="sm" type="button" wire:click="resetWorkflowTemplateForm" variant="ghost"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset
<flux:button size="sm" type="button" wire:click="closeWorkflowTemplateModal" variant="ghost"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Cancel
</flux:button>
</div>
</form>
</x-slot:form>
</flux:modal>
<div
class="overflow-hidden rounded-2xl border border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900">
@@ -616,15 +750,38 @@
</tbody>
</table>
</div>
</x-helpdesk.settings-section>
<x-helpdesk.settings-pagination :paginator="$workflowTemplates" />
</x-helpdesk.settings-section>
@endif
@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.prevent="saveCannedResponse" class="grid gap-3 md:grid-cols-6">
<x-slot:actions>
<flux:button
size="sm"
type="button"
wire:click="openCreateCannedResponseModal"
variant="primary"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60"
>
<i class="fa-solid fa-plus mr-1" aria-hidden="true"></i>
Create canned response
</flux:button>
</x-slot:actions>
<flux:modal
name="helpdesk-canned-editor"
wire:model="cannedModalOpen"
class="hd-settings-modal w-full max-w-6xl"
@close="closeCannedResponseModal"
>
<div class="mb-5 border-b border-zinc-200 pb-4 dark:border-zinc-700">
<flux:heading size="lg">{{ filled($cannedForm['id'] ?? null) ? 'Edit canned response' : 'Create canned response' }}</flux:heading>
<flux:subheading class="mt-1">Create or edit this canned response without leaving the settings list.</flux:subheading>
</div>
<form wire:submit="saveCannedResponse" class="grid items-start gap-x-4 gap-y-5 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" />
@@ -654,12 +811,12 @@
<flux:button size="sm" type="submit" variant="primary"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save canned
response</flux:button>
<flux:button size="sm" type="button" wire:click="resetCannedResponseForm" variant="ghost"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset
<flux:button size="sm" type="button" wire:click="closeCannedResponseModal" variant="ghost"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Cancel
</flux:button>
</div>
</form>
</x-slot:form>
</flux:modal>
<div class="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
@forelse ($cannedResponses as $response)
<div class="rounded-2xl border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900">
@@ -692,14 +849,37 @@
No canned responses yet.</div>
@endforelse
</div>
</x-helpdesk.settings-section>
<x-helpdesk.settings-pagination :paginator="$cannedResponses" />
</x-helpdesk.settings-section>
@endif
@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.prevent="saveSlaPolicy" class="grid gap-3 md:grid-cols-6">
<x-slot:actions>
<flux:button
size="sm"
type="button"
wire:click="openCreateSlaPolicyModal"
variant="primary"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60"
>
<i class="fa-solid fa-plus mr-1" aria-hidden="true"></i>
Create SLA policy
</flux:button>
</x-slot:actions>
<flux:modal
name="helpdesk-sla-editor"
wire:model="slaModalOpen"
class="hd-settings-modal w-full max-w-5xl"
@close="closeSlaPolicyModal"
>
<div class="mb-5 border-b border-zinc-200 pb-4 dark:border-zinc-700">
<flux:heading size="lg">{{ filled($slaForm['id'] ?? null) ? 'Edit SLA policy' : 'Create SLA policy' }}</flux:heading>
<flux:subheading class="mt-1">Create or edit this SLA policy without leaving the settings list.</flux:subheading>
</div>
<form wire:submit="saveSlaPolicy" class="grid items-start gap-x-4 gap-y-5 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">
@@ -731,12 +911,12 @@
<flux:button size="sm" type="submit" variant="primary"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save SLA
policy</flux:button>
<flux:button size="sm" type="button" wire:click="resetSlaPolicyForm" variant="ghost"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset
<flux:button size="sm" type="button" wire:click="closeSlaPolicyModal" variant="ghost"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Cancel
</flux:button>
</div>
</form>
</x-slot:form>
</flux:modal>
<div
class="overflow-hidden rounded-2xl border border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900">
<table class="min-w-full divide-y divide-zinc-200 text-sm dark:divide-zinc-700">
@@ -778,14 +958,37 @@
</tbody>
</table>
</div>
</x-helpdesk.settings-section>
<x-helpdesk.settings-pagination :paginator="$slaPolicies" />
</x-helpdesk.settings-section>
@endif
@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.prevent="saveAutomationRule" class="grid gap-3 md:grid-cols-6">
<x-slot:actions>
<flux:button
size="sm"
type="button"
wire:click="openCreateAutomationRuleModal"
variant="primary"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60"
>
<i class="fa-solid fa-plus mr-1" aria-hidden="true"></i>
Create automation rule
</flux:button>
</x-slot:actions>
<flux:modal
name="helpdesk-automation-editor"
wire:model="automationModalOpen"
class="hd-settings-modal w-full max-w-6xl"
@close="closeAutomationRuleModal"
>
<div class="mb-5 border-b border-zinc-200 pb-4 dark:border-zinc-700">
<flux:heading size="lg">{{ filled($automationForm['id'] ?? null) ? 'Edit automation rule' : 'Create automation rule' }}</flux:heading>
<flux:subheading class="mt-1">Create or edit this automation rule without leaving the settings list.</flux:subheading>
</div>
<form wire:submit="saveAutomationRule" class="grid items-start gap-x-4 gap-y-5 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"
@@ -830,12 +1033,12 @@
<flux:button size="sm" type="submit" variant="primary"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save
automation rule</flux:button>
<flux:button size="sm" type="button" wire:click="resetAutomationRuleForm" variant="ghost"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset
<flux:button size="sm" type="button" wire:click="closeAutomationRuleModal" variant="ghost"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Cancel
</flux:button>
</div>
</form>
</x-slot:form>
</flux:modal>
<div class="grid gap-3 md:grid-cols-2">
@forelse ($automationRules as $rule)
<div class="rounded-2xl border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900">
@@ -858,14 +1061,37 @@
No automation rules yet.</div>
@endforelse
</div>
</x-helpdesk.settings-section>
<x-helpdesk.settings-pagination :paginator="$automationRules" />
</x-helpdesk.settings-section>
@endif
@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.prevent="saveChecklistTemplate" class="grid gap-3 md:grid-cols-6">
<x-slot:actions>
<flux:button
size="sm"
type="button"
wire:click="openCreateChecklistTemplateModal"
variant="primary"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60"
>
<i class="fa-solid fa-plus mr-1" aria-hidden="true"></i>
Create checklist
</flux:button>
</x-slot:actions>
<flux:modal
name="helpdesk-checklists-editor"
wire:model="checklistModalOpen"
class="hd-settings-modal w-full max-w-5xl"
@close="closeChecklistTemplateModal"
>
<div class="mb-5 border-b border-zinc-200 pb-4 dark:border-zinc-700">
<flux:heading size="lg">{{ filled($checklistTemplateForm['id'] ?? null) ? 'Edit checklist template' : 'Create checklist' }}</flux:heading>
<flux:subheading class="mt-1">Create or edit this checklist template without leaving the settings list.</flux:subheading>
</div>
<form wire:submit="saveChecklistTemplate" class="grid items-start gap-x-4 gap-y-5 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">
@@ -891,12 +1117,12 @@
<flux:button size="sm" type="submit" variant="primary"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save
checklist</flux:button>
<flux:button size="sm" type="button" wire:click="resetChecklistTemplateForm" variant="ghost"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset
<flux:button size="sm" type="button" wire:click="closeChecklistTemplateModal" variant="ghost"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Cancel
</flux:button>
</div>
</form>
</x-slot:form>
</flux:modal>
<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">
@@ -918,14 +1144,37 @@
No checklist templates yet.</div>
@endforelse
</div>
</x-helpdesk.settings-section>
<x-helpdesk.settings-pagination :paginator="$checklistTemplates" />
</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.prevent="saveTag" class="grid gap-3 xl:grid-cols-12">
<x-slot:actions>
<flux:button
size="sm"
type="button"
wire:click="openCreateTagModal"
variant="primary"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60"
>
<i class="fa-solid fa-plus mr-1" aria-hidden="true"></i>
Create tag
</flux:button>
</x-slot:actions>
<flux:modal
name="helpdesk-tags-editor"
wire:model="tagModalOpen"
class="hd-settings-modal w-full max-w-4xl"
@close="closeTagModal"
>
<div class="mb-5 border-b border-zinc-200 pb-4 dark:border-zinc-700">
<flux:heading size="lg">{{ filled($tagForm['id'] ?? null) ? 'Edit tag' : 'Create tag' }}</flux:heading>
<flux:subheading class="mt-1">Create or edit this tag without leaving the settings list.</flux:subheading>
</div>
<form wire:submit="saveTag" class="grid items-start gap-x-4 gap-y-5 xl:grid-cols-12">
<div class="xl:col-span-4">
<flux:input wire:model="tagForm.name" label="Name" />
</div>
@@ -937,12 +1186,12 @@
<flux:button size="sm" type="submit" variant="primary"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save tag
</flux:button>
<flux:button size="sm" type="button" wire:click="resetTagForm" variant="ghost"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset
<flux:button size="sm" type="button" wire:click="closeTagModal" variant="ghost"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Cancel
</flux:button>
</div>
</form>
</x-slot:form>
</flux:modal>
<div class="flex flex-wrap gap-3">
@forelse ($tags as $tag)
<div
@@ -963,14 +1212,37 @@
No tags yet.</div>
@endforelse
</div>
</x-helpdesk.settings-section>
<x-helpdesk.settings-pagination :paginator="$tags" />
</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.prevent="saveProduct" class="grid gap-3 md:grid-cols-6">
<x-slot:actions>
<flux:button
size="sm"
type="button"
wire:click="openCreateProductModal"
variant="primary"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60"
>
<i class="fa-solid fa-plus mr-1" aria-hidden="true"></i>
Create product
</flux:button>
</x-slot:actions>
<flux:modal
name="helpdesk-products-editor"
wire:model="productModalOpen"
class="hd-settings-modal w-full max-w-5xl"
@close="closeProductModal"
>
<div class="mb-5 border-b border-zinc-200 pb-4 dark:border-zinc-700">
<flux:heading size="lg">{{ filled($productForm['id'] ?? null) ? 'Edit product/service' : 'Create product' }}</flux:heading>
<flux:subheading class="mt-1">Create or edit this product/service without leaving the settings list.</flux:subheading>
</div>
<form wire:submit="saveProduct" class="grid items-start gap-x-4 gap-y-5 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" />
@@ -985,12 +1257,12 @@
<flux:button size="sm" type="submit" variant="primary"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save
product/service</flux:button>
<flux:button size="sm" type="button" wire:click="resetProductForm" variant="ghost"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset
<flux:button size="sm" type="button" wire:click="closeProductModal" variant="ghost"
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Cancel
</flux:button>
</div>
</form>
</x-slot:form>
</flux:modal>
<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">
@@ -1013,7 +1285,8 @@
No products or services yet.</div>
@endforelse
</div>
</x-helpdesk.settings-section>
<x-helpdesk.settings-pagination :paginator="$products" />
</x-helpdesk.settings-section>
@endif
</div>