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(),
];
}
}