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\Support\Str;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
use Livewire\Component; use Livewire\Component;
use Livewire\WithPagination;
use Modules\Helpdesk\Models\AutomationRule; use Modules\Helpdesk\Models\AutomationRule;
use Modules\Helpdesk\Models\CannedResponse; use Modules\Helpdesk\Models\CannedResponse;
use Modules\Helpdesk\Models\Category; use Modules\Helpdesk\Models\Category;
@@ -30,10 +31,26 @@ use Modules\Helpdesk\Support\Color;
final class SettingsManager extends Component final class SettingsManager extends Component
{ {
use WithPagination;
public string $section = 'statuses'; public string $section = 'statuses';
public bool $teamModalOpen = false; 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> */ /** @var array<string, mixed> */
public array $statusForm = ['id' => null, 'name' => '', 'slug' => '', 'color' => 'gray', 'sort_order' => 0, 'is_closed' => false, 'is_default' => false]; 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->persistSettingModel(Status::class, $this->statusForm['id'], $data);
$this->resetStatusForm(); $this->resetStatusForm();
$this->statusModalOpen = false;
session()->flash('success', 'Status saved.'); 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 = 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->statusForm['color'] = Color::normalise($this->statusForm['color'] ?? 'gray');
$this->section = 'statuses'; $this->section = 'statuses';
$this->statusModalOpen = true;
} }
public function deleteStatus(int $id): void public function deleteStatus(int $id): void
@@ -157,6 +176,21 @@ final class SettingsManager extends Component
session()->flash('success', 'Status deleted.'); 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 public function resetStatusForm(): void
{ {
$this->statusForm = ['id' => null, 'name' => '', 'slug' => '', 'color' => 'gray', 'sort_order' => 0, 'is_closed' => false, 'is_default' => false]; $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->persistSettingModel(Priority::class, $this->priorityForm['id'], $data);
$this->resetPriorityForm(); $this->resetPriorityForm();
$this->priorityModalOpen = false;
session()->flash('success', 'Priority saved.'); 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 = Priority::query()->findOrFail($id)->only(['id', 'name', 'slug', 'color', 'sort_order', 'is_default']);
$this->priorityForm['color'] = Color::normalise($this->priorityForm['color'] ?? 'gray'); $this->priorityForm['color'] = Color::normalise($this->priorityForm['color'] ?? 'gray');
$this->section = 'priorities'; $this->section = 'priorities';
$this->priorityModalOpen = true;
} }
public function deletePriority(int $id): void public function deletePriority(int $id): void
@@ -196,6 +232,21 @@ final class SettingsManager extends Component
session()->flash('success', 'Priority deleted.'); 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 public function resetPriorityForm(): void
{ {
$this->priorityForm = ['id' => null, 'name' => '', 'slug' => '', 'color' => 'gray', 'sort_order' => 0, 'is_default' => false]; $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->persistSettingModel(Category::class, $this->categoryForm['id'], $data);
$this->resetCategoryForm(); $this->resetCategoryForm();
$this->categoryModalOpen = false;
session()->flash('success', 'Category saved.'); 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 = Category::query()->findOrFail($id)->only(['id', 'name', 'slug', 'color', 'description', 'is_default']);
$this->categoryForm['color'] = Color::normalise($this->categoryForm['color'] ?? 'blue'); $this->categoryForm['color'] = Color::normalise($this->categoryForm['color'] ?? 'blue');
$this->section = 'categories'; $this->section = 'categories';
$this->categoryModalOpen = true;
} }
public function deleteCategory(int $id): void public function deleteCategory(int $id): void
@@ -237,6 +290,21 @@ final class SettingsManager extends Component
session()->flash('success', 'Category deleted.'); 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 public function resetCategoryForm(): void
{ {
$this->categoryForm = ['id' => null, 'name' => '', 'slug' => '', 'color' => 'blue', 'description' => '', 'is_default' => false]; $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->persistSettingModel(Project::class, $this->projectForm['id'], $data);
$this->resetProjectForm(); $this->resetProjectForm();
$this->projectModalOpen = false;
session()->flash('success', 'Project saved.'); 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 = 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->projectForm['color'] = Color::normalise($this->projectForm['color'] ?? 'blue');
$this->section = 'projects'; $this->section = 'projects';
$this->projectModalOpen = true;
} }
public function deleteProject(int $id): void public function deleteProject(int $id): void
@@ -417,6 +487,21 @@ final class SettingsManager extends Component
session()->flash('success', 'Project deleted.'); 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 public function resetProjectForm(): void
{ {
$this->projectForm = ['id' => null, 'team_id' => null, 'title' => '', 'slug' => '', 'color' => 'blue', 'description' => '', 'is_default' => false, 'archived_at' => null]; $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); $board->statuses()->sync($sync);
$this->resetBoardForm(); $this->resetBoardForm();
$this->boardModalOpen = false;
$this->section = 'boards'; $this->section = 'boards';
session()->flash('success', 'Kanban board saved.'); session()->flash('success', 'Kanban board saved.');
} }
@@ -476,6 +562,7 @@ final class SettingsManager extends Component
]; ];
$this->section = 'boards'; $this->section = 'boards';
$this->boardModalOpen = true;
} }
public function deleteBoard(int $id): void public function deleteBoard(int $id): void
@@ -484,6 +571,21 @@ final class SettingsManager extends Component
session()->flash('success', 'Kanban board deleted.'); 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 public function resetBoardForm(): void
{ {
$this->boardForm = ['id' => null, 'name' => '', 'slug' => '', 'description' => '', 'owner_id' => null, 'is_default' => false, 'sort_order' => 0, 'status_ids' => []]; $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->persistSettingModel(GitRepository::class, $this->repositoryForm['id'], $data);
$this->resetRepositoryForm(); $this->resetRepositoryForm();
$this->repositoryModalOpen = false;
session()->flash('success', 'Git repository saved.'); 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->repositoryForm = GitRepository::query()->findOrFail($id)->only(['id', 'project_id', 'name', 'provider', 'remote_url', 'default_branch', 'webhook_secret']);
$this->section = 'git'; $this->section = 'git';
$this->repositoryModalOpen = true;
} }
public function deleteRepository(int $id): void public function deleteRepository(int $id): void
@@ -521,6 +625,21 @@ final class SettingsManager extends Component
session()->flash('success', 'Git repository deleted.'); 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 public function resetRepositoryForm(): void
{ {
$this->repositoryForm = ['id' => null, 'project_id' => null, 'name' => '', 'provider' => 'git', 'remote_url' => '', 'default_branch' => 'main', 'webhook_secret' => '']; $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->persistSettingModel(WorkflowTemplate::class, $this->workflowForm['id'], $data);
$this->resetWorkflowTemplateForm(); $this->resetWorkflowTemplateForm();
$this->workflowModalOpen = false;
session()->flash('success', 'Workflow message saved.'); 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->workflowForm = WorkflowTemplate::query()->findOrFail($id)->only(['id', 'name', 'event', 'subject', 'html_body', 'text_body', 'to_role', 'cc', 'bcc', 'is_active']);
$this->section = 'workflow'; $this->section = 'workflow';
$this->workflowModalOpen = true;
} }
public function deleteWorkflowTemplate(int $id): void public function deleteWorkflowTemplate(int $id): void
@@ -559,6 +680,21 @@ final class SettingsManager extends Component
session()->flash('success', 'Workflow message deleted.'); 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 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]; $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']); $data['slug'] = $this->slug($data['slug'] ?: $data['name']);
$this->persistSettingModel(CannedResponse::class, $this->cannedForm['id'], $data); $this->persistSettingModel(CannedResponse::class, $this->cannedForm['id'], $data);
$this->resetCannedResponseForm(); $this->resetCannedResponseForm();
$this->cannedModalOpen = false;
session()->flash('success', 'Canned response saved.'); 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->cannedForm = CannedResponse::query()->findOrFail($id)->only(['id', 'category_id', 'team_id', 'name', 'slug', 'visibility', 'subject', 'body', 'is_active', 'sort_order']);
$this->section = 'canned'; $this->section = 'canned';
$this->cannedModalOpen = true;
} }
public function deleteCannedResponse(int $id): void public function deleteCannedResponse(int $id): void
@@ -597,6 +735,21 @@ final class SettingsManager extends Component
session()->flash('success', 'Canned response deleted.'); 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 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]; $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->persistSettingModel(SlaPolicy::class, $this->slaForm['id'], $data);
$this->resetSlaPolicyForm(); $this->resetSlaPolicyForm();
$this->slaModalOpen = false;
session()->flash('success', 'SLA policy saved.'); 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->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->section = 'sla';
$this->slaModalOpen = true;
} }
public function deleteSlaPolicy(int $id): void public function deleteSlaPolicy(int $id): void
@@ -638,6 +793,21 @@ final class SettingsManager extends Component
session()->flash('success', 'SLA policy deleted.'); 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 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]; $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']); $data['slug'] = $this->slug($data['slug'] ?: $data['name']);
$this->persistSettingModel(AutomationRule::class, $this->automationForm['id'], $data); $this->persistSettingModel(AutomationRule::class, $this->automationForm['id'], $data);
$this->resetAutomationRuleForm(); $this->resetAutomationRuleForm();
$this->automationModalOpen = false;
session()->flash('success', 'Automation rule saved.'); 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->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->section = 'automation';
$this->automationModalOpen = true;
} }
public function deleteAutomationRule(int $id): void public function deleteAutomationRule(int $id): void
@@ -681,6 +853,21 @@ final class SettingsManager extends Component
session()->flash('success', 'Automation rule deleted.'); 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 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]; $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']); $data['slug'] = $this->slug($data['slug'] ?: $data['name']);
$this->persistSettingModel(Product::class, $this->productForm['id'], $data); $this->persistSettingModel(Product::class, $this->productForm['id'], $data);
$this->resetProductForm(); $this->resetProductForm();
$this->productModalOpen = false;
session()->flash('success', 'Product/service saved.'); 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->productForm = Product::query()->findOrFail($id)->only(['id', 'name', 'slug', 'type', 'version', 'release_channel', 'description', 'is_active']);
$this->section = 'products'; $this->section = 'products';
$this->productModalOpen = true;
} }
public function deleteProduct(int $id): void public function deleteProduct(int $id): void
@@ -717,6 +906,21 @@ final class SettingsManager extends Component
session()->flash('success', 'Product/service deleted.'); 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 public function resetProductForm(): void
{ {
$this->productForm = ['id' => null, 'name' => '', 'slug' => '', 'type' => 'service', 'version' => '', 'release_channel' => '', 'description' => '', 'is_active' => true]; $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']); $data['slug'] = $this->slug($data['slug'] ?: $data['name']);
$this->persistSettingModel(Tag::class, $this->tagForm['id'], $data); $this->persistSettingModel(Tag::class, $this->tagForm['id'], $data);
$this->resetTagForm(); $this->resetTagForm();
$this->tagModalOpen = false;
session()->flash('success', 'Tag saved.'); 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 = Tag::query()->findOrFail($id)->only(['id', 'name', 'slug', 'color']);
$this->tagForm['color'] = Color::normalise($this->tagForm['color'] ?? 'gray'); $this->tagForm['color'] = Color::normalise($this->tagForm['color'] ?? 'gray');
$this->section = 'tags'; $this->section = 'tags';
$this->tagModalOpen = true;
} }
public function deleteTag(int $id): void public function deleteTag(int $id): void
@@ -750,6 +956,21 @@ final class SettingsManager extends Component
session()->flash('success', 'Tag deleted.'); 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 public function resetTagForm(): void
{ {
$this->tagForm = ['id' => null, 'name' => '', 'slug' => '', 'color' => 'gray']; $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(); $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->persistSettingModel(ChecklistTemplate::class, $this->checklistTemplateForm['id'], $data);
$this->resetChecklistTemplateForm(); $this->resetChecklistTemplateForm();
$this->checklistModalOpen = false;
session()->flash('success', 'Checklist template saved.'); 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"), 'items_text' => collect($template->items ?? [])->map(fn ($item): string => is_array($item) ? (string) ($item['title'] ?? '') : (string) $item)->implode("\n"),
]; ];
$this->section = 'checklists'; $this->section = 'checklists';
$this->checklistModalOpen = true;
} }
public function deleteChecklistTemplate(int $id): void public function deleteChecklistTemplate(int $id): void
@@ -791,6 +1014,21 @@ final class SettingsManager extends Component
session()->flash('success', 'Checklist template deleted.'); 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 public function resetChecklistTemplateForm(): void
{ {
$this->checklistTemplateForm = ['id' => null, 'category_id' => null, 'project_id' => null, 'name' => '', 'slug' => '', 'items_text' => '', 'is_active' => true]; $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 { 'boardOwners' => User::query()->where(static function ($query): void {
$query->where('helpdesk_agent', true)->orWhere('helpdesk_admin', true); $query->where('helpdesk_agent', true)->orWhere('helpdesk_admin', true);
})->orderBy('name')->get(['id', 'name', 'email']), })->orderBy('name')->get(['id', 'name', 'email']),
'boards' => KanbanBoard::query()->with(['statuses', 'owner:id,name,email'])->withCount('tickets')->orderBy('sort_order')->orderBy('name')->get(), 'boards' => KanbanBoard::query()
'repositories' => GitRepository::query()->with(['project'])->withCount(['commits', 'webhookLogs'])->orderBy('name')->get(), ->with(['statuses', 'owner:id,name,email'])
'workflowTemplates' => WorkflowTemplate::query()->orderBy('event')->orderBy('name')->get(), ->withCount('tickets')
'cannedResponses' => CannedResponse::query()->with(['category', 'team'])->orderBy('sort_order')->orderBy('name')->get(), ->orderBy('sort_order')
'slaPolicies' => SlaPolicy::query()->with(['priority', 'category'])->orderByDesc('is_default')->orderBy('name')->get(), ->orderBy('name')
'automationRules' => AutomationRule::query()->orderBy('sort_order')->orderBy('name')->get(), ->paginate(10, ['*'], 'boardsPage'),
'products' => Product::query()->withCount('tickets')->orderBy('name')->get(), 'repositories' => GitRepository::query()
'tags' => Tag::query()->orderBy('name')->get(), ->with(['project'])
'checklistTemplates' => ChecklistTemplate::query()->orderBy('name')->get(), ->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(), 'colourOptions' => Color::options(),
])->layout('layouts.app'); ])->layout('layouts.app');
} }
@@ -830,7 +1098,7 @@ final class SettingsManager extends Component
->when($this->hasDefaultColumn('helpdesk_statuses'), fn ($query) => $query->orderByDesc('is_default')) ->when($this->hasDefaultColumn('helpdesk_statuses'), fn ($query) => $query->orderByDesc('is_default'))
->orderBy('sort_order') ->orderBy('sort_order')
->orderBy('name') ->orderBy('name')
->get(); ->paginate(10, ['*'], 'statusesPage');
} }
private function orderedPriorities(): mixed private function orderedPriorities(): mixed
@@ -840,7 +1108,7 @@ final class SettingsManager extends Component
->when($this->hasDefaultColumn('helpdesk_priorities'), fn ($query) => $query->orderByDesc('is_default')) ->when($this->hasDefaultColumn('helpdesk_priorities'), fn ($query) => $query->orderByDesc('is_default'))
->orderBy('sort_order') ->orderBy('sort_order')
->orderBy('name') ->orderBy('name')
->get(); ->paginate(10, ['*'], 'prioritiesPage');
} }
private function orderedCategories(): mixed private function orderedCategories(): mixed
@@ -849,7 +1117,7 @@ final class SettingsManager extends Component
->withCount('tickets') ->withCount('tickets')
->when($this->hasDefaultColumn('helpdesk_categories'), fn ($query) => $query->orderByDesc('is_default')) ->when($this->hasDefaultColumn('helpdesk_categories'), fn ($query) => $query->orderByDesc('is_default'))
->orderBy('name') ->orderBy('name')
->get(); ->paginate(10, ['*'], 'categoriesPage');
} }
private function orderedTeams(): mixed private function orderedTeams(): mixed
@@ -859,7 +1127,7 @@ final class SettingsManager extends Component
->withCount(['projects', 'tickets', 'users']) ->withCount(['projects', 'tickets', 'users'])
->when($this->hasDefaultColumn('helpdesk_teams'), fn ($query) => $query->orderByDesc('is_default')) ->when($this->hasDefaultColumn('helpdesk_teams'), fn ($query) => $query->orderByDesc('is_default'))
->orderBy('name') ->orderBy('name')
->get(); ->paginate(10, ['*'], 'teamsPage');
} }
private function orderedProjects(): mixed private function orderedProjects(): mixed
@@ -869,7 +1137,7 @@ final class SettingsManager extends Component
->withCount('tickets') ->withCount('tickets')
->when($this->hasDefaultColumn('helpdesk_projects'), fn ($query) => $query->orderByDesc('is_default')) ->when($this->hasDefaultColumn('helpdesk_projects'), fn ($query) => $query->orderByDesc('is_default'))
->orderBy('title') ->orderBy('title')
->get(); ->paginate(10, ['*'], 'projectsPage');
} }
/** /**
@@ -982,3 +982,38 @@ span.flatpickr-weekday {
box-shadow: 0 0 0 1000px #09090b inset !important; box-shadow: 0 0 0 1000px #09090b inset !important;
transition: background-color 9999s ease-in-out 0s; 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 * 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. * hydration where the compiled Blade view may be newer than the loaded component.
*/ */
$statuses = collect($statuses ?? []); $statuses = $statuses ?? collect();
$priorities = collect($priorities ?? []); $priorities = $priorities ?? collect();
$categories = collect($categories ?? []); $categories = $categories ?? collect();
$teams = collect($teams ?? []); $teams = $teams ?? collect();
$projects = collect($projects ?? []); $projects = $projects ?? collect();
$boards = collect($boards ?? []); $boards = $boards ?? collect();
$repositories = collect($repositories ?? []); $repositories = $repositories ?? collect();
$workflowTemplates = collect($workflowTemplates ?? []); $workflowTemplates = $workflowTemplates ?? collect();
$cannedResponses = collect($cannedResponses ?? []); $cannedResponses = $cannedResponses ?? collect();
$slaPolicies = collect($slaPolicies ?? []); $slaPolicies = $slaPolicies ?? collect();
$automationRules = collect($automationRules ?? []); $automationRules = $automationRules ?? collect();
$checklistTemplates = collect($checklistTemplates ?? []); $checklistTemplates = $checklistTemplates ?? collect();
$tags = collect($tags ?? []); $tags = $tags ?? collect();
$products = collect($products ?? []); $products = $products ?? collect();
@endphp @endphp
<div class="hd-helpdesk-surface hd-mobile-ready hd-core-settings space-y-4" dusk="helpdesk-settings-manager"> <div class="hd-helpdesk-surface hd-mobile-ready hd-core-settings space-y-4" dusk="helpdesk-settings-manager">
@@ -43,20 +43,20 @@
</div> </div>
<div class="grid gap-2 md:grid-cols-4 xl:grid-cols-7"> <div class="grid gap-2 md:grid-cols-4 xl:grid-cols-7">
@foreach ([ @foreach ([
'statuses' => ['label' => 'Statuses', 'count' => $statuses->count()], 'statuses' => ['label' => 'Statuses', 'count' => method_exists($statuses, 'total') ? $statuses->total() : $statuses->count()],
'priorities' => ['label' => 'Priorities', 'count' => $priorities->count()], 'priorities' => ['label' => 'Priorities', 'count' => method_exists($priorities, 'total') ? $priorities->total() : $priorities->count()],
'categories' => ['label' => 'Categories', 'count' => $categories->count()], 'categories' => ['label' => 'Categories', 'count' => method_exists($categories, 'total') ? $categories->total() : $categories->count()],
'teams' => ['label' => 'Teams', 'count' => $teams->count()], 'teams' => ['label' => 'Teams', 'count' => method_exists($teams, 'total') ? $teams->total() : $teams->count()],
'projects' => ['label' => 'Projects', 'count' => $projects->count()], 'projects' => ['label' => 'Projects', 'count' => method_exists($projects, 'total') ? $projects->total() : $projects->count()],
'boards' => ['label' => 'Boards', 'count' => $boards->count()], 'boards' => ['label' => 'Boards', 'count' => method_exists($boards, 'total') ? $boards->total() : $boards->count()],
'git' => ['label' => 'Git', 'count' => $repositories->count()], 'git' => ['label' => 'Git', 'count' => method_exists($repositories, 'total') ? $repositories->total() : $repositories->count()],
'workflow' => ['label' => 'Workflow', 'count' => $workflowTemplates->count()], 'workflow' => ['label' => 'Workflow', 'count' => method_exists($workflowTemplates, 'total') ? $workflowTemplates->total() : $workflowTemplates->count()],
'canned' => ['label' => 'Canned', 'count' => $cannedResponses->count()], 'canned' => ['label' => 'Canned', 'count' => method_exists($cannedResponses, 'total') ? $cannedResponses->total() : $cannedResponses->count()],
'sla' => ['label' => 'SLA', 'count' => $slaPolicies->count()], 'sla' => ['label' => 'SLA', 'count' => method_exists($slaPolicies, 'total') ? $slaPolicies->total() : $slaPolicies->count()],
'automation' => ['label' => 'Rules', 'count' => $automationRules->count()], 'automation' => ['label' => 'Rules', 'count' => method_exists($automationRules, 'total') ? $automationRules->total() : $automationRules->count()],
'checklists' => ['label' => 'Checks', 'count' => $checklistTemplates->count()], 'checklists' => ['label' => 'Checks', 'count' => method_exists($checklistTemplates, 'total') ? $checklistTemplates->total() : $checklistTemplates->count()],
'tags' => ['label' => 'Tags', 'count' => $tags->count()], 'tags' => ['label' => 'Tags', 'count' => method_exists($tags, 'total') ? $tags->total() : $tags->count()],
'products' => ['label' => 'Products', 'count' => $products->count()], 'products' => ['label' => 'Products', 'count' => method_exists($products, 'total') ? $products->total() : $products->count()],
] as $key => $item) ] as $key => $item)
@php @php
$sectionPalette = match ($key) { $sectionPalette = match ($key) {
@@ -103,12 +103,34 @@
@if ($section === 'statuses') @if ($section === 'statuses')
<x-helpdesk.settings-section title="Statuses" <x-helpdesk.settings-section title="Statuses"
description="Control workflow stages and mark which statuses close tickets."> description="Control workflow stages and mark which statuses close tickets.">
<x-slot:form> <x-slot:actions>
<form wire:submit.prevent="saveStatus" class="grid gap-3 xl:grid-cols-12"> <flux:button
<div class="xl:col-span-3"> 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" /> <flux:input wire:model="statusForm.name" label="Name" />
</div> </div>
<div class="xl:col-span-3"> <div class="xl:col-span-4">
<flux:input wire:model="statusForm.slug" label="Slug" placeholder="auto-generated" /> <flux:input wire:model="statusForm.slug" label="Slug" placeholder="auto-generated" />
</div> </div>
<x-helpdesk.colour-select class="xl:col-span-12" model="statusForm.color" label="Badge colour" :options="$colourOptions" /> <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"> class="rounded border-zinc-300 bg-zinc-950 text-blue-600 focus:ring-blue-500">
Default status for new tickets Default status for new tickets
</label> </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"> <div class="flex flex-wrap items-center gap-2 xl:col-span-12">
<flux:button size="sm" type="submit" variant="primary" <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 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>
<flux:button size="sm" type="button" wire:click="resetStatusForm" variant="ghost" <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">Reset 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> </flux:button>
</div> </div>
</form> </form>
</x-slot:form> </flux:modal>
<x-helpdesk.lookup-table :items="$statuses" edit="editStatus" delete="deleteStatus" /> <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 @endif
@if ($section === 'priorities') @if ($section === 'priorities')
<x-helpdesk.settings-section title="Priorities" description="Define ticket urgency levels and display order."> <x-helpdesk.settings-section title="Priorities" description="Define ticket urgency levels and display order.">
<x-slot:form> <x-slot:actions>
<form wire:submit.prevent="savePriority" class="grid gap-3 xl:grid-cols-12"> <flux:button
<div class="xl:col-span-3"> 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" /> <flux:input wire:model="priorityForm.name" label="Name" />
</div> </div>
<div class="xl:col-span-3"> <div class="xl:col-span-4">
<flux:input wire:model="priorityForm.slug" label="Slug" placeholder="auto-generated" /> <flux:input wire:model="priorityForm.slug" label="Slug" placeholder="auto-generated" />
</div> </div>
<x-helpdesk.colour-select class="xl:col-span-12" model="priorityForm.color" label="Badge colour" :options="$colourOptions" /> <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" <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 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>
<flux:button size="sm" type="button" wire:click="resetPriorityForm" variant="ghost" <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">Reset 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> </flux:button>
</div> </div>
</form> </form>
</x-slot:form> </flux:modal>
<x-helpdesk.lookup-table :items="$priorities" edit="editPriority" delete="deletePriority" /> <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 @endif
@if ($section === 'categories') @if ($section === 'categories')
<x-helpdesk.settings-section title="Categories" <x-helpdesk.settings-section title="Categories"
description="Group tickets by support area."> description="Group tickets by support area.">
<x-slot:form> <x-slot:actions>
<form wire:submit.prevent="saveCategory" class="grid gap-3 xl:grid-cols-12"> <flux:button
<div class="xl:col-span-3"> size="sm"
</div> type="button"
<div class="xl:col-span-3"> 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" /> <flux:input wire:model="categoryForm.name" label="Name" />
</div> </div>
<div class="xl:col-span-3"> <div class="xl:col-span-4">
<flux:input wire:model="categoryForm.slug" label="Slug" placeholder="auto-generated" /> <flux:input wire:model="categoryForm.slug" label="Slug" placeholder="auto-generated" />
</div> </div>
<x-helpdesk.colour-select class="xl:col-span-12" model="categoryForm.color" label="Badge colour" :options="$colourOptions" /> <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" <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 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> category</flux:button>
<flux:button size="sm" type="button" wire:click="resetCategoryForm" variant="ghost" <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">Reset 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> </flux:button>
</div> </div>
</form> </form>
</x-slot:form> </flux:modal>
<x-helpdesk.lookup-table :items="$categories" edit="editCategory" delete="deleteCategory" /> <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 @endif
@if ($section === 'teams') @if ($section === 'teams')
@@ -241,7 +296,7 @@
<flux:modal <flux:modal
name="helpdesk-team-editor" name="helpdesk-team-editor"
wire:model="teamModalOpen" wire:model="teamModalOpen"
class="w-full max-w-5xl" class="hd-settings-modal w-full max-w-5xl"
@close="closeTeamModal" @close="closeTeamModal"
> >
<form wire:submit="saveTeam" class="space-y-5"> <form wire:submit="saveTeam" class="space-y-5">
@@ -307,19 +362,6 @@
Default team for new tickets Default team for new tickets
</label> </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"> <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"> <flux:button type="button" variant="ghost" wire:click="closeTeamModal">
Cancel Cancel
@@ -331,14 +373,37 @@
</div> </div>
</form> </form>
</flux:modal> </flux:modal>
</x-helpdesk.settings-section> <x-helpdesk.settings-pagination :paginator="$teams" />
</x-helpdesk.settings-section>
@endif @endif
@if ($section === 'projects') @if ($section === 'projects')
<x-helpdesk.settings-section title="Projects" <x-helpdesk.settings-section title="Projects"
description="Create projects and optionally assign them to support teams."> description="Create projects and optionally assign them to support teams.">
<x-slot:form> <x-slot:actions>
<form wire:submit.prevent="saveProject" class="grid gap-3 xl:grid-cols-12"> <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 class="xl:col-span-2">
</div> </div>
<div class="xl:col-span-2"> <div class="xl:col-span-2">
@@ -368,22 +433,45 @@
<flux:button size="sm" type="submit" variant="primary" <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 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>
<flux:button size="sm" type="button" wire:click="resetProjectForm" variant="ghost" <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">Reset 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> </flux:button>
</div> </div>
</form> </form>
</x-slot:form> </flux:modal>
<x-helpdesk.project-table :items="$projects" /> <x-helpdesk.project-table :items="$projects" />
</x-helpdesk.settings-section> <x-helpdesk.settings-pagination :paginator="$projects" />
</x-helpdesk.settings-section>
@endif @endif
@if ($section === 'boards') @if ($section === 'boards')
<x-helpdesk.settings-section title="Kanban Boards" <x-helpdesk.settings-section title="Kanban Boards"
description="Create configurable workflow boards and choose which statuses appear as draggable columns."> description="Create configurable workflow boards and choose which statuses appear as draggable columns.">
<x-slot:form> <x-slot:actions>
<form wire:submit.prevent="saveBoard" class="grid gap-3 md:grid-cols-7"> <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.name" label="Name" />
<flux:input wire:model="boardForm.slug" label="Slug" placeholder="auto-generated" /> <flux:input wire:model="boardForm.slug" label="Slug" placeholder="auto-generated" />
<flux:input wire:model="boardForm.sort_order" label="Sort" type="number" min="0" /> <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" <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 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>
<flux:button size="sm" type="button" wire:click="resetBoardForm" variant="ghost" <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">Reset 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> </flux:button>
</div> </div>
</form> </form>
</x-slot:form> </flux:modal>
<div <div
class="overflow-hidden rounded-2xl border border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900"> class="overflow-hidden rounded-2xl border border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900">
@@ -485,14 +573,37 @@
</tbody> </tbody>
</table> </table>
</div> </div>
</x-helpdesk.settings-section> <x-helpdesk.settings-pagination :paginator="$boards" />
</x-helpdesk.settings-section>
@endif @endif
@if ($section === 'git') @if ($section === 'git')
<x-helpdesk.settings-section title="Git repositories" <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."> description="Configure repositories that can be linked to projects and tickets. Commits can then be stored against tickets for traceability.">
<x-slot:form> <x-slot:actions>
<form wire:submit.prevent="saveRepository" class="grid gap-3 md:grid-cols-6"> <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"> <flux:select wire:model="repositoryForm.project_id" label="Project">
<option value="">No project</option> <option value="">No project</option>
@foreach ($projects as $project) @foreach ($projects as $project)
@@ -512,22 +623,45 @@
<flux:button size="sm" type="submit" variant="primary" <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 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> repository</flux:button>
<flux:button size="sm" type="button" wire:click="resetRepositoryForm" variant="ghost" <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">Reset 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> </flux:button>
</div> </div>
</form> </form>
</x-slot:form> </flux:modal>
<x-helpdesk.repository-table :items="$repositories" /> <x-helpdesk.repository-table :items="$repositories" />
</x-helpdesk.settings-section> <x-helpdesk.settings-pagination :paginator="$repositories" />
</x-helpdesk.settings-section>
@endif @endif
@if ($section === 'workflow') @if ($section === 'workflow')
<x-helpdesk.settings-section title="Workflow messages" <x-helpdesk.settings-section title="Workflow messages"
description="Define the professional email messages sent when tickets are raised, updated, replied to and closed."> description="Define the professional email messages sent when tickets are raised, updated, replied to and closed.">
<x-slot:form> <x-slot:actions>
<form wire:submit.prevent="saveWorkflowTemplate" class="grid gap-3 md:grid-cols-6"> <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:input wire:model="workflowForm.name" label="Name" />
<flux:select wire:model="workflowForm.event" label="Event"> <flux:select wire:model="workflowForm.event" label="Event">
<option value="ticket.created">Ticket raised</option> <option value="ticket.created">Ticket raised</option>
@@ -564,12 +698,12 @@
<flux:button size="sm" type="submit" variant="primary" <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 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> workflow message</flux:button>
<flux:button size="sm" type="button" wire:click="resetWorkflowTemplateForm" variant="ghost" <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">Reset 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> </flux:button>
</div> </div>
</form> </form>
</x-slot:form> </flux:modal>
<div <div
class="overflow-hidden rounded-2xl border border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900"> class="overflow-hidden rounded-2xl border border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900">
@@ -616,15 +750,38 @@
</tbody> </tbody>
</table> </table>
</div> </div>
</x-helpdesk.settings-section> <x-helpdesk.settings-pagination :paginator="$workflowTemplates" />
</x-helpdesk.settings-section>
@endif @endif
@if ($section === 'canned') @if ($section === 'canned')
<x-helpdesk.settings-section title="Canned responses" <x-helpdesk.settings-section title="Canned responses"
description="Save professional reusable replies with macro tokens for ticket, requester, agent, status and URL values."> description="Save professional reusable replies with macro tokens for ticket, requester, agent, status and URL values.">
<x-slot:form> <x-slot:actions>
<form wire:submit.prevent="saveCannedResponse" class="grid gap-3 md:grid-cols-6"> <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.name" label="Name" />
<flux:input wire:model="cannedForm.slug" label="Slug" placeholder="auto-generated" /> <flux:input wire:model="cannedForm.slug" label="Slug" placeholder="auto-generated" />
<flux:input wire:model="cannedForm.sort_order" label="Sort" type="number" min="0" /> <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" <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 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> response</flux:button>
<flux:button size="sm" type="button" wire:click="resetCannedResponseForm" variant="ghost" <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">Reset 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> </flux:button>
</div> </div>
</form> </form>
</x-slot:form> </flux:modal>
<div class="grid gap-3 md:grid-cols-2 xl:grid-cols-3"> <div class="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
@forelse ($cannedResponses as $response) @forelse ($cannedResponses as $response)
<div class="rounded-2xl border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900"> <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> No canned responses yet.</div>
@endforelse @endforelse
</div> </div>
</x-helpdesk.settings-section> <x-helpdesk.settings-pagination :paginator="$cannedResponses" />
</x-helpdesk.settings-section>
@endif @endif
@if ($section === 'sla') @if ($section === 'sla')
<x-helpdesk.settings-section title="SLA policies" <x-helpdesk.settings-section title="SLA policies"
description="Define first response and resolution targets used for breach indicators, reporting and ticket cards."> description="Define first response and resolution targets used for breach indicators, reporting and ticket cards.">
<x-slot:form> <x-slot:actions>
<form wire:submit.prevent="saveSlaPolicy" class="grid gap-3 md:grid-cols-6"> <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.name" label="Name" />
<flux:input wire:model="slaForm.slug" label="Slug" placeholder="auto-generated" /> <flux:input wire:model="slaForm.slug" label="Slug" placeholder="auto-generated" />
<flux:select wire:model="slaForm.priority_id" label="Priority"> <flux:select wire:model="slaForm.priority_id" label="Priority">
@@ -731,12 +911,12 @@
<flux:button size="sm" type="submit" variant="primary" <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 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> policy</flux:button>
<flux:button size="sm" type="button" wire:click="resetSlaPolicyForm" variant="ghost" <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">Reset 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> </flux:button>
</div> </div>
</form> </form>
</x-slot:form> </flux:modal>
<div <div
class="overflow-hidden rounded-2xl border border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900"> 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"> <table class="min-w-full divide-y divide-zinc-200 text-sm dark:divide-zinc-700">
@@ -778,14 +958,37 @@
</tbody> </tbody>
</table> </table>
</div> </div>
</x-helpdesk.settings-section> <x-helpdesk.settings-pagination :paginator="$slaPolicies" />
</x-helpdesk.settings-section>
@endif @endif
@if ($section === 'automation') @if ($section === 'automation')
<x-helpdesk.settings-section title="Email automation rules" <x-helpdesk.settings-section title="Email automation rules"
description="Match incoming support messages and automatically assign priority, category, team, project, agent and status."> description="Match incoming support messages and automatically assign priority, category, team, project, agent and status.">
<x-slot:form> <x-slot:actions>
<form wire:submit.prevent="saveAutomationRule" class="grid gap-3 md:grid-cols-6"> <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.name" label="Name" />
<flux:input wire:model="automationForm.slug" label="Slug" placeholder="auto-generated" /> <flux:input wire:model="automationForm.slug" label="Slug" placeholder="auto-generated" />
<flux:input wire:model="automationForm.sort_order" label="Sort" type="number" <flux:input wire:model="automationForm.sort_order" label="Sort" type="number"
@@ -830,12 +1033,12 @@
<flux:button size="sm" type="submit" variant="primary" <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 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> automation rule</flux:button>
<flux:button size="sm" type="button" wire:click="resetAutomationRuleForm" variant="ghost" <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">Reset 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> </flux:button>
</div> </div>
</form> </form>
</x-slot:form> </flux:modal>
<div class="grid gap-3 md:grid-cols-2"> <div class="grid gap-3 md:grid-cols-2">
@forelse ($automationRules as $rule) @forelse ($automationRules as $rule)
<div class="rounded-2xl border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900"> <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> No automation rules yet.</div>
@endforelse @endforelse
</div> </div>
</x-helpdesk.settings-section> <x-helpdesk.settings-pagination :paginator="$automationRules" />
</x-helpdesk.settings-section>
@endif @endif
@if ($section === 'checklists') @if ($section === 'checklists')
<x-helpdesk.settings-section title="Checklist templates" <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."> description="Apply repeatable checklist items by category or project and show completion progress on tickets and Kanban cards.">
<x-slot:form> <x-slot:actions>
<form wire:submit.prevent="saveChecklistTemplate" class="grid gap-3 md:grid-cols-6"> <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.name" label="Name" />
<flux:input wire:model="checklistTemplateForm.slug" label="Slug" /> <flux:input wire:model="checklistTemplateForm.slug" label="Slug" />
<flux:select wire:model="checklistTemplateForm.category_id" label="Category"> <flux:select wire:model="checklistTemplateForm.category_id" label="Category">
@@ -891,12 +1117,12 @@
<flux:button size="sm" type="submit" variant="primary" <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 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> checklist</flux:button>
<flux:button size="sm" type="button" wire:click="resetChecklistTemplateForm" variant="ghost" <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">Reset 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> </flux:button>
</div> </div>
</form> </form>
</x-slot:form> </flux:modal>
<div class="grid gap-3 md:grid-cols-2 xl:grid-cols-3"> <div class="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
@forelse ($checklistTemplates as $template) @forelse ($checklistTemplates as $template)
<div class="rounded-2xl border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900"> <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> No checklist templates yet.</div>
@endforelse @endforelse
</div> </div>
</x-helpdesk.settings-section> <x-helpdesk.settings-pagination :paginator="$checklistTemplates" />
</x-helpdesk.settings-section>
@endif @endif
@if ($section === 'tags') @if ($section === 'tags')
<x-helpdesk.settings-section title="Ticket tags" <x-helpdesk.settings-section title="Ticket tags"
description="Maintain visible ticket labels for search, triage and Kanban card context."> description="Maintain visible ticket labels for search, triage and Kanban card context.">
<x-slot:form> <x-slot:actions>
<form wire:submit.prevent="saveTag" class="grid gap-3 xl:grid-cols-12"> <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"> <div class="xl:col-span-4">
<flux:input wire:model="tagForm.name" label="Name" /> <flux:input wire:model="tagForm.name" label="Name" />
</div> </div>
@@ -937,12 +1186,12 @@
<flux:button size="sm" type="submit" variant="primary" <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 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>
<flux:button size="sm" type="button" wire:click="resetTagForm" variant="ghost" <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">Reset 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> </flux:button>
</div> </div>
</form> </form>
</x-slot:form> </flux:modal>
<div class="flex flex-wrap gap-3"> <div class="flex flex-wrap gap-3">
@forelse ($tags as $tag) @forelse ($tags as $tag)
<div <div
@@ -963,14 +1212,37 @@
No tags yet.</div> No tags yet.</div>
@endforelse @endforelse
</div> </div>
</x-helpdesk.settings-section> <x-helpdesk.settings-pagination :paginator="$tags" />
</x-helpdesk.settings-section>
@endif @endif
@if ($section === 'products') @if ($section === 'products')
<x-helpdesk.settings-section title="Products and services" <x-helpdesk.settings-section title="Products and services"
description="Link tickets to products, services, releases or builds and report support cost/time by product."> description="Link tickets to products, services, releases or builds and report support cost/time by product.">
<x-slot:form> <x-slot:actions>
<form wire:submit.prevent="saveProduct" class="grid gap-3 md:grid-cols-6"> <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.name" label="Name" />
<flux:input wire:model="productForm.slug" label="Slug" /> <flux:input wire:model="productForm.slug" label="Slug" />
<flux:input wire:model="productForm.type" label="Type" /> <flux:input wire:model="productForm.type" label="Type" />
@@ -985,12 +1257,12 @@
<flux:button size="sm" type="submit" variant="primary" <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 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> product/service</flux:button>
<flux:button size="sm" type="button" wire:click="resetProductForm" variant="ghost" <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">Reset 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> </flux:button>
</div> </div>
</form> </form>
</x-slot:form> </flux:modal>
<div class="grid gap-3 md:grid-cols-2 xl:grid-cols-3"> <div class="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
@forelse ($products as $product) @forelse ($products as $product)
<div class="rounded-2xl border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900"> <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> No products or services yet.</div>
@endforelse @endforelse
</div> </div>
</x-helpdesk.settings-section> <x-helpdesk.settings-pagination :paginator="$products" />
</x-helpdesk.settings-section>
@endif @endif
</div> </div>