major helpdesk updates

This commit is contained in:
Mark Gregory
2026-06-15 13:47:58 +01:00
parent 330d6922a4
commit b668116b11
47 changed files with 2350 additions and 23 deletions
+277 -1
View File
@@ -9,12 +9,18 @@ use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
use Livewire\Component;
use Modules\Helpdesk\Models\AutomationRule;
use Modules\Helpdesk\Models\CannedResponse;
use Modules\Helpdesk\Models\ChecklistTemplate;
use Modules\Helpdesk\Models\Category;
use Modules\Helpdesk\Models\GitRepository;
use Modules\Helpdesk\Models\KanbanBoard;
use Modules\Helpdesk\Models\Priority;
use Modules\Helpdesk\Models\Product;
use Modules\Helpdesk\Models\Project;
use Modules\Helpdesk\Models\Status;
use Modules\Helpdesk\Models\SlaPolicy;
use Modules\Helpdesk\Models\Tag;
use Modules\Helpdesk\Models\Team;
use Modules\Helpdesk\Models\WorkflowTemplate;
@@ -43,9 +49,45 @@ final class SettingsManager extends Component
/** @var array<string, mixed> */
public array $repositoryForm = ['id' => null, 'company_id' => null, 'project_id' => null, 'name' => '', 'provider' => 'git', 'remote_url' => '', 'default_branch' => 'main', 'webhook_secret' => ''];
/** @var array<string, mixed> */
public array $cannedForm = ['id' => null, 'company_id' => null, 'category_id' => null, 'team_id' => null, 'name' => '', 'slug' => '', 'visibility' => 'agents', 'subject' => '', 'body' => '', 'is_active' => true, 'sort_order' => 0];
/** @var array<string, mixed> */
public array $slaForm = ['id' => null, 'company_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];
/** @var array<string, mixed> */
public array $automationForm = ['id' => null, 'company_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];
/** @var array<string, mixed> */
public array $productForm = ['id' => null, 'company_id' => null, 'name' => '', 'slug' => '', 'type' => 'service', 'version' => '', 'release_channel' => '', 'description' => '', 'is_active' => true];
/** @var array<string, mixed> */
public array $tagForm = ['id' => null, 'company_id' => null, 'name' => '', 'slug' => '', 'color' => 'zinc'];
/** @var array<string, mixed> */
public array $checklistTemplateForm = ['id' => null, 'company_id' => null, 'category_id' => null, 'project_id' => null, 'name' => '', 'slug' => '', 'items_text' => '', 'is_active' => true];
/** @var array<string, mixed> */
public array $workflowForm = ['id' => null, 'name' => '', 'event' => 'ticket.created', 'subject' => '', 'html_body' => '', 'text_body' => '', 'to_role' => 'requester', 'cc' => '', 'bcc' => '', 'is_active' => true];
public function mount(): void
{
$this->statusForm += ['id' => null, 'name' => '', 'slug' => '', 'color' => 'zinc', 'sort_order' => 0, 'is_closed' => false];
$this->priorityForm += ['id' => null, 'name' => '', 'slug' => '', 'color' => 'zinc', 'sort_order' => 0];
$this->categoryForm += ['id' => null, 'company_id' => null, 'name' => '', 'slug' => '', 'color' => 'blue', 'description' => ''];
$this->teamForm += ['id' => null, 'company_id' => null, 'name' => '', 'slug' => '', 'color' => 'indigo', 'description' => '', 'archived_at' => null];
$this->projectForm += ['id' => null, 'company_id' => null, 'team_id' => null, 'title' => '', 'slug' => '', 'color' => 'blue', 'description' => '', 'archived_at' => null];
$this->boardForm += ['id' => null, 'name' => '', 'slug' => '', 'description' => '', 'is_default' => false, 'sort_order' => 0, 'status_ids' => []];
$this->repositoryForm += ['id' => null, 'company_id' => null, 'project_id' => null, 'name' => '', 'provider' => 'git', 'remote_url' => '', 'default_branch' => 'main', 'webhook_secret' => ''];
$this->workflowForm += ['id' => null, 'name' => '', 'event' => 'ticket.created', 'subject' => '', 'html_body' => '', 'text_body' => '', 'to_role' => 'requester', 'cc' => '', 'bcc' => '', 'is_active' => true];
$this->cannedForm += ['id' => null, 'company_id' => null, 'category_id' => null, 'team_id' => null, 'name' => '', 'slug' => '', 'visibility' => 'agents', 'subject' => '', 'body' => '', 'is_active' => true, 'sort_order' => 0];
$this->slaForm += ['id' => null, 'company_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->automationForm += ['id' => null, 'company_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->productForm += ['id' => null, 'company_id' => null, 'name' => '', 'slug' => '', 'type' => 'service', 'version' => '', 'release_channel' => '', 'description' => '', 'is_active' => true];
$this->tagForm += ['id' => null, 'company_id' => null, 'name' => '', 'slug' => '', 'color' => 'zinc'];
$this->checklistTemplateForm += ['id' => null, 'company_id' => null, 'category_id' => null, 'project_id' => null, 'name' => '', 'slug' => '', 'items_text' => '', 'is_active' => true];
}
public function saveStatus(): void
{
$data = $this->validate([
@@ -286,6 +328,8 @@ final class SettingsManager extends Component
'repositoryForm.webhook_secret' => ['nullable', 'string', 'max:120'],
])['repositoryForm'];
$data['provider'] = strtolower(trim((string) $data['provider']));
GitRepository::query()->updateOrCreate(['id' => $this->repositoryForm['id']], $data);
$this->resetRepositoryForm();
session()->flash('success', 'Git repository saved.');
@@ -344,6 +388,232 @@ final class SettingsManager extends Component
$this->workflowForm = ['id' => null, 'name' => '', 'event' => 'ticket.created', 'subject' => '', 'html_body' => '', 'text_body' => '', 'to_role' => 'requester', 'cc' => '', 'bcc' => '', 'is_active' => true];
}
public function saveCannedResponse(): void
{
$data = $this->validate([
'cannedForm.company_id' => ['nullable', 'integer'],
'cannedForm.category_id' => ['nullable', 'integer', 'exists:helpdesk_categories,id'],
'cannedForm.team_id' => ['nullable', 'integer', 'exists:helpdesk_teams,id'],
'cannedForm.name' => ['required', 'string', 'max:160'],
'cannedForm.slug' => ['nullable', 'string', 'max:180'],
'cannedForm.visibility' => ['required', 'string', 'max:40'],
'cannedForm.subject' => ['nullable', 'string', 'max:220'],
'cannedForm.body' => ['required', 'string'],
'cannedForm.is_active' => ['boolean'],
'cannedForm.sort_order' => ['required', 'integer', 'min:0'],
])['cannedForm'];
$data['slug'] = $this->slug($data['slug'] ?: $data['name']);
CannedResponse::query()->updateOrCreate(['id' => $this->cannedForm['id']], $data);
$this->resetCannedResponseForm();
session()->flash('success', 'Canned response saved.');
}
public function editCannedResponse(int $id): void
{
$this->cannedForm = CannedResponse::query()->findOrFail($id)->only(['id','company_id','category_id','team_id','name','slug','visibility','subject','body','is_active','sort_order']);
$this->section = 'canned';
}
public function deleteCannedResponse(int $id): void
{
CannedResponse::query()->findOrFail($id)->delete();
session()->flash('success', 'Canned response deleted.');
}
public function resetCannedResponseForm(): void
{
$this->cannedForm = ['id' => null, 'company_id' => null, 'category_id' => null, 'team_id' => null, 'name' => '', 'slug' => '', 'visibility' => 'agents', 'subject' => '', 'body' => '', 'is_active' => true, 'sort_order' => 0];
}
public function saveSlaPolicy(): void
{
$data = $this->validate([
'slaForm.company_id' => ['nullable', 'integer'],
'slaForm.priority_id' => ['nullable', 'integer', 'exists:helpdesk_priorities,id'],
'slaForm.category_id' => ['nullable', 'integer', 'exists:helpdesk_categories,id'],
'slaForm.name' => ['required', 'string', 'max:160'],
'slaForm.slug' => ['nullable', 'string', 'max:180'],
'slaForm.first_response_minutes' => ['required', 'integer', 'min:1'],
'slaForm.resolution_minutes' => ['required', 'integer', 'min:1'],
'slaForm.business_hours_only' => ['boolean'],
'slaForm.is_default' => ['boolean'],
'slaForm.is_active' => ['boolean'],
])['slaForm'];
$data['slug'] = $this->slug($data['slug'] ?: $data['name']);
if ((bool) ($data['is_default'] ?? false)) {
SlaPolicy::query()->whereKeyNot($this->slaForm['id'])->update(['is_default' => false]);
}
SlaPolicy::query()->updateOrCreate(['id' => $this->slaForm['id']], $data);
$this->resetSlaPolicyForm();
session()->flash('success', 'SLA policy saved.');
}
public function editSlaPolicy(int $id): void
{
$this->slaForm = SlaPolicy::query()->findOrFail($id)->only(['id','company_id','priority_id','category_id','name','slug','first_response_minutes','resolution_minutes','business_hours_only','is_default','is_active']);
$this->section = 'sla';
}
public function deleteSlaPolicy(int $id): void
{
SlaPolicy::query()->findOrFail($id)->delete();
session()->flash('success', 'SLA policy deleted.');
}
public function resetSlaPolicyForm(): void
{
$this->slaForm = ['id' => null, 'company_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];
}
public function saveAutomationRule(): void
{
$data = $this->validate([
'automationForm.company_id' => ['nullable', 'integer'],
'automationForm.name' => ['required', 'string', 'max:160'],
'automationForm.slug' => ['nullable', 'string', 'max:180'],
'automationForm.match_sender' => ['nullable', 'string', 'max:190'],
'automationForm.match_subject' => ['nullable', 'string', 'max:220'],
'automationForm.match_mailbox' => ['nullable', 'string', 'max:190'],
'automationForm.match_category_id' => ['nullable', 'integer', 'exists:helpdesk_categories,id'],
'automationForm.set_priority_id' => ['nullable', 'integer', 'exists:helpdesk_priorities,id'],
'automationForm.set_category_id' => ['nullable', 'integer', 'exists:helpdesk_categories,id'],
'automationForm.set_team_id' => ['nullable', 'integer', 'exists:helpdesk_teams,id'],
'automationForm.set_project_id' => ['nullable', 'integer', 'exists:helpdesk_projects,id'],
'automationForm.set_status_id' => ['nullable', 'integer', 'exists:helpdesk_statuses,id'],
'automationForm.set_agent_id' => ['nullable', 'integer', 'exists:users,id'],
'automationForm.is_active' => ['boolean'],
'automationForm.sort_order' => ['required', 'integer', 'min:0'],
])['automationForm'];
$data['slug'] = $this->slug($data['slug'] ?: $data['name']);
AutomationRule::query()->updateOrCreate(['id' => $this->automationForm['id']], $data);
$this->resetAutomationRuleForm();
session()->flash('success', 'Automation rule saved.');
}
public function editAutomationRule(int $id): void
{
$this->automationForm = AutomationRule::query()->findOrFail($id)->only(['id','company_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';
}
public function deleteAutomationRule(int $id): void
{
AutomationRule::query()->findOrFail($id)->delete();
session()->flash('success', 'Automation rule deleted.');
}
public function resetAutomationRuleForm(): void
{
$this->automationForm = ['id' => null, 'company_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];
}
public function saveProduct(): void
{
$data = $this->validate([
'productForm.company_id' => ['nullable', 'integer'],
'productForm.name' => ['required', 'string', 'max:160'],
'productForm.slug' => ['nullable', 'string', 'max:180'],
'productForm.type' => ['required', 'string', 'max:60'],
'productForm.version' => ['nullable', 'string', 'max:80'],
'productForm.release_channel' => ['nullable', 'string', 'max:80'],
'productForm.description' => ['nullable', 'string'],
'productForm.is_active' => ['boolean'],
])['productForm'];
$data['slug'] = $this->slug($data['slug'] ?: $data['name']);
Product::query()->updateOrCreate(['id' => $this->productForm['id']], $data);
$this->resetProductForm();
session()->flash('success', 'Product/service saved.');
}
public function editProduct(int $id): void
{
$this->productForm = Product::query()->findOrFail($id)->only(['id','company_id','name','slug','type','version','release_channel','description','is_active']);
$this->section = 'products';
}
public function deleteProduct(int $id): void
{
Product::query()->findOrFail($id)->delete();
session()->flash('success', 'Product/service deleted.');
}
public function resetProductForm(): void
{
$this->productForm = ['id' => null, 'company_id' => null, 'name' => '', 'slug' => '', 'type' => 'service', 'version' => '', 'release_channel' => '', 'description' => '', 'is_active' => true];
}
public function saveTag(): void
{
$data = $this->validate([
'tagForm.company_id' => ['nullable', 'integer'],
'tagForm.name' => ['required', 'string', 'max:80'],
'tagForm.slug' => ['nullable', 'string', 'max:100'],
'tagForm.color' => ['required', 'string', 'max:30'],
])['tagForm'];
$data['slug'] = $this->slug($data['slug'] ?: $data['name']);
Tag::query()->updateOrCreate(['id' => $this->tagForm['id']], $data);
$this->resetTagForm();
session()->flash('success', 'Tag saved.');
}
public function editTag(int $id): void
{
$this->tagForm = Tag::query()->findOrFail($id)->only(['id','company_id','name','slug','color']);
$this->section = 'tags';
}
public function deleteTag(int $id): void
{
Tag::query()->findOrFail($id)->delete();
session()->flash('success', 'Tag deleted.');
}
public function resetTagForm(): void
{
$this->tagForm = ['id' => null, 'company_id' => null, 'name' => '', 'slug' => '', 'color' => 'zinc'];
}
public function saveChecklistTemplate(): void
{
$data = $this->validate([
'checklistTemplateForm.company_id' => ['nullable', 'integer'],
'checklistTemplateForm.category_id' => ['nullable', 'integer', 'exists:helpdesk_categories,id'],
'checklistTemplateForm.project_id' => ['nullable', 'integer', 'exists:helpdesk_projects,id'],
'checklistTemplateForm.name' => ['required', 'string', 'max:160'],
'checklistTemplateForm.slug' => ['nullable', 'string', 'max:180'],
'checklistTemplateForm.items_text' => ['required', 'string'],
'checklistTemplateForm.is_active' => ['boolean'],
])['checklistTemplateForm'];
$itemsText = (string) ($data['items_text'] ?? '');
unset($data['items_text']);
$data['slug'] = $this->slug($data['slug'] ?: $data['name']);
$data['items'] = collect(preg_split('/\r\n|\r|\n/', $itemsText) ?: [])->map(fn ($line): string => trim($line))->filter()->values()->all();
ChecklistTemplate::query()->updateOrCreate(['id' => $this->checklistTemplateForm['id']], $data);
$this->resetChecklistTemplateForm();
session()->flash('success', 'Checklist template saved.');
}
public function editChecklistTemplate(int $id): void
{
$template = ChecklistTemplate::query()->findOrFail($id);
$this->checklistTemplateForm = $template->only(['id','company_id','category_id','project_id','name','slug','is_active']) + [
'items_text' => collect($template->items ?? [])->map(fn ($item): string => is_array($item) ? (string) ($item['title'] ?? '') : (string) $item)->implode("\n"),
];
$this->section = 'checklists';
}
public function deleteChecklistTemplate(int $id): void
{
ChecklistTemplate::query()->findOrFail($id)->delete();
session()->flash('success', 'Checklist template deleted.');
}
public function resetChecklistTemplateForm(): void
{
$this->checklistTemplateForm = ['id' => null, 'company_id' => null, 'category_id' => null, 'project_id' => null, 'name' => '', 'slug' => '', 'items_text' => '', 'is_active' => true];
}
public function render(): View
{
return view('helpdesk::livewire.settings-manager', [
@@ -354,8 +624,14 @@ final class SettingsManager extends Component
'teams' => Team::query()->withCount(['projects', 'tickets'])->orderBy('name')->get(),
'projects' => Project::query()->with(['team'])->withCount('tickets')->orderBy('title')->get(),
'boards' => KanbanBoard::query()->with('statuses')->orderBy('sort_order')->orderBy('name')->get(),
'repositories' => GitRepository::query()->with(['project'])->withCount('commits')->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(),
])->layout('layouts.app');
}