resetPage(); } public function updatedStatusFilter(): void { $this->resetPage(); } public function boot(TicketService $ticketService, UserAppAccessService $appAccessService): void { $this->ticketService = $ticketService; $this->appAccessService = $appAccessService; } public function mount(): void { // Auto-open drawer if requested from URL if ($this->raise) { $this->drawerOpen = true; if ($this->appId) { $this->form->connectedAppId = $this->appId; } } // Pre-populate dynamic choice lists $this->searchApps(); $this->searchRoles(); $this->searchUsers(); $this->searchAssignableUsers(); // Select first role by default in notified roles array if empty $defaultRole = Role::where('name', 'Support')->first(); if ($defaultRole && empty($this->form->notifiedRoleIds)) { $this->form->notifiedRoleIds = [$defaultRole->id]; } // Auto-open ticket if requested from URL if ($this->ticketParam && (int) $this->ticketParam > 0) { $this->showTicket((int) $this->ticketParam); } } /** * Re-hydrate searchable arrays before every render to prevent losing selections. */ public function rendering(): void { if (empty($this->appsSearchable)) { $this->searchApps(); } if (empty($this->rolesSearchable)) { $this->searchRoles(); } if (empty($this->usersSearchable)) { $this->searchUsers(); } if (empty($this->assignableUsersSearchable)) { $this->searchAssignableUsers($this->assignableUsersSearchTerm); } } /** * Compute visible tickets based on permissions. */ #[Computed] public function tickets(): PaginatedDataCollection { $user = auth()->user(); $search = trim($this->search); $search = empty($search) ? null : $search; if ($user->hasRole(DefaultUserRole::Admin) || $user->can(TicketPermissionEnum::Read->value)) { return $this->ticketService->getList($search, $this->statusFilter, 10); } return $this->ticketService->getListForUser($user->id, $search, $this->statusFilter, 10); } /** * Compute expiring apps for the current user. */ #[Computed] public function eligibleApps(): Collection { $user = auth()->user(); if (!$user) { return collect(); } return $this->appAccessService->getActiveServices($user) ->filter(fn($app) => $app->isWarning); } /** * Compute available ticket statuses for filtering. */ #[Computed] public function statuses(): Collection { return TicketStatus::query()->get(); } /** * Search connected apps for mary-choices select dropdown. */ public function searchApps(string $value = ''): void { $selectedId = $this->form->connectedAppId; $apps = $this->eligibleApps(); if ($selectedId && !$apps->contains('id', $selectedId)) { $selectedApp = ConnectedApp::find($selectedId); if ($selectedApp) { $apps->push($selectedApp); } } $this->appsSearchable = array_values($apps ->when('' !== $value, fn($col) => $col->filter(fn($app) => str_contains(strtolower($app->name), strtolower($value)))) ->map(fn($app) => [ 'id' => $app->id, 'name' => $app->name.(isset($app->days_remaining) ? ' ('.$app->days_remaining.' days left)' : ''), ]) ->toArray()); } /** * Search roles for mary-choices select dropdown. */ public function searchRoles(string $value = ''): void { $selectedIds = $this->form->notifiedRoleIds; $this->rolesSearchable = array_values(Role::query() ->where(function ($query) use ($value, $selectedIds): void { if ('' !== $value) { $query->where('name', 'like', "%{$value}%"); } if (!empty($selectedIds)) { $query->orWhereIn('id', $selectedIds); } }) ->select('id', 'name') ->get() ->toArray()); } /** * Search users for mary-choices select dropdown. */ public function searchUsers(string $value = ''): void { $selectedIds = $this->form->notifiedUserIds; $this->usersSearchable = array_values(User::query() ->where('id', '!=', auth()->id()) ->where(function ($query) use ($value, $selectedIds): void { if ('' !== $value) { $query->where('name', 'like', "%{$value}%") ->orWhere('email', 'like', "%{$value}%"); } if (!empty($selectedIds)) { $query->orWhereIn('id', $selectedIds); } }) ->select('id', 'name') ->get() ->toArray()); } /** * Search assignable users dynamically with incremental pagination capability. */ public function searchAssignableUsers(string $value = ''): void { // Reset page limits if search keyword shifts if ($this->assignableUsersSearchTerm !== $value) { $this->assignableUsersSearchTerm = $value; $this->assignableUsersPerPage = 10; } $query = User::query(); $currentAssigneeId = $this->assignedUserId; if (is_array($currentAssigneeId)) { $currentAssigneeId = !empty($currentAssigneeId) ? $currentAssigneeId[0] : null; } if ('' !== $value) { $query->where(function ($q) use ($value) { $q->where('name', 'like', "%{$value}%") ->orWhere('email', 'like', "%{$value}%"); }); if ($currentAssigneeId && (int) $currentAssigneeId > 0) { $query->orWhere('id', (int) $currentAssigneeId); } } // Track total count matching query criteria to determine if "Load More" exists $totalMatches = $query->count(); $this->assignableUsersHasMore = $totalMatches > $this->assignableUsersPerPage; $users = $query ->select('id', 'name') ->limit($this->assignableUsersPerPage) ->get(); // If there is no search term and we have an assigned user, ensure they are in the list if ('' === $value && $currentAssigneeId && (int) $currentAssigneeId > 0) { if (!$users->contains('id', (int) $currentAssigneeId)) { $assignedUser = User::find($currentAssigneeId); if ($assignedUser) { $users->push($assignedUser); } } } $this->assignableUsersSearchable = $users ->map(fn($u) => ['id' => $u->id, 'name' => $u->name]) ->toArray(); } /** * Increments the page window for assignable users dropdown. */ public function loadMoreAssignableUsers(): void { $this->assignableUsersPerPage += 10; $this->searchAssignableUsers($this->assignableUsersSearchTerm); } /** * Select all matching assignable users based on current search context. */ public function selectAllAssignableUsers(): void { // Pluck IDs matching current search context criteria $matchedIds = User::query() ->when('' !== $this->assignableUsersSearchTerm, function ($q) { $q->where('name', 'like', "%{$this->assignableUsersSearchTerm}%") ->orWhere('email', 'like', "%{$this->assignableUsersSearchTerm}%"); }) ->pluck('id') ->toArray(); // Assign the first matched record or pass the array depending on single/multiple context. // Since updateAssignment expects a single nullable ID, we take the primary match or convert accordingly. $this->assignedUserId = !empty($matchedIds) ? (int) $matchedIds[0] : null; $this->updateAssignment(); } /** * Clear assignment selection. */ public function clearAssignableUsers(): void { $this->assignedUserId = null; $this->updateAssignment(); } public function selectAllRoles(): void { $this->form->notifiedRoleIds = Role::query()->pluck('id')->toArray(); $this->searchRoles(); } public function clearRoles(): void { $this->form->notifiedRoleIds = []; } public function selectAllUsers(): void { $this->form->notifiedUserIds = User::query() ->where('id', '!=', auth()->id()) ->pluck('id') ->toArray(); $this->searchUsers(); } public function clearUsers(): void { $this->form->notifiedUserIds = []; } public function saveTicket(): void { $this->form->validate(); $dto = new CreateTicketRequest( userId: (int) auth()->id(), connectedAppId: $this->form->connectedAppId ? (int) $this->form->connectedAppId : null, issueCategory: $this->form->issueCategory, description: $this->form->description ? trim($this->form->description) : null, notifiedRoleIds: $this->form->routingType === 'role' ? array_map('intval', $this->form->notifiedRoleIds) : [], notifiedUserIds: $this->form->routingType === 'user' ? array_map('intval', $this->form->notifiedUserIds) : [], attachment: $this->form->attachment, ); $this->attempt( action: function () use ($dto): void { $this->ticketService->create($dto); $this->form->reset(); $this->drawerOpen = false; $this->raise = false; $this->appId = null; $this->searchApps(); $this->searchRoles(); $this->searchUsers(); }, successMessage: 'Support Ticket has been raised successfully!' ); } public function showTicket(int $id): void { $this->attempt( action: function () use ($id): void { $this->selectedTicketId = $id; $this->selectedTicket = $this->ticketService->getTicket($id); $this->assignedUserId = $this->selectedTicket->assignedUserId; $this->replyMessage = ''; // Keep assignment lookup synchronized upon details injection $this->searchAssignableUsers(); $this->detailsModalOpen = true; }, showSuccess: false ); } public function changeStatus(int $ticketId, string $statusSlug): void { $this->attempt( action: function () use ($ticketId, $statusSlug): void { $this->ticketService->updateStatus($ticketId, $statusSlug); if ($this->selectedTicketId === $ticketId) { $this->selectedTicket = $this->ticketService->getTicket($ticketId); } }, successMessage: 'Ticket status updated successfully!' ); } public function saveReply(): void { $this->validate(['replyMessage' => 'required|string|min:1']); $this->attempt( action: function (): void { $dto = new PostReplyRequest( ticketId: $this->selectedTicketId, userId: (int) auth()->id(), message: $this->replyMessage, ); $this->ticketService->postReply($dto); $this->replyMessage = ''; $this->selectedTicket = $this->ticketService->getTicket($this->selectedTicketId); }, successMessage: 'Comment posted successfully!' ); } public function updateAssignment(): void { $this->attempt( action: function (): void { $assignedId = $this->assignedUserId; if (is_array($assignedId)) { $assignedId = !empty($assignedId) ? $assignedId[0] : null; } $finalAssignedUserId = ($assignedId && (int) $assignedId > 0) ? (int) $assignedId : null; $this->ticketService->assignUser( $this->selectedTicketId, $finalAssignedUserId ); $this->selectedTicket = $this->ticketService->getTicket($this->selectedTicketId); // Refresh list options with correct paginated users $this->searchAssignableUsers($this->assignableUsersSearchTerm); }, successMessage: 'Ticket assignment updated successfully!' ); } public function updatedAssignedUserId($value): void { $this->updateAssignment(); } #[Computed] public function canComment(): bool { if (!$this->selectedTicket) { return false; } $user = auth()->user(); if (!$user) { return false; } if ($user->hasRole(DefaultUserRole::Admin)) { return true; } if ($this->selectedTicket->userId === $user->id) { return true; } if ($this->selectedTicket->assignedUserId === $user->id) { return true; } $ticket = Ticket::find($this->selectedTicketId); if ($ticket) { if ($ticket->notifiedUsers()->where('users.id', $user->id)->exists()) { return true; } if ($ticket->notifiedRoles()->whereIn('roles.id', $user->roles->pluck('id')->toArray())->exists()) { return true; } } return false; } public function toggleDrawer(): void { $this->drawerOpen = !$this->drawerOpen; if (!$this->drawerOpen) { $this->reset(['raise', 'appId']); $this->form->reset(); } } }; ?>
@if(!auth()->user()->hasRole(DefaultUserRole::Admin) && $this->eligibleApps->isEmpty()) You do not currently have any active services expiring within 3 days. You can still submit a general inquiry ticket without choosing a service. @endif Raise Ticket @if($this->tickets->items()->isEmpty())
@if(!empty(trim($search)) || $statusFilter !== 'all')

No Matching Tickets Found

We couldn't find any support tickets matching your search query or filter criteria. Try adjusting your search term or status filter.

@else

No Support Tickets Found

You have not raised any support tickets yet. Click "Raise Ticket" to submit your renewal queries.

@endif
@else @scope('cell_ticketId', $row) @endscope @scope('cell_appName', $row) @if($row->appName) {{ $row->appName }} @else General Support @endif @endscope @scope('cell_statusName', $row) @php $badgeClass = match($row->status) { TicketStatusEnum::Open => 'badge-warning badge-soft', TicketStatusEnum::InProgress => 'badge-info badge-soft', TicketStatusEnum::Resolved => 'badge-success badge-soft', TicketStatusEnum::Closed => 'badge-neutral badge-soft', TicketStatusEnum::Hold => 'badge-warning badge-soft', default => 'badge-ghost', }; @endphp {{ $row->status->label() }} @endscope @scope('cell_createdAt', $row) {{ Carbon\Carbon::parse($row->createdAt)->diffForHumans() }} @endscope @scope('actions', $row)
@if(auth()->user()->hasRole('Admin') || auth()->user()->hasRole('admin') || auth()->user()->can('ticket:update') || $row->assignedUserId === auth()->id()) @foreach($this->statuses as $status) @php $enumCase = TicketStatusEnum::tryFrom($status->name); @endphp @if($enumCase && $enumCase !== $row->status) @endif @endforeach @endif
@endscope
@endif
@if($form->issueCategory === 'Others')
@if($form->attachment)
File successfully uploaded.
@endif
@else
@endif
@if($form->routingType === 'role')
@else
@endif
@if($selectedTicket)
Support Ticket

{{ $selectedTicket->ticketId }}

@php $badgeClass = match($selectedTicket->status) { TicketStatusEnum::Open => 'badge-warning badge-soft', TicketStatusEnum::InProgress => 'badge-info badge-soft', TicketStatusEnum::Resolved => 'badge-success badge-soft', TicketStatusEnum::Closed => 'badge-neutral badge-soft', TicketStatusEnum::Hold => 'badge-warning badge-soft', default => 'badge-ghost', }; @endphp
{{ $selectedTicket->status->label() }} Raised {{ Carbon\Carbon::parse($selectedTicket->createdAt)->diffForHumans() }}
Customer

{{ $selectedTicket->userName }}

Associated Service

{{ $selectedTicket->appName ?? 'General Inquiry' }}

Issue Category

{{ $selectedTicket->issueCategory }}

Assigned Support Saving...
@if(auth()->user()->hasRole(DefaultUserRole::Admin))
@else @if($selectedTicket->assignedUserName)
{{ $selectedTicket->assignedUserName }}
@else Unassigned @endif @endif
Notified Recipient(s) @if(!empty($selectedTicket->notifiedUserNames))
Users: @foreach($selectedTicket->notifiedUserNames as $name) @endforeach
@endif @if(!empty($selectedTicket->notifiedRoleNames))
Roles: @foreach($selectedTicket->notifiedRoleNames as $name) @endforeach
@endif @if(empty($selectedTicket->notifiedUserNames) && empty($selectedTicket->notifiedRoleNames))

None specified

@endif
Description Details @php $description = $selectedTicket->description ?? 'No custom details provided.'; $words = preg_split('/\s+/', trim($description)); $wordCount = count($words); $isLong = $wordCount > 50; if ($isLong) { $shortDescription = implode(' ', array_slice($words, 0, 50)) . '...'; } else { $shortDescription = $description; } @endphp

{{ $shortDescription }}

@if($isLong)

{{ $description }}

@endif
@if(!empty($selectedTicket->attachments))
Attachments
@foreach($selectedTicket->attachments as $attachment)
@php $icon = match(true) { Str::contains($attachment['mime_type'], 'pdf') => 'lucide.file-text', Str::contains($attachment['mime_type'], 'image') => 'lucide.image', default => 'lucide.file', }; @endphp

{{ $attachment['file_name'] }}

{{ round($attachment['file_size'] / 1024, 1) }} KB

@endforeach
@endif @if(auth()->user()->hasRole('Admin') || auth()->user()->hasRole('admin') || auth()->user()->can('ticket:update') || auth()->id() === $selectedTicket->assignedUserId)
Change Status
@foreach($this->statuses as $status) @php $enumCase = TicketStatusEnum::tryFrom($status->name); @endphp @if($enumCase && $enumCase !== $selectedTicket->status) @endif @endforeach
@endif

Discussion Comments

{{ count($selectedTicket->replies) }} comments
@if($this->canComment)
@if(empty($selectedTicket->replies))

No comments yet

Be the first to start the conversation on this support ticket.

@else @foreach($selectedTicket->replies as $reply)
{{ $reply['userInitials'] }}
{{ $reply['userName'] }}
{{ $reply['createdAt'] }}

{{ $reply['message'] }}

@endforeach @endif
@else

Conversation Access Restricted

Only raised/tagged participants (ticket owner, assigned support, directly notified users, or notified-role members) have access to the discussion feed.

@endif
@endif