1268 lines
51 KiB
PHP
Executable File
1268 lines
51 KiB
PHP
Executable File
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Modules\Helpdesk\Livewire;
|
|
|
|
use App\Models\User;
|
|
use Illuminate\Contracts\View\View;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\QueryException;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\Validation\ValidationException;
|
|
use Livewire\Attributes\Layout;
|
|
use Livewire\Component;
|
|
use Livewire\WithPagination;
|
|
use Modules\Helpdesk\Models\AutomationRule;
|
|
use Modules\Helpdesk\Models\CannedResponse;
|
|
use Modules\Helpdesk\Models\Category;
|
|
use Modules\Helpdesk\Models\ChecklistTemplate;
|
|
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\SlaPolicy;
|
|
use Modules\Helpdesk\Models\Status;
|
|
use Modules\Helpdesk\Models\Tag;
|
|
use Modules\Helpdesk\Models\Team;
|
|
use Modules\Helpdesk\Models\WorkflowTemplate;
|
|
use Modules\Helpdesk\Support\Color;
|
|
|
|
final class SettingsManager extends Component
|
|
{
|
|
use WithPagination;
|
|
|
|
public string $section = 'statuses';
|
|
|
|
public bool $teamModalOpen = false;
|
|
|
|
public bool $statusModalOpen = false;
|
|
|
|
public bool $priorityModalOpen = false;
|
|
|
|
public bool $categoryModalOpen = false;
|
|
|
|
public bool $projectModalOpen = false;
|
|
|
|
public bool $boardModalOpen = false;
|
|
|
|
public bool $repositoryModalOpen = false;
|
|
|
|
public bool $workflowModalOpen = false;
|
|
|
|
public bool $cannedModalOpen = false;
|
|
|
|
public bool $slaModalOpen = false;
|
|
|
|
public bool $automationModalOpen = false;
|
|
|
|
public bool $checklistModalOpen = false;
|
|
|
|
public bool $tagModalOpen = false;
|
|
|
|
public bool $productModalOpen = false;
|
|
|
|
/** @var array<string, mixed> */
|
|
public array $statusForm = ['id' => null, 'name' => '', 'slug' => '', 'color' => 'gray', 'sort_order' => 0, 'is_closed' => false, 'is_default' => false];
|
|
|
|
/** @var array<string, mixed> */
|
|
public array $priorityForm = ['id' => null, 'name' => '', 'slug' => '', 'color' => 'gray', 'sort_order' => 0, 'is_default' => false];
|
|
|
|
/** @var array<string, mixed> */
|
|
public array $categoryForm = ['id' => null, 'name' => '', 'slug' => '', 'color' => 'blue', 'description' => '', 'is_default' => false];
|
|
|
|
/** @var array<string, mixed> */
|
|
public array $teamForm = ['id' => null, 'name' => '', 'slug' => '', 'color' => 'indigo', 'description' => '', 'is_default' => false, 'archived_at' => null, 'user_ids' => []];
|
|
|
|
/** @var array<string, mixed> */
|
|
public array $projectForm = ['id' => null, 'team_id' => null, 'title' => '', 'slug' => '', 'color' => 'blue', 'description' => '', 'is_default' => false, 'archived_at' => null];
|
|
|
|
/** @var array<string, mixed> */
|
|
public array $boardForm = ['id' => null, 'name' => '', 'slug' => '', 'description' => '', 'owner_id' => null, 'is_default' => false, 'sort_order' => 0, 'status_ids' => []];
|
|
|
|
/** @var array<string, mixed> */
|
|
public array $repositoryForm = ['id' => null, 'project_id' => null, 'name' => '', 'provider' => 'git', 'remote_url' => '', 'default_branch' => 'main', 'webhook_secret' => ''];
|
|
|
|
/** @var array<string, mixed> */
|
|
public array $cannedForm = ['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, '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, '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, 'name' => '', 'slug' => '', 'type' => 'service', 'version' => '', 'release_channel' => '', 'description' => '', 'is_active' => true];
|
|
|
|
/** @var array<string, mixed> */
|
|
public array $tagForm = ['id' => null, 'name' => '', 'slug' => '', 'color' => 'gray'];
|
|
|
|
/** @var array<string, mixed> */
|
|
public array $checklistTemplateForm = ['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->section = $this->validSection((string) request()->query('section', $this->section));
|
|
$this->statusForm += ['id' => null, 'name' => '', 'slug' => '', 'color' => 'gray', 'sort_order' => 0, 'is_closed' => false, 'is_default' => false];
|
|
$this->priorityForm += ['id' => null, 'name' => '', 'slug' => '', 'color' => 'gray', 'sort_order' => 0, 'is_default' => false];
|
|
$this->categoryForm += ['id' => null, 'name' => '', 'slug' => '', 'color' => 'blue', 'description' => '', 'is_default' => false];
|
|
$this->teamForm += ['id' => null, 'name' => '', 'slug' => '', 'color' => 'indigo', 'description' => '', 'is_default' => false, 'archived_at' => null, 'user_ids' => []];
|
|
$this->projectForm += ['id' => null, 'team_id' => null, 'title' => '', 'slug' => '', 'color' => 'blue', 'description' => '', 'is_default' => false, 'archived_at' => null];
|
|
$this->boardForm += ['id' => null, 'name' => '', 'slug' => '', 'description' => '', 'owner_id' => null, 'is_default' => false, 'sort_order' => 0, 'status_ids' => []];
|
|
$this->repositoryForm += ['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, 'category_id' => null, 'team_id' => null, 'name' => '', 'slug' => '', 'visibility' => 'agents', 'subject' => '', 'body' => '', 'is_active' => true, 'sort_order' => 0];
|
|
$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->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->productForm += ['id' => null, 'name' => '', 'slug' => '', 'type' => 'service', 'version' => '', 'release_channel' => '', 'description' => '', 'is_active' => true];
|
|
$this->tagForm += ['id' => null, 'name' => '', 'slug' => '', 'color' => 'gray'];
|
|
$this->checklistTemplateForm += ['id' => null, 'category_id' => null, 'project_id' => null, 'name' => '', 'slug' => '', 'items_text' => '', 'is_active' => true];
|
|
}
|
|
|
|
/**
|
|
* Keep settings section navigation constrained to known panels.
|
|
*/
|
|
private function validSection(string $section): string
|
|
{
|
|
$allowed = [
|
|
'statuses',
|
|
'priorities',
|
|
'categories',
|
|
'teams',
|
|
'projects',
|
|
'boards',
|
|
'git',
|
|
'workflow',
|
|
'canned',
|
|
'sla',
|
|
'automation',
|
|
'checklists',
|
|
'tags',
|
|
'products',
|
|
'defaults',
|
|
];
|
|
|
|
return in_array($section, $allowed, true) ? $section : 'statuses';
|
|
}
|
|
|
|
public function saveStatus(): void
|
|
{
|
|
$data = $this->validate([
|
|
'statusForm.name' => ['required', 'string', 'max:80'],
|
|
'statusForm.slug' => ['nullable', 'string', 'max:100'],
|
|
'statusForm.color' => ['required', 'string', Color::validationRule()],
|
|
'statusForm.sort_order' => ['required', 'integer', 'min:0'],
|
|
'statusForm.is_closed' => ['boolean'],
|
|
'statusForm.is_default' => ['boolean'],
|
|
])['statusForm'];
|
|
|
|
$data['slug'] = $this->slug($data['slug'] ?: $data['name']);
|
|
$data = $this->guardDefaultColumn('helpdesk_statuses', $data);
|
|
|
|
if ((bool) ($data['is_default'] ?? false)) {
|
|
$this->clearOtherDefaults(Status::class, $this->statusForm['id']);
|
|
}
|
|
|
|
$this->persistSettingModel(Status::class, $this->statusForm['id'], $data);
|
|
$this->resetStatusForm();
|
|
$this->statusModalOpen = false;
|
|
session()->flash('success', 'Status saved.');
|
|
}
|
|
|
|
public function editStatus(int $id): void
|
|
{
|
|
$this->statusForm = Status::query()->findOrFail($id)->only(['id', 'name', 'slug', 'color', 'sort_order', 'is_closed', 'is_default']);
|
|
$this->statusForm['color'] = Color::normalise($this->statusForm['color'] ?? 'gray');
|
|
$this->section = 'statuses';
|
|
$this->statusModalOpen = true;
|
|
}
|
|
|
|
public function deleteStatus(int $id): void
|
|
{
|
|
Status::query()->findOrFail($id)->delete();
|
|
session()->flash('success', 'Status deleted.');
|
|
}
|
|
|
|
public function openCreateStatusModal(): void
|
|
{
|
|
$this->resetValidation();
|
|
$this->resetStatusForm();
|
|
$this->section = 'statuses';
|
|
$this->statusModalOpen = true;
|
|
}
|
|
|
|
public function closeStatusModal(): void
|
|
{
|
|
$this->resetValidation();
|
|
$this->resetStatusForm();
|
|
$this->statusModalOpen = false;
|
|
}
|
|
|
|
public function resetStatusForm(): void
|
|
{
|
|
$this->statusForm = ['id' => null, 'name' => '', 'slug' => '', 'color' => 'gray', 'sort_order' => 0, 'is_closed' => false, 'is_default' => false];
|
|
}
|
|
|
|
public function savePriority(): void
|
|
{
|
|
$data = $this->validate([
|
|
'priorityForm.name' => ['required', 'string', 'max:80'],
|
|
'priorityForm.slug' => ['nullable', 'string', 'max:100'],
|
|
'priorityForm.color' => ['required', 'string', Color::validationRule()],
|
|
'priorityForm.sort_order' => ['required', 'integer', 'min:0'],
|
|
'priorityForm.is_default' => ['boolean'],
|
|
])['priorityForm'];
|
|
|
|
$data['slug'] = $this->slug($data['slug'] ?: $data['name']);
|
|
$data = $this->guardDefaultColumn('helpdesk_priorities', $data);
|
|
|
|
if ((bool) ($data['is_default'] ?? false)) {
|
|
$this->clearOtherDefaults(Priority::class, $this->priorityForm['id']);
|
|
}
|
|
$this->persistSettingModel(Priority::class, $this->priorityForm['id'], $data);
|
|
$this->resetPriorityForm();
|
|
$this->priorityModalOpen = false;
|
|
session()->flash('success', 'Priority saved.');
|
|
}
|
|
|
|
public function editPriority(int $id): void
|
|
{
|
|
$this->priorityForm = Priority::query()->findOrFail($id)->only(['id', 'name', 'slug', 'color', 'sort_order', 'is_default']);
|
|
$this->priorityForm['color'] = Color::normalise($this->priorityForm['color'] ?? 'gray');
|
|
$this->section = 'priorities';
|
|
$this->priorityModalOpen = true;
|
|
}
|
|
|
|
public function deletePriority(int $id): void
|
|
{
|
|
Priority::query()->findOrFail($id)->delete();
|
|
session()->flash('success', 'Priority deleted.');
|
|
}
|
|
|
|
public function openCreatePriorityModal(): void
|
|
{
|
|
$this->resetValidation();
|
|
$this->resetPriorityForm();
|
|
$this->section = 'priorities';
|
|
$this->priorityModalOpen = true;
|
|
}
|
|
|
|
public function closePriorityModal(): void
|
|
{
|
|
$this->resetValidation();
|
|
$this->resetPriorityForm();
|
|
$this->priorityModalOpen = false;
|
|
}
|
|
|
|
public function resetPriorityForm(): void
|
|
{
|
|
$this->priorityForm = ['id' => null, 'name' => '', 'slug' => '', 'color' => 'gray', 'sort_order' => 0, 'is_default' => false];
|
|
}
|
|
|
|
public function saveCategory(): void
|
|
{
|
|
$this->normalizeNullableFormValues('categoryForm', []);
|
|
|
|
$data = $this->validate([
|
|
'categoryForm.name' => ['required', 'string', 'max:120'],
|
|
'categoryForm.slug' => ['nullable', 'string', 'max:140'],
|
|
'categoryForm.color' => ['required', 'string', Color::validationRule()],
|
|
'categoryForm.description' => ['nullable', 'string'],
|
|
'categoryForm.is_default' => ['boolean'],
|
|
])['categoryForm'];
|
|
|
|
$data['slug'] = $this->slug($data['slug'] ?: $data['name']);
|
|
$data = $this->guardDefaultColumn('helpdesk_categories', $data);
|
|
|
|
if ((bool) ($data['is_default'] ?? false)) {
|
|
$this->clearOtherDefaults(Category::class, $this->categoryForm['id']);
|
|
}
|
|
$this->persistSettingModel(Category::class, $this->categoryForm['id'], $data);
|
|
$this->resetCategoryForm();
|
|
$this->categoryModalOpen = false;
|
|
session()->flash('success', 'Category saved.');
|
|
}
|
|
|
|
public function editCategory(int $id): void
|
|
{
|
|
$this->categoryForm = Category::query()->findOrFail($id)->only(['id', 'name', 'slug', 'color', 'description', 'is_default']);
|
|
$this->categoryForm['color'] = Color::normalise($this->categoryForm['color'] ?? 'blue');
|
|
$this->section = 'categories';
|
|
$this->categoryModalOpen = true;
|
|
}
|
|
|
|
public function deleteCategory(int $id): void
|
|
{
|
|
Category::query()->findOrFail($id)->delete();
|
|
session()->flash('success', 'Category deleted.');
|
|
}
|
|
|
|
public function openCreateCategoryModal(): void
|
|
{
|
|
$this->resetValidation();
|
|
$this->resetCategoryForm();
|
|
$this->section = 'categories';
|
|
$this->categoryModalOpen = true;
|
|
}
|
|
|
|
public function closeCategoryModal(): void
|
|
{
|
|
$this->resetValidation();
|
|
$this->resetCategoryForm();
|
|
$this->categoryModalOpen = false;
|
|
}
|
|
|
|
public function resetCategoryForm(): void
|
|
{
|
|
$this->categoryForm = ['id' => null, 'name' => '', 'slug' => '', 'color' => 'blue', 'description' => '', 'is_default' => false];
|
|
}
|
|
|
|
public function saveTeam(): void
|
|
{
|
|
$this->resetValidation();
|
|
$this->normalizeNullableFormValues('teamForm', []);
|
|
|
|
$data = $this->validate([
|
|
'teamForm.name' => ['required', 'string', 'max:120'],
|
|
'teamForm.slug' => ['nullable', 'string', 'max:140'],
|
|
'teamForm.color' => ['required', 'string', Color::validationRule()],
|
|
'teamForm.description' => ['nullable', 'string'],
|
|
'teamForm.is_default' => ['boolean'],
|
|
'teamForm.user_ids' => ['array'],
|
|
'teamForm.user_ids.*' => ['integer', 'distinct', 'exists:users,id'],
|
|
])['teamForm'];
|
|
|
|
$teamId = filled($this->teamForm['id'] ?? null)
|
|
? (int) $this->teamForm['id']
|
|
: null;
|
|
|
|
$userIds = collect($data['user_ids'] ?? [])
|
|
->map(static fn ($id): int => (int) $id)
|
|
->unique()
|
|
->values()
|
|
->all();
|
|
|
|
unset($data['user_ids']);
|
|
|
|
$data['slug'] = $this->slug($data['slug'] ?: $data['name']);
|
|
$data = $this->guardDefaultColumn('helpdesk_teams', $data);
|
|
|
|
if ((bool) ($data['is_default'] ?? false)) {
|
|
$this->clearOtherDefaults(Team::class, $teamId);
|
|
}
|
|
|
|
try {
|
|
if ($teamId !== null) {
|
|
/** @var Team $team */
|
|
$team = Team::withTrashed()->findOrFail($teamId);
|
|
|
|
if ($team->trashed()) {
|
|
$team->restore();
|
|
}
|
|
|
|
$team->fill($data);
|
|
$team->save();
|
|
} else {
|
|
// Team slugs are unique even after soft deletion. Recreating a
|
|
// previously deleted team should restore that record instead
|
|
// of failing with a duplicate-key exception.
|
|
/** @var Team|null $team */
|
|
$team = Team::withTrashed()
|
|
->where('slug', $data['slug'])
|
|
->first();
|
|
|
|
if ($team !== null) {
|
|
if (! $team->trashed()) {
|
|
throw ValidationException::withMessages([
|
|
'teamForm.slug' => 'A team with this slug already exists.',
|
|
]);
|
|
}
|
|
|
|
$team->restore();
|
|
$team->fill($data);
|
|
$team->save();
|
|
} else {
|
|
$team = Team::query()->create($data);
|
|
}
|
|
}
|
|
|
|
if (Schema::hasTable('helpdesk_team_user')) {
|
|
$team->users()->sync($userIds);
|
|
}
|
|
} catch (ValidationException $exception) {
|
|
throw $exception;
|
|
} catch (QueryException $exception) {
|
|
throw ValidationException::withMessages([
|
|
'teamForm.slug' => 'The team could not be saved. Check that the name/slug is unique and try again.',
|
|
]);
|
|
}
|
|
|
|
$this->resetTeamForm();
|
|
$this->teamModalOpen = false;
|
|
$this->section = 'teams';
|
|
|
|
session()->flash('success', 'Team saved successfully.');
|
|
}
|
|
|
|
public function editTeam(int $id): void
|
|
{
|
|
$team = Team::withTrashed()->with('users:id')->findOrFail($id);
|
|
$this->teamForm = $team->only(['id', 'name', 'slug', 'color', 'description', 'is_default', 'archived_at']);
|
|
$this->teamForm['user_ids'] = $team->users->pluck('id')->map(static fn ($id): int => (int) $id)->all();
|
|
$this->teamForm['color'] = Color::normalise($this->teamForm['color'] ?? 'indigo');
|
|
$this->section = 'teams';
|
|
$this->teamModalOpen = true;
|
|
}
|
|
|
|
public function openCreateTeamModal(): void
|
|
{
|
|
$this->resetValidation();
|
|
$this->resetTeamForm();
|
|
$this->section = 'teams';
|
|
$this->teamModalOpen = true;
|
|
}
|
|
|
|
public function closeTeamModal(): void
|
|
{
|
|
$this->resetValidation();
|
|
$this->resetTeamForm();
|
|
$this->teamModalOpen = false;
|
|
}
|
|
|
|
public function deleteTeam(int $id): void
|
|
{
|
|
Team::query()->findOrFail($id)->delete();
|
|
session()->flash('success', 'Team deleted.');
|
|
}
|
|
|
|
public function removeUserFromTeam(int $teamId, int $userId): void
|
|
{
|
|
$team = Team::query()->findOrFail($teamId);
|
|
$team->users()->detach($userId);
|
|
|
|
if ((int) ($this->teamForm['id'] ?? 0) === $teamId) {
|
|
$this->teamForm['user_ids'] = array_values(array_filter(
|
|
array_map('intval', $this->teamForm['user_ids'] ?? []),
|
|
static fn (int $id): bool => $id !== $userId,
|
|
));
|
|
}
|
|
|
|
session()->flash('success', 'User removed from team.');
|
|
}
|
|
|
|
public function resetTeamForm(): void
|
|
{
|
|
$this->teamForm = ['id' => null, 'name' => '', 'slug' => '', 'color' => 'indigo', 'description' => '', 'is_default' => false, 'archived_at' => null, 'user_ids' => []];
|
|
}
|
|
|
|
public function saveProject(): void
|
|
{
|
|
$this->normalizeNullableFormValues('projectForm', ['team_id']);
|
|
|
|
$data = $this->validate([
|
|
'projectForm.team_id' => ['nullable', 'integer', 'exists:helpdesk_teams,id'],
|
|
'projectForm.title' => ['required', 'string', 'max:160'],
|
|
'projectForm.slug' => ['nullable', 'string', 'max:180'],
|
|
'projectForm.color' => ['required', 'string', Color::validationRule()],
|
|
'projectForm.description' => ['nullable', 'string'],
|
|
'projectForm.is_default' => ['boolean'],
|
|
])['projectForm'];
|
|
|
|
$data['slug'] = $this->slug($data['slug'] ?: $data['title']);
|
|
$data = $this->guardDefaultColumn('helpdesk_projects', $data);
|
|
|
|
if ((bool) ($data['is_default'] ?? false)) {
|
|
$this->clearOtherDefaults(Project::class, $this->projectForm['id']);
|
|
}
|
|
$this->persistSettingModel(Project::class, $this->projectForm['id'], $data);
|
|
$this->resetProjectForm();
|
|
$this->projectModalOpen = false;
|
|
session()->flash('success', 'Project saved.');
|
|
}
|
|
|
|
public function editProject(int $id): void
|
|
{
|
|
$this->projectForm = Project::query()->findOrFail($id)->only(['id', 'team_id', 'title', 'slug', 'color', 'description', 'is_default', 'archived_at']);
|
|
$this->projectForm['color'] = Color::normalise($this->projectForm['color'] ?? 'blue');
|
|
$this->section = 'projects';
|
|
$this->projectModalOpen = true;
|
|
}
|
|
|
|
public function deleteProject(int $id): void
|
|
{
|
|
Project::query()->findOrFail($id)->delete();
|
|
session()->flash('success', 'Project deleted.');
|
|
}
|
|
|
|
public function openCreateProjectModal(): void
|
|
{
|
|
$this->resetValidation();
|
|
$this->resetProjectForm();
|
|
$this->section = 'projects';
|
|
$this->projectModalOpen = true;
|
|
}
|
|
|
|
public function closeProjectModal(): void
|
|
{
|
|
$this->resetValidation();
|
|
$this->resetProjectForm();
|
|
$this->projectModalOpen = false;
|
|
}
|
|
|
|
public function resetProjectForm(): void
|
|
{
|
|
$this->projectForm = ['id' => null, 'team_id' => null, 'title' => '', 'slug' => '', 'color' => 'blue', 'description' => '', 'is_default' => false, 'archived_at' => null];
|
|
}
|
|
|
|
public function saveBoard(): void
|
|
{
|
|
$this->normalizeNullableFormValues('boardForm', ['owner_id']);
|
|
|
|
$data = $this->validate([
|
|
'boardForm.name' => ['required', 'string', 'max:160'],
|
|
'boardForm.slug' => ['nullable', 'string', 'max:180'],
|
|
'boardForm.description' => ['nullable', 'string'],
|
|
'boardForm.owner_id' => ['nullable', 'integer', 'exists:users,id'],
|
|
'boardForm.is_default' => ['boolean'],
|
|
'boardForm.sort_order' => ['required', 'integer', 'min:0'],
|
|
'boardForm.status_ids' => ['array'],
|
|
'boardForm.status_ids.*' => ['integer', 'exists:helpdesk_statuses,id'],
|
|
])['boardForm'];
|
|
|
|
$statusIds = collect($data['status_ids'] ?? [])->map(static fn ($id): int => (int) $id)->filter()->values();
|
|
unset($data['status_ids']);
|
|
|
|
$data['owner_id'] = blank($data['owner_id'] ?? null) ? null : (int) $data['owner_id'];
|
|
$data['slug'] = $this->slug($data['slug'] ?: $data['name']);
|
|
|
|
if ((bool) ($data['is_default'] ?? false)) {
|
|
$this->clearOtherDefaults(KanbanBoard::class, $this->boardForm['id']);
|
|
}
|
|
|
|
$board = $this->persistSettingModel(KanbanBoard::class, $this->boardForm['id'], $data);
|
|
|
|
$sync = [];
|
|
foreach ($statusIds as $index => $statusId) {
|
|
$sync[$statusId] = ['sort_order' => ($index + 1) * 10];
|
|
}
|
|
|
|
$board->statuses()->sync($sync);
|
|
$this->resetBoardForm();
|
|
$this->boardModalOpen = false;
|
|
$this->section = 'boards';
|
|
session()->flash('success', 'Kanban board saved.');
|
|
}
|
|
|
|
public function editBoard(int $id): void
|
|
{
|
|
$board = KanbanBoard::query()->with('statuses')->findOrFail($id);
|
|
|
|
$this->boardForm = [
|
|
'id' => $board->id,
|
|
'name' => $board->name,
|
|
'slug' => $board->slug,
|
|
'description' => $board->description,
|
|
'owner_id' => $board->owner_id,
|
|
'is_default' => $board->is_default,
|
|
'sort_order' => $board->sort_order,
|
|
'status_ids' => $board->statuses->pluck('id')->map(static fn ($id): int => (int) $id)->all(),
|
|
];
|
|
|
|
$this->section = 'boards';
|
|
$this->boardModalOpen = true;
|
|
}
|
|
|
|
public function deleteBoard(int $id): void
|
|
{
|
|
KanbanBoard::query()->findOrFail($id)->delete();
|
|
session()->flash('success', 'Kanban board deleted.');
|
|
}
|
|
|
|
public function openCreateBoardModal(): void
|
|
{
|
|
$this->resetValidation();
|
|
$this->resetBoardForm();
|
|
$this->section = 'boards';
|
|
$this->boardModalOpen = true;
|
|
}
|
|
|
|
public function closeBoardModal(): void
|
|
{
|
|
$this->resetValidation();
|
|
$this->resetBoardForm();
|
|
$this->boardModalOpen = false;
|
|
}
|
|
|
|
public function resetBoardForm(): void
|
|
{
|
|
$this->boardForm = ['id' => null, 'name' => '', 'slug' => '', 'description' => '', 'owner_id' => null, 'is_default' => false, 'sort_order' => 0, 'status_ids' => []];
|
|
}
|
|
|
|
public function saveRepository(): void
|
|
{
|
|
$this->normalizeNullableFormValues('repositoryForm', ['project_id', 'webhook_secret']);
|
|
|
|
$data = $this->validate([
|
|
'repositoryForm.project_id' => ['nullable', 'integer', 'exists:helpdesk_projects,id'],
|
|
'repositoryForm.name' => ['required', 'string', 'max:160'],
|
|
'repositoryForm.provider' => ['required', 'string', 'max:40'],
|
|
'repositoryForm.remote_url' => ['required', 'string', 'max:500'],
|
|
'repositoryForm.default_branch' => ['required', 'string', 'max:120'],
|
|
'repositoryForm.webhook_secret' => ['nullable', 'string', 'max:120'],
|
|
])['repositoryForm'];
|
|
|
|
$data['provider'] = strtolower(trim((string) $data['provider']));
|
|
|
|
$this->persistSettingModel(GitRepository::class, $this->repositoryForm['id'], $data);
|
|
$this->resetRepositoryForm();
|
|
$this->repositoryModalOpen = false;
|
|
session()->flash('success', 'Git repository saved.');
|
|
}
|
|
|
|
public function editRepository(int $id): void
|
|
{
|
|
$this->repositoryForm = GitRepository::query()->findOrFail($id)->only(['id', 'project_id', 'name', 'provider', 'remote_url', 'default_branch', 'webhook_secret']);
|
|
$this->section = 'git';
|
|
$this->repositoryModalOpen = true;
|
|
}
|
|
|
|
public function deleteRepository(int $id): void
|
|
{
|
|
GitRepository::query()->findOrFail($id)->delete();
|
|
session()->flash('success', 'Git repository deleted.');
|
|
}
|
|
|
|
public function openCreateRepositoryModal(): void
|
|
{
|
|
$this->resetValidation();
|
|
$this->resetRepositoryForm();
|
|
$this->section = 'git';
|
|
$this->repositoryModalOpen = true;
|
|
}
|
|
|
|
public function closeRepositoryModal(): void
|
|
{
|
|
$this->resetValidation();
|
|
$this->resetRepositoryForm();
|
|
$this->repositoryModalOpen = false;
|
|
}
|
|
|
|
public function resetRepositoryForm(): void
|
|
{
|
|
$this->repositoryForm = ['id' => null, 'project_id' => null, 'name' => '', 'provider' => 'git', 'remote_url' => '', 'default_branch' => 'main', 'webhook_secret' => ''];
|
|
}
|
|
|
|
public function saveWorkflowTemplate(): void
|
|
{
|
|
$this->normalizeNullableFormValues('workflowForm', ['text_body', 'cc', 'bcc']);
|
|
|
|
$data = $this->validate([
|
|
'workflowForm.name' => ['required', 'string', 'max:160'],
|
|
'workflowForm.event' => ['required', 'string', 'max:80'],
|
|
'workflowForm.subject' => ['required', 'string', 'max:220'],
|
|
'workflowForm.html_body' => ['required', 'string'],
|
|
'workflowForm.text_body' => ['nullable', 'string'],
|
|
'workflowForm.to_role' => ['required', 'string', 'max:40'],
|
|
'workflowForm.cc' => ['nullable', 'string', 'max:500'],
|
|
'workflowForm.bcc' => ['nullable', 'string', 'max:500'],
|
|
'workflowForm.is_active' => ['boolean'],
|
|
])['workflowForm'];
|
|
|
|
$this->persistSettingModel(WorkflowTemplate::class, $this->workflowForm['id'], $data);
|
|
$this->resetWorkflowTemplateForm();
|
|
$this->workflowModalOpen = false;
|
|
session()->flash('success', 'Workflow message saved.');
|
|
}
|
|
|
|
public function editWorkflowTemplate(int $id): void
|
|
{
|
|
$this->workflowForm = WorkflowTemplate::query()->findOrFail($id)->only(['id', 'name', 'event', 'subject', 'html_body', 'text_body', 'to_role', 'cc', 'bcc', 'is_active']);
|
|
$this->section = 'workflow';
|
|
$this->workflowModalOpen = true;
|
|
}
|
|
|
|
public function deleteWorkflowTemplate(int $id): void
|
|
{
|
|
WorkflowTemplate::query()->findOrFail($id)->delete();
|
|
session()->flash('success', 'Workflow message deleted.');
|
|
}
|
|
|
|
public function openCreateWorkflowTemplateModal(): void
|
|
{
|
|
$this->resetValidation();
|
|
$this->resetWorkflowTemplateForm();
|
|
$this->section = 'workflow';
|
|
$this->workflowModalOpen = true;
|
|
}
|
|
|
|
public function closeWorkflowTemplateModal(): void
|
|
{
|
|
$this->resetValidation();
|
|
$this->resetWorkflowTemplateForm();
|
|
$this->workflowModalOpen = false;
|
|
}
|
|
|
|
public function resetWorkflowTemplateForm(): void
|
|
{
|
|
$this->workflowForm = ['id' => null, 'name' => '', 'event' => 'ticket.created', 'subject' => '', 'html_body' => '', 'text_body' => '', 'to_role' => 'requester', 'cc' => '', 'bcc' => '', 'is_active' => true];
|
|
}
|
|
|
|
public function saveCannedResponse(): void
|
|
{
|
|
$this->normalizeNullableFormValues('cannedForm', ['category_id', 'team_id', 'subject']);
|
|
|
|
$data = $this->validate([
|
|
'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']);
|
|
$this->persistSettingModel(CannedResponse::class, $this->cannedForm['id'], $data);
|
|
$this->resetCannedResponseForm();
|
|
$this->cannedModalOpen = false;
|
|
session()->flash('success', 'Canned response saved.');
|
|
}
|
|
|
|
public function editCannedResponse(int $id): void
|
|
{
|
|
$this->cannedForm = CannedResponse::query()->findOrFail($id)->only(['id', 'category_id', 'team_id', 'name', 'slug', 'visibility', 'subject', 'body', 'is_active', 'sort_order']);
|
|
$this->section = 'canned';
|
|
$this->cannedModalOpen = true;
|
|
}
|
|
|
|
public function deleteCannedResponse(int $id): void
|
|
{
|
|
CannedResponse::query()->findOrFail($id)->delete();
|
|
session()->flash('success', 'Canned response deleted.');
|
|
}
|
|
|
|
public function openCreateCannedResponseModal(): void
|
|
{
|
|
$this->resetValidation();
|
|
$this->resetCannedResponseForm();
|
|
$this->section = 'canned';
|
|
$this->cannedModalOpen = true;
|
|
}
|
|
|
|
public function closeCannedResponseModal(): void
|
|
{
|
|
$this->resetValidation();
|
|
$this->resetCannedResponseForm();
|
|
$this->cannedModalOpen = false;
|
|
}
|
|
|
|
public function resetCannedResponseForm(): void
|
|
{
|
|
$this->cannedForm = ['id' => null, 'category_id' => null, 'team_id' => null, 'name' => '', 'slug' => '', 'visibility' => 'agents', 'subject' => '', 'body' => '', 'is_active' => true, 'sort_order' => 0];
|
|
}
|
|
|
|
public function saveSlaPolicy(): void
|
|
{
|
|
$this->normalizeNullableFormValues('slaForm', ['priority_id', 'category_id']);
|
|
|
|
$data = $this->validate([
|
|
'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)) {
|
|
$this->clearOtherDefaults(SlaPolicy::class, $this->slaForm['id']);
|
|
}
|
|
$this->persistSettingModel(SlaPolicy::class, $this->slaForm['id'], $data);
|
|
$this->resetSlaPolicyForm();
|
|
$this->slaModalOpen = false;
|
|
session()->flash('success', 'SLA policy saved.');
|
|
}
|
|
|
|
public function editSlaPolicy(int $id): void
|
|
{
|
|
$this->slaForm = SlaPolicy::query()->findOrFail($id)->only(['id', 'priority_id', 'category_id', 'name', 'slug', 'first_response_minutes', 'resolution_minutes', 'business_hours_only', 'is_default', 'is_active']);
|
|
$this->section = 'sla';
|
|
$this->slaModalOpen = true;
|
|
}
|
|
|
|
public function deleteSlaPolicy(int $id): void
|
|
{
|
|
SlaPolicy::query()->findOrFail($id)->delete();
|
|
session()->flash('success', 'SLA policy deleted.');
|
|
}
|
|
|
|
public function openCreateSlaPolicyModal(): void
|
|
{
|
|
$this->resetValidation();
|
|
$this->resetSlaPolicyForm();
|
|
$this->section = 'sla';
|
|
$this->slaModalOpen = true;
|
|
}
|
|
|
|
public function closeSlaPolicyModal(): void
|
|
{
|
|
$this->resetValidation();
|
|
$this->resetSlaPolicyForm();
|
|
$this->slaModalOpen = false;
|
|
}
|
|
|
|
public function resetSlaPolicyForm(): void
|
|
{
|
|
$this->slaForm = ['id' => null, 'priority_id' => null, 'category_id' => null, 'name' => '', 'slug' => '', 'first_response_minutes' => 240, 'resolution_minutes' => 2880, 'business_hours_only' => false, 'is_default' => false, 'is_active' => true];
|
|
}
|
|
|
|
public function saveAutomationRule(): void
|
|
{
|
|
$this->normalizeNullableFormValues('automationForm', ['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']);
|
|
|
|
$data = $this->validate([
|
|
'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']);
|
|
$this->persistSettingModel(AutomationRule::class, $this->automationForm['id'], $data);
|
|
$this->resetAutomationRuleForm();
|
|
$this->automationModalOpen = false;
|
|
session()->flash('success', 'Automation rule saved.');
|
|
}
|
|
|
|
public function editAutomationRule(int $id): void
|
|
{
|
|
$this->automationForm = AutomationRule::query()->findOrFail($id)->only(['id', 'name', 'slug', 'match_sender', 'match_subject', 'match_mailbox', 'match_category_id', 'set_priority_id', 'set_category_id', 'set_team_id', 'set_project_id', 'set_status_id', 'set_agent_id', 'is_active', 'sort_order']);
|
|
$this->section = 'automation';
|
|
$this->automationModalOpen = true;
|
|
}
|
|
|
|
public function deleteAutomationRule(int $id): void
|
|
{
|
|
AutomationRule::query()->findOrFail($id)->delete();
|
|
session()->flash('success', 'Automation rule deleted.');
|
|
}
|
|
|
|
public function openCreateAutomationRuleModal(): void
|
|
{
|
|
$this->resetValidation();
|
|
$this->resetAutomationRuleForm();
|
|
$this->section = 'automation';
|
|
$this->automationModalOpen = true;
|
|
}
|
|
|
|
public function closeAutomationRuleModal(): void
|
|
{
|
|
$this->resetValidation();
|
|
$this->resetAutomationRuleForm();
|
|
$this->automationModalOpen = false;
|
|
}
|
|
|
|
public function resetAutomationRuleForm(): void
|
|
{
|
|
$this->automationForm = ['id' => null, 'name' => '', 'slug' => '', 'match_sender' => '', 'match_subject' => '', 'match_mailbox' => '', 'match_category_id' => null, 'set_priority_id' => null, 'set_category_id' => null, 'set_team_id' => null, 'set_project_id' => null, 'set_status_id' => null, 'set_agent_id' => null, 'is_active' => true, 'sort_order' => 0];
|
|
}
|
|
|
|
public function saveProduct(): void
|
|
{
|
|
$this->normalizeNullableFormValues('productForm', ['version', 'release_channel', 'description']);
|
|
|
|
$data = $this->validate([
|
|
'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']);
|
|
$this->persistSettingModel(Product::class, $this->productForm['id'], $data);
|
|
$this->resetProductForm();
|
|
$this->productModalOpen = false;
|
|
session()->flash('success', 'Product/service saved.');
|
|
}
|
|
|
|
public function editProduct(int $id): void
|
|
{
|
|
$this->productForm = Product::query()->findOrFail($id)->only(['id', 'name', 'slug', 'type', 'version', 'release_channel', 'description', 'is_active']);
|
|
$this->section = 'products';
|
|
$this->productModalOpen = true;
|
|
}
|
|
|
|
public function deleteProduct(int $id): void
|
|
{
|
|
Product::query()->findOrFail($id)->delete();
|
|
session()->flash('success', 'Product/service deleted.');
|
|
}
|
|
|
|
public function openCreateProductModal(): void
|
|
{
|
|
$this->resetValidation();
|
|
$this->resetProductForm();
|
|
$this->section = 'products';
|
|
$this->productModalOpen = true;
|
|
}
|
|
|
|
public function closeProductModal(): void
|
|
{
|
|
$this->resetValidation();
|
|
$this->resetProductForm();
|
|
$this->productModalOpen = false;
|
|
}
|
|
|
|
public function resetProductForm(): void
|
|
{
|
|
$this->productForm = ['id' => null, 'name' => '', 'slug' => '', 'type' => 'service', 'version' => '', 'release_channel' => '', 'description' => '', 'is_active' => true];
|
|
}
|
|
|
|
public function saveTag(): void
|
|
{
|
|
$this->normalizeNullableFormValues('tagForm', []);
|
|
|
|
$data = $this->validate([
|
|
'tagForm.name' => ['required', 'string', 'max:80'],
|
|
'tagForm.slug' => ['nullable', 'string', 'max:100'],
|
|
'tagForm.color' => ['required', 'string', Color::validationRule()],
|
|
])['tagForm'];
|
|
$data['slug'] = $this->slug($data['slug'] ?: $data['name']);
|
|
$this->persistSettingModel(Tag::class, $this->tagForm['id'], $data);
|
|
$this->resetTagForm();
|
|
$this->tagModalOpen = false;
|
|
session()->flash('success', 'Tag saved.');
|
|
}
|
|
|
|
public function editTag(int $id): void
|
|
{
|
|
$this->tagForm = Tag::query()->findOrFail($id)->only(['id', 'name', 'slug', 'color']);
|
|
$this->tagForm['color'] = Color::normalise($this->tagForm['color'] ?? 'gray');
|
|
$this->section = 'tags';
|
|
$this->tagModalOpen = true;
|
|
}
|
|
|
|
public function deleteTag(int $id): void
|
|
{
|
|
Tag::query()->findOrFail($id)->delete();
|
|
session()->flash('success', 'Tag deleted.');
|
|
}
|
|
|
|
public function openCreateTagModal(): void
|
|
{
|
|
$this->resetValidation();
|
|
$this->resetTagForm();
|
|
$this->section = 'tags';
|
|
$this->tagModalOpen = true;
|
|
}
|
|
|
|
public function closeTagModal(): void
|
|
{
|
|
$this->resetValidation();
|
|
$this->resetTagForm();
|
|
$this->tagModalOpen = false;
|
|
}
|
|
|
|
public function resetTagForm(): void
|
|
{
|
|
$this->tagForm = ['id' => null, 'name' => '', 'slug' => '', 'color' => 'gray'];
|
|
}
|
|
|
|
public function saveChecklistTemplate(): void
|
|
{
|
|
$this->normalizeNullableFormValues('checklistTemplateForm', ['category_id', 'project_id']);
|
|
|
|
$data = $this->validate([
|
|
'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();
|
|
$this->persistSettingModel(ChecklistTemplate::class, $this->checklistTemplateForm['id'], $data);
|
|
$this->resetChecklistTemplateForm();
|
|
$this->checklistModalOpen = false;
|
|
session()->flash('success', 'Checklist template saved.');
|
|
}
|
|
|
|
public function editChecklistTemplate(int $id): void
|
|
{
|
|
$template = ChecklistTemplate::query()->findOrFail($id);
|
|
$this->checklistTemplateForm = $template->only(['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';
|
|
$this->checklistModalOpen = true;
|
|
}
|
|
|
|
public function deleteChecklistTemplate(int $id): void
|
|
{
|
|
ChecklistTemplate::query()->findOrFail($id)->delete();
|
|
session()->flash('success', 'Checklist template deleted.');
|
|
}
|
|
|
|
public function openCreateChecklistTemplateModal(): void
|
|
{
|
|
$this->resetValidation();
|
|
$this->resetChecklistTemplateForm();
|
|
$this->section = 'checklists';
|
|
$this->checklistModalOpen = true;
|
|
}
|
|
|
|
public function closeChecklistTemplateModal(): void
|
|
{
|
|
$this->resetValidation();
|
|
$this->resetChecklistTemplateForm();
|
|
$this->checklistModalOpen = false;
|
|
}
|
|
|
|
public function resetChecklistTemplateForm(): void
|
|
{
|
|
$this->checklistTemplateForm = ['id' => null, 'category_id' => null, 'project_id' => null, 'name' => '', 'slug' => '', 'items_text' => '', 'is_active' => true];
|
|
}
|
|
|
|
#[Layout('layouts.app')]
|
|
public function render(): View
|
|
{
|
|
return view()->file(
|
|
resource_path('themes/backend/views/modules/helpdesk/livewire/settings-manager.blade.php'),
|
|
[
|
|
'statuses' => $this->orderedStatuses(),
|
|
'priorities' => $this->orderedPriorities(),
|
|
'categories' => $this->orderedCategories(),
|
|
'teams' => $this->orderedTeams(),
|
|
'teamUsers' => User::query()
|
|
->orderBy('name')
|
|
->get(['id', 'name', 'email', 'helpdesk_agent', 'helpdesk_admin']),
|
|
'projects' => $this->orderedProjects(),
|
|
'boardOwners' => User::query()->where(static function ($query): void {
|
|
$query->where('helpdesk_agent', true)->orWhere('helpdesk_admin', true);
|
|
})->orderBy('name')->get(['id', 'name', 'email']),
|
|
'boards' => KanbanBoard::query()
|
|
->with(['statuses', 'owner:id,name,email'])
|
|
->withCount('tickets')
|
|
->orderBy('sort_order')
|
|
->orderBy('name')
|
|
->paginate(10, ['*'], 'boardsPage'),
|
|
'repositories' => GitRepository::query()
|
|
->with(['project'])
|
|
->withCount(['commits', 'webhookLogs'])
|
|
->orderBy('name')
|
|
->paginate(10, ['*'], 'gitPage'),
|
|
'workflowTemplates' => WorkflowTemplate::query()
|
|
->orderBy('event')
|
|
->orderBy('name')
|
|
->paginate(10, ['*'], 'workflowPage'),
|
|
'cannedResponses' => CannedResponse::query()
|
|
->with(['category', 'team'])
|
|
->orderBy('sort_order')
|
|
->orderBy('name')
|
|
->paginate(10, ['*'], 'cannedPage'),
|
|
'slaPolicies' => SlaPolicy::query()
|
|
->with(['priority', 'category'])
|
|
->orderByDesc('is_default')
|
|
->orderBy('name')
|
|
->paginate(10, ['*'], 'slaPage'),
|
|
'automationRules' => AutomationRule::query()
|
|
->orderBy('sort_order')
|
|
->orderBy('name')
|
|
->paginate(10, ['*'], 'rulesPage'),
|
|
'products' => Product::query()
|
|
->withCount('tickets')
|
|
->orderBy('name')
|
|
->paginate(10, ['*'], 'productsPage'),
|
|
'tags' => Tag::query()
|
|
->orderBy('name')
|
|
->paginate(10, ['*'], 'tagsPage'),
|
|
'checklistTemplates' => ChecklistTemplate::query()
|
|
->orderBy('name')
|
|
->paginate(10, ['*'], 'checksPage'),
|
|
'colourOptions' => Color::options(),
|
|
]
|
|
);
|
|
}
|
|
|
|
private function orderedStatuses(): mixed
|
|
{
|
|
return Status::query()
|
|
->withCount('tickets')
|
|
->when($this->hasDefaultColumn('helpdesk_statuses'), fn ($query) => $query->orderByDesc('is_default'))
|
|
->orderBy('sort_order')
|
|
->orderBy('name')
|
|
->paginate(10, ['*'], 'statusesPage');
|
|
}
|
|
|
|
private function orderedPriorities(): mixed
|
|
{
|
|
return Priority::query()
|
|
->withCount('tickets')
|
|
->when($this->hasDefaultColumn('helpdesk_priorities'), fn ($query) => $query->orderByDesc('is_default'))
|
|
->orderBy('sort_order')
|
|
->orderBy('name')
|
|
->paginate(10, ['*'], 'prioritiesPage');
|
|
}
|
|
|
|
private function orderedCategories(): mixed
|
|
{
|
|
return Category::query()
|
|
->withCount('tickets')
|
|
->when($this->hasDefaultColumn('helpdesk_categories'), fn ($query) => $query->orderByDesc('is_default'))
|
|
->orderBy('name')
|
|
->paginate(10, ['*'], 'categoriesPage');
|
|
}
|
|
|
|
private function orderedTeams(): mixed
|
|
{
|
|
return Team::query()
|
|
->with(['users:id,name,email'])
|
|
->withCount(['projects', 'tickets', 'users'])
|
|
->when($this->hasDefaultColumn('helpdesk_teams'), fn ($query) => $query->orderByDesc('is_default'))
|
|
->orderBy('name')
|
|
->paginate(10, ['*'], 'teamsPage');
|
|
}
|
|
|
|
private function orderedProjects(): mixed
|
|
{
|
|
return Project::query()
|
|
->with(['team'])
|
|
->withCount('tickets')
|
|
->when($this->hasDefaultColumn('helpdesk_projects'), fn ($query) => $query->orderByDesc('is_default'))
|
|
->orderBy('title')
|
|
->paginate(10, ['*'], 'projectsPage');
|
|
}
|
|
|
|
/**
|
|
* Persist a settings record without inserting a null primary key.
|
|
*
|
|
* Eloquent updateOrCreate(['id' => null], ...) merges the null id into the
|
|
* create payload. PostgreSQL rejects that explicit null for bigserial ids,
|
|
* which made every new Helpdesk settings CRUD form appear to do nothing.
|
|
*
|
|
* @template TModel of \Illuminate\Database\Eloquent\Model
|
|
*
|
|
* @param class-string<TModel> $modelClass
|
|
* @param array<string, mixed> $data
|
|
* @return TModel
|
|
*/
|
|
private function persistSettingModel(string $modelClass, int|string|null $id, array $data): object
|
|
{
|
|
unset($data['id']);
|
|
|
|
try {
|
|
if (filled($id)) {
|
|
/** @var TModel $model */
|
|
$model = $modelClass::query()->findOrFail((int) $id);
|
|
$model->fill($data);
|
|
$model->save();
|
|
|
|
return $model;
|
|
}
|
|
|
|
/** @var TModel $model */
|
|
$model = $modelClass::query()->create($data);
|
|
|
|
return $model;
|
|
} catch (QueryException $exception) {
|
|
throw ValidationException::withMessages([
|
|
'settings' => 'The record could not be saved. Check the slug is unique for this section and try again.',
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clear the default flag on every other record without generating invalid null-id SQL.
|
|
*
|
|
* @param class-string<Model> $modelClass
|
|
*/
|
|
private function clearOtherDefaults(string $modelClass, int|string|null $currentId): void
|
|
{
|
|
$query = $modelClass::query();
|
|
|
|
if (filled($currentId)) {
|
|
$query->whereKeyNot((int) $currentId);
|
|
}
|
|
|
|
$query->update(['is_default' => false]);
|
|
}
|
|
|
|
/**
|
|
* Convert browser select empty strings into null before Livewire validation.
|
|
*
|
|
* Flux/select controls submit optional values as empty strings. Laravel's
|
|
* nullable integer and exists rules expect a real null, so normalising here
|
|
* keeps every settings CRUD form usable from the browser.
|
|
*
|
|
* @param array<int, string> $keys
|
|
*/
|
|
private function normalizeNullableFormValues(string $formProperty, array $keys): void
|
|
{
|
|
foreach ($keys as $key) {
|
|
if (! array_key_exists($key, $this->{$formProperty})) {
|
|
continue;
|
|
}
|
|
|
|
if (blank($this->{$formProperty}[$key])) {
|
|
$this->{$formProperty}[$key] = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
private function normalizeNullableScalar(string $property): void
|
|
{
|
|
if (blank($this->{$property})) {
|
|
$this->{$property} = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove is_default before save when the database has not been migrated yet.
|
|
*
|
|
* @param array<string, mixed> $data
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function guardDefaultColumn(string $table, array $data): array
|
|
{
|
|
if (! $this->hasDefaultColumn($table)) {
|
|
unset($data['is_default']);
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
private function hasDefaultColumn(string $table): bool
|
|
{
|
|
return Schema::hasTable($table) && Schema::hasColumn($table, 'is_default');
|
|
}
|
|
|
|
private function slug(string $value): string
|
|
{
|
|
return Str::slug($value);
|
|
}
|
|
}
|