mtUtil = $mtUtil; } protected function canAccess(): void { if (! auth()->user()->can('managementtools.ticket_own') && ! auth()->user()->can('managementtools.ticket_manage')) { abort(403, 'Unauthorized action.'); } } protected function canManage(): bool { return auth()->user()->can('managementtools.ticket_manage'); } protected function ticketQuery(int $businessId) { $query = MtUserTicket::forBusiness($businessId) ->with(['user', 'assignee']) ->select('mt_user_tickets.*'); if (! $this->canManage()) { $query->where(function ($q) { $q->where('user_id', auth()->id()) ->orWhere('assigned_to', auth()->id()); }); } return $query; } protected function findTicket(int $businessId, int $id): MtUserTicket { $ticket = MtUserTicket::forBusiness($businessId)->findOrFail($id); if (! $this->canManage() && (int) $ticket->user_id !== (int) auth()->id() && (int) $ticket->assigned_to !== (int) auth()->id()) { abort(403, 'Unauthorized action.'); } return $ticket; } public function index(Request $request) { $this->canAccess(); $businessId = (int) $request->session()->get('user.business_id'); if ($request->ajax()) { $tickets = $this->ticketQuery($businessId)->orderByDesc('updated_at'); if ($request->filled('status') && $request->status !== 'all') { $tickets->where('status', (int) $request->status); } if ($request->filled('priority') && $request->priority !== 'all') { $tickets->where('priority', $request->priority); } if ($this->canManage() && $request->filled('user_id')) { $tickets->where('user_id', (int) $request->user_id); } return DataTables::of($tickets) ->addColumn('reference', fn ($row) => $row->reference) ->addColumn('requester', fn ($row) => $row->user ? trim($row->user->first_name.' '.($row->user->last_name ?? '')) : '—') ->addColumn('assignee_name', fn ($row) => $row->assignee ? trim($row->assignee->first_name.' '.($row->assignee->last_name ?? '')) : '—') ->addColumn('status_label', function ($row) { return ''.$row->statusLabel().''; }) ->addColumn('priority_label', function ($row) { return ''.$row->priorityLabel().''; }) ->addColumn('action', function ($row) { $show = action([self::class, 'show'], [$row->id]); return ' '.__('managementtools::lang.view').''; }) ->editColumn('subject', fn ($row) => e($row->subject)) ->editColumn('updated_at', fn ($row) => $this->mtUtil->formatDateTime($row->updated_at)) ->rawColumns(['status_label', 'priority_label', 'action']) ->make(true); } $stats = $this->buildStats($businessId); $users = $this->canManage() ? User::where('business_id', $businessId)->user()->pluck('first_name', 'id') : collect(); return view('managementtools::tickets.index', [ 'stats' => $stats, 'statuses' => MtUserTicket::statuses(), 'priorities' => MtUserTicket::priorities(), 'users' => $users, 'can_manage' => $this->canManage(), ]); } public function create() { $this->canAccess(); if (! auth()->user()->can('managementtools.ticket_own')) { abort(403, 'Unauthorized action.'); } $businessId = (int) request()->session()->get('user.business_id'); $recipients = User::where('business_id', $businessId) ->user() ->where('allow_login', 1) ->where('id', '!=', auth()->id()) ->whereNull('deleted_at') ->orderBy('first_name') ->get() ->mapWithKeys(function ($u) { return [$u->id => trim($u->first_name.' '.($u->last_name ?? ''))]; }); return view('managementtools::tickets.create', [ 'priorities' => MtUserTicket::priorities(), 'recipients' => $recipients, ]); } public function store(Request $request) { $this->canAccess(); if (! auth()->user()->can('managementtools.ticket_own')) { abort(403, 'Unauthorized action.'); } $businessId = (int) $request->session()->get('user.business_id'); $request->validate([ 'subject' => 'required|string|max:255', 'body' => 'required|string|max:5000', 'priority' => 'required|in:low,normal,high,urgent', 'assigned_to' => 'nullable|integer|exists:users,id', ]); $ticket = MtUserTicket::create([ 'business_id' => $businessId, 'user_id' => auth()->id(), 'subject' => $request->subject, 'body' => $request->body, 'status' => MtUserTicket::STATUS_OPEN, 'priority' => $request->priority, 'assigned_to' => $request->input('assigned_to') ?: null, 'is_read_by_admin' => false, 'is_read_by_user' => true, ]); if ($request->ajax() || $request->wantsJson()) { return response()->json([ 'success' => true, 'msg' => __('managementtools::lang.ticket_created'), 'redirect' => action([self::class, 'show'], [$ticket->id]), ]); } return redirect()->action([self::class, 'show'], [$ticket->id]) ->with('status', ['success' => 1, 'msg' => __('managementtools::lang.ticket_created')]); } public function show(Request $request, $id) { $this->canAccess(); $businessId = (int) $request->session()->get('user.business_id'); $ticket = $this->findTicket($businessId, $id); $ticket->load(['user', 'assignee', 'replies.user']); if ($this->canManage()) { $ticket->update(['is_read_by_admin' => true]); } elseif ((int) $ticket->user_id === (int) auth()->id() || (int) $ticket->assigned_to === (int) auth()->id()) { $ticket->update(['is_read_by_user' => true]); } $agents = $this->canManage() ? User::where('business_id', $businessId)->user()->get() : collect(); return view('managementtools::tickets.show', [ 'ticket' => $ticket, 'statuses' => MtUserTicket::statuses(), 'priorities' => MtUserTicket::priorities(), 'agents' => $agents, 'can_manage' => $this->canManage(), ]); } public function reply(Request $request, $id) { $this->canAccess(); $businessId = (int) $request->session()->get('user.business_id'); $ticket = $this->findTicket($businessId, $id); if (in_array((int) $ticket->status, [MtUserTicket::STATUS_CLOSED], true)) { return response()->json(['success' => false, 'msg' => __('managementtools::lang.ticket_closed_no_reply')]); } $request->validate([ 'body' => 'required_without:attachment|nullable|string|max:5000', 'attachment' => 'nullable|file|max:5120|mimes:jpg,jpeg,png,gif,pdf,doc,docx,xls,xlsx,zip', ]); $attachmentPath = null; $attachmentType = null; if ($request->hasFile('attachment')) { $file = $request->file('attachment'); $attachmentType = $file->getClientMimeType(); $attachmentPath = $file->store('tickets/'.$businessId, 'public'); } MtUserTicketReply::create([ 'business_id' => $businessId, 'ticket_id' => $ticket->id, 'user_id' => auth()->id(), 'body' => $request->input('body'), 'attachment' => $attachmentPath, 'attachment_type' => $attachmentType, ]); $updates = ['updated_at' => now()]; if ($this->canManage()) { $updates['is_read_by_user'] = false; if ((int) $ticket->status === MtUserTicket::STATUS_OPEN) { $updates['status'] = MtUserTicket::STATUS_IN_PROGRESS; } } else { $updates['is_read_by_admin'] = false; } $ticket->update($updates); return response()->json(['success' => true, 'msg' => __('managementtools::lang.ticket_reply_sent')]); } public function update(Request $request, $id) { $this->canAccess(); if (! $this->canManage()) { abort(403, 'Unauthorized action.'); } $businessId = (int) $request->session()->get('user.business_id'); $ticket = $this->findTicket($businessId, $id); $request->validate([ 'status' => 'nullable|integer|in:0,1,2,3', 'priority' => 'nullable|in:low,normal,high,urgent', 'assigned_to' => 'nullable|integer|exists:users,id', ]); $ticket->update(array_filter([ 'status' => $request->has('status') ? (int) $request->status : $ticket->status, 'priority' => $request->input('priority', $ticket->priority), 'assigned_to' => $request->input('assigned_to') ?: null, 'is_read_by_user' => false, ], fn ($v) => $v !== null)); return response()->json(['success' => true, 'msg' => __('managementtools::lang.ticket_updated')]); } public function destroy($id) { $this->canAccess(); if (! $this->canManage()) { abort(403, 'Unauthorized action.'); } $businessId = (int) request()->session()->get('user.business_id'); $ticket = $this->findTicket($businessId, $id); foreach ($ticket->replies as $reply) { if ($reply->attachment) { Storage::disk('public')->delete($reply->attachment); } } $ticket->delete(); return response()->json(['success' => true, 'msg' => __('managementtools::lang.ticket_deleted')]); } protected function buildStats(int $businessId): array { $base = $this->ticketQuery($businessId); return [ 'open' => (clone $base)->where('status', MtUserTicket::STATUS_OPEN)->count(), 'in_progress' => (clone $base)->where('status', MtUserTicket::STATUS_IN_PROGRESS)->count(), 'resolved' => (clone $base)->where('status', MtUserTicket::STATUS_RESOLVED)->count(), 'closed' => (clone $base)->where('status', MtUserTicket::STATUS_CLOSED)->count(), 'total' => (clone $base)->count(), ]; } }