ticketService = $ticketService; $this->appAccessService = $appAccessService; } public function mount(): void { // Pre-populate dynamic choice lists $this->searchApps(); $this->searchRoles(); $this->searchUsers(); // 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 drawer if requested from URL if ($this->raise) { $this->drawerOpen = true; if ($this->appId) { $this->form->connectedAppId = $this->appId; } } } /** * 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(); } } /** * Compute visible tickets based on permissions. */ #[Computed] public function tickets(): Collection { $user = auth()->user(); if (!$user) { return collect(); } if ($user->hasRole('Admin') || $user->can(TicketPermissionEnum::Read->value)) { $tickets = $this->ticketService->getAll(); if ($this->statusFilter !== 'all') { $tickets = $tickets->filter(fn($t) => $t->statusSlug === $this->statusFilter); } return $tickets; } return $this->ticketService->getAllForUser($user->id); } /** * 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->is_warning); } /** * 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(); // Ensure selected app is preserved in the list even if warning state changed or not in initial list if ($selectedId && !$apps->contains('id', $selectedId)) { $selectedApp = ConnectedApp::find($selectedId); if ($selectedApp) { $apps->push($selectedApp); } } $this->appsSearchable = $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 = 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 = 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(); } /** * Select all roles in choices element. */ public function selectAllRoles(): void { $this->form->notifiedRoleIds = Role::query() ->pluck('id') ->toArray(); $this->searchRoles(); } /** * Clear all selected roles in choices element. */ public function clearRoles(): void { $this->form->notifiedRoleIds = []; } /** * Select all users in choices element. */ public function selectAllUsers(): void { $this->form->notifiedUserIds = User::query() ->where('id', '!=', auth()->id()) ->pluck('id') ->toArray(); $this->searchUsers(); } /** * Clear all selected users in choices element. */ public function clearUsers(): void { $this->form->notifiedUserIds = []; } /** * Submit and save support ticket using the new Livewire Form class. */ 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); // Reset form state & close drawer $this->form->reset(); $this->drawerOpen = false; $this->raise = false; $this->appId = null; // Re-hydrate choice lists $this->searchApps(); $this->searchRoles(); $this->searchUsers(); }, successMessage: 'Support Ticket has been raised successfully!' ); } /** * Open Ticket Details modal. */ public function showTicket(int $id): void { $this->attempt( action: function () use ($id): void { $this->selectedTicketId = $id; $this->selectedTicket = $this->ticketService->getTicket($id); $this->detailsModalOpen = true; }, showSuccess: false ); } /** * Update ticket status (Admin/Support only). */ public function changeStatus(int $ticketId, string $statusSlug): void { $this->attempt( action: function () use ($ticketId, $statusSlug): void { $this->ticketService->updateStatus($ticketId, $statusSlug); // Refresh active selected ticket details if open if ($this->selectedTicketId === $ticketId) { $this->selectedTicket = $this->ticketService->getTicket($ticketId); } }, successMessage: 'Ticket status updated successfully!' ); } /** * Toggle Drawer manually. */ public function toggleDrawer(): void { $this->drawerOpen = !$this->drawerOpen; if (!$this->drawerOpen) { $this->reset(['raise', 'appId']); $this->form->reset(); } } }; ?>
@if(auth()->user()->hasRole('Admin') || auth()->user()->can('ticket:read')) @endif Raise Ticket @if($this->tickets->isEmpty())

No Support Tickets Found

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

@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['statusSlug']) { 'open' => 'badge-warning badge-soft', 'in-progress' => 'badge-info badge-soft', 'resolved' => 'badge-success badge-soft', 'closed' => 'badge-neutral badge-soft', default => 'badge-ghost', }; @endphp {{ $row['statusName'] }} @endscope @scope('cell_createdAt', $row) {{ Carbon\Carbon::parse($row['createdAt'])->diffForHumans() }} @endscope @scope('actions', $row)
@if(auth()->user()->hasRole('Admin') || auth()->user()->can('ticket:update')) @foreach($this->statuses as $status) @if($status->slug !== $row['statusSlug']) @endif @endforeach @endif
@endscope
@endif
@if($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 @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->statusSlug) { 'open' => 'badge-warning badge-soft', 'in-progress' => 'badge-info badge-soft', 'resolved' => 'badge-success badge-soft', 'closed' => 'badge-neutral badge-soft', default => 'badge-ghost', }; @endphp
{{ $selectedTicket->statusName }} Raised {{ Carbon\Carbon::parse($selectedTicket->createdAt)->diffForHumans() }}
Customer

{{ $selectedTicket->userName }}

Associated Service

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

Issue Category

{{ $selectedTicket->issueCategory }}

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

{{ $selectedTicket->description ?? 'No custom details provided.' }}

@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()->can('ticket:update'))
Change Status
@foreach($this->statuses as $status) @if($status->slug !== $selectedTicket->statusSlug) @endif @endforeach
@endif
@endif