is_super_admin || $user->can('ticketing.settings')); } /** * Require either: * - existing agent access, OR * - bootstrap state + site admin (so first admin can seed the system) */ private function requireAgentAccess(): void { // Site admins always have access if ($this->isSiteAdmin()) { return; } $hasAccess = TicketingAgentAccess::where('user_id', Auth::id())->exists(); if (!$hasAccess) { abort(403, 'You need agent access to manage ticketing settings.'); } } private function requireManagerAccess(int $groupId): void { $isManager = TicketingAgentAccess::where('user_id', Auth::id()) ->where('group_id', $groupId) ->where('role', 'manager') ->exists(); if (!$isManager) { abort(403); } } public function index(): Response { $this->requireAgentAccess(); $userId = Auth::id(); $isBootstrap = $this->isBootstrapState(); $myGroupIds = TicketingAgentAccess::where('user_id', $userId)->pluck('group_id'); $groups = $isBootstrap ? collect() : TicketingGroup::whereIn('id', $myGroupIds)->get(); $agents = $isBootstrap ? collect() : TicketingAgentAccess::whereIn('group_id', $myGroupIds)->with('group')->get(); if ($agents->isNotEmpty()) { $agentUserIds = $agents->pluck('user_id')->unique(); $agentUsers = \DB::table('users')->whereIn('id', $agentUserIds)->get(['id', 'name', 'email'])->keyBy('id'); $agents->each(fn($a) => $a->user = $agentUsers[$a->user_id] ?? null); } $priorities = $isBootstrap ? collect() : PriorityLevel::orderBy('sort_order')->get(); $projects = $isBootstrap ? collect() : TicketingProject::whereIn('group_id', $myGroupIds) ->orderBy('name') ->get(); $emailConnections = $isBootstrap ? collect() : EmailConnection::whereIn('group_id', $myGroupIds)->get(); return Inertia::render('Ticketing/Settings', [ 'groups' => $groups, 'agents' => $agents, 'priorities' => $priorities, 'projects' => $projects, 'ticketStatuses' => TicketStatus::orderBy('sort_order')->get(), 'emailConnections' => $emailConnections, 'myGroupIds' => $myGroupIds, 'isBootstrap' => $isBootstrap, 'isSiteAdmin' => $this->isSiteAdmin(), 'autoCloseDays' => (int) Setting::get('ticketing.auto_close_days', 0), ]); } public function storeGroup(Request $request) { // During bootstrap, any site admin can create the first group. // After bootstrap, require existing agent access. if ($this->isBootstrapState()) { if (!$this->isSiteAdmin()) { abort(403, 'Only site admins can create the first group.'); } } else { $this->requireAgentAccess(); } $validated = $request->validate([ 'name' => 'required|string|max:100', 'email_address' => 'nullable|email', 'color' => 'required|string|regex:/^#[0-9a-fA-F]{6}$/', 'prefix' => 'required|string|max:10|alpha_num|unique:ticketing_groups,prefix', ]); $group = TicketingGroup::create($validated); // Auto-add creator as manager TicketingAgentAccess::create([ 'user_id' => Auth::id(), 'group_id' => $group->id, 'role' => 'manager', ]); // Seed default priorities for this group if none exist globally if (PriorityLevel::count() === 0) { $defaults = [ ['name' => 'Low', 'color' => '#6b7280', 'sort_order' => 1], ['name' => 'Medium', 'color' => '#3b82f6', 'sort_order' => 2], ['name' => 'High', 'color' => '#f59e0b', 'sort_order' => 3], ['name' => 'Urgent', 'color' => '#ef4444', 'sort_order' => 4], ]; foreach ($defaults as $d) { PriorityLevel::create([ 'name' => $d['name'], 'color' => $d['color'], 'description' => null, 'sort_order' => $d['sort_order'], ]); } } return back()->with('success', 'Group created.'); } public function updateGroup(Request $request, TicketingGroup $group) { $this->requireManagerAccess($group->id); $validated = $request->validate([ 'name' => 'required|string|max:100', 'email_address' => 'nullable|email', 'color' => 'required|string|regex:/^#[0-9a-fA-F]{6}$/', 'prefix' => 'required|string|max:10|alpha_num|unique:ticketing_groups,prefix,' . $group->id, ]); $group->update($validated); return back()->with('success', 'Group updated.'); } public function storeAgent(Request $request) { $this->requireAgentAccess(); $validated = $request->validate([ 'user_id' => 'required|exists:users,id', 'group_id' => 'required|exists:ticketing_groups,id', 'role' => 'required|in:agent,manager', ]); $this->requireManagerAccess($validated['group_id']); TicketingAgentAccess::updateOrCreate( ['user_id' => $validated['user_id'], 'group_id' => $validated['group_id']], ['role' => $validated['role']] ); return back()->with('success', 'Agent added.'); } public function destroyAgent(TicketingAgentAccess $access) { $this->requireManagerAccess($access->group_id); $access->delete(); return back()->with('success', 'Agent removed.'); } public function storePriority(Request $request) { if (!$this->isSiteAdmin()) { abort(403, 'Only site admins can manage global priorities.'); } $validated = $request->validate([ 'name' => 'required|string|max:100', 'color' => 'required|string|regex:/^#[0-9a-fA-F]{6}$/', 'description' => 'nullable|string', 'sort_order' => 'integer|min:0', ]); PriorityLevel::create($validated); return back()->with('success', 'Priority level created.'); } public function updatePriority(Request $request, PriorityLevel $priority) { if (!$this->isSiteAdmin()) { abort(403, 'Only site admins can manage global priorities.'); } $validated = $request->validate([ 'name' => 'required|string|max:100', 'color' => 'required|string|regex:/^#[0-9a-fA-F]{6}$/', 'description' => 'nullable|string', 'sort_order' => 'required|integer|min:0', ]); $priority->update($validated); return back()->with('success', 'Priority level updated.'); } public function destroyPriority(PriorityLevel $priority) { if (!$this->isSiteAdmin()) { abort(403, 'Only site admins can manage global priorities.'); } if ($priority->tickets()->exists()) { return back()->withErrors([ 'priority' => 'Cannot delete a priority that is in use by tickets.', ]); } $priority->delete(); return back()->with('success', 'Priority level removed.'); } public function storeProject(Request $request) { $this->requireAgentAccess(); $validated = $request->validate([ 'group_id' => 'required|exists:ticketing_groups,id', 'name' => 'required|string|max:100', 'description' => 'nullable|string', 'status' => 'required|in:active,archived', ]); $this->requireManagerAccess($validated['group_id']); TicketingProject::create([ ...$validated, 'created_by' => Auth::id(), ]); return back()->with('success', 'Project created.'); } public function updateProject(Request $request, TicketingProject $project) { $this->requireAgentAccess(); $this->requireManagerAccess($project->group_id); $validated = $request->validate([ 'name' => 'required|string|max:100', 'description' => 'nullable|string', 'status' => 'required|in:active,archived', ]); $project->update($validated); return back()->with('success', 'Project updated.'); } public function destroyProject(TicketingProject $project) { $this->requireAgentAccess(); $this->requireManagerAccess($project->group_id); if ($project->tickets()->exists()) { return back()->withErrors([ 'project' => 'Cannot delete a project that is in use by tickets.', ]); } $project->delete(); return back()->with('success', 'Project removed.'); } public function storeEmailConnection(Request $request) { $this->requireAgentAccess(); $validated = $request->validate([ 'group_id' => 'required|exists:ticketing_groups,id', 'type' => 'required|in:gmail,imap', 'active' => 'boolean', 'config.host' => 'nullable|string|max:255', 'config.port' => 'nullable|integer', 'config.username' => 'nullable|string|max:255', 'config.password' => 'nullable|string|max:500', 'config.encryption' => 'nullable|in:ssl,tls,none', 'config.mailbox' => 'nullable|string|max:255', ]); $this->requireManagerAccess($validated['group_id']); EmailConnection::create([ 'group_id' => $validated['group_id'], 'type' => $validated['type'], 'active' => $request->boolean('active', true), 'config' => $request->input('config', []), ]); return back()->with('success', 'Email connection saved.'); } public function updateEmailConnection(Request $request, EmailConnection $connection) { $this->requireAgentAccess(); $this->requireManagerAccess($connection->group_id); $request->validate([ 'type' => 'required|in:gmail,imap', 'active' => 'boolean', 'config.host' => 'nullable|string|max:255', 'config.port' => 'nullable|integer', 'config.username' => 'nullable|string|max:255', 'config.password' => 'nullable|string|max:500', 'config.encryption' => 'nullable|in:ssl,tls,none', 'config.mailbox' => 'nullable|string|max:255', ]); $connection->update([ 'type' => $request->type, 'active' => $request->boolean('active', true), 'config' => $request->input('config', $connection->config), ]); return back()->with('success', 'Email connection updated.'); } public function destroyEmailConnection(EmailConnection $connection) { $this->requireAgentAccess(); $this->requireManagerAccess($connection->group_id); $connection->delete(); return back()->with('success', 'Email connection removed.'); } public function storeStatus(Request $request) { if (! $this->isSiteAdmin()) { abort(403, 'Only site admins can manage ticket statuses.'); } $validated = $request->validate([ 'name' => 'required|string|max:100', 'color' => 'required|string|regex:/^#[0-9a-fA-F]{6}$/', 'sort_order' => 'required|integer|min:0', 'is_closed' => 'boolean', ]); // Derive a slug from the name (lowercase, underscores) — must be unique $slug = \Str::slug($validated['name'], '_'); if (TicketStatus::where('slug', $slug)->exists()) { $slug = $slug . '_' . time(); } TicketStatus::create([ 'slug' => $slug, 'name' => $validated['name'], 'color' => $validated['color'], 'sort_order' => $validated['sort_order'], 'is_closed' => $request->boolean('is_closed', false), 'is_system' => false, ]); return back()->with('success', 'Status created.'); } public function updateStatus(Request $request, TicketStatus $status) { if (! $this->isSiteAdmin()) { abort(403, 'Only site admins can manage ticket statuses.'); } $validated = $request->validate([ 'name' => 'required|string|max:100', 'color' => 'required|string|regex:/^#[0-9a-fA-F]{6}$/', 'sort_order' => 'required|integer|min:0', 'is_closed' => 'boolean', ]); // System statuses: only allow name, color, sort_order to be changed — not is_closed $update = [ 'name' => $validated['name'], 'color' => $validated['color'], 'sort_order' => $validated['sort_order'], ]; if (! $status->is_system) { $update['is_closed'] = $request->boolean('is_closed', false); } $status->update($update); return back()->with('success', 'Status updated.'); } public function destroyStatus(TicketStatus $status) { if (! $this->isSiteAdmin()) { abort(403, 'Only site admins can manage ticket statuses.'); } if ($status->is_system) { return back()->withErrors(['status' => 'System statuses cannot be deleted.']); } if ($status->tickets()->exists()) { return back()->withErrors(['status' => 'Cannot delete a status that is in use by tickets.']); } $status->delete(); return back()->with('success', 'Status removed.'); } public function updateAutomation(Request $request) { if (! $this->isSiteAdmin()) { abort(403, 'Only site admins can manage automation settings.'); } $validated = $request->validate([ 'auto_close_days' => 'required|integer|min:0|max:365', ]); Setting::set('ticketing.auto_close_days', $validated['auto_close_days']); return back()->with('success', 'Automation settings saved.'); } }