loadLogs(false); } protected function getAgent(): AgentClient { return $this->agent ??= new AgentClient; } public function loadLogs(bool $refreshTable = true): void { try { $result = $this->getAgent()->send('email.get_logs', [ 'limit' => 200, ]); $this->logs = $result['logs'] ?? []; $this->logsLoaded = true; } catch (\Exception $e) { $this->logs = []; $this->logsLoaded = true; Notification::make() ->title(__('Failed to load email logs')) ->body($e->getMessage()) ->danger() ->send(); } if ($refreshTable) { $this->resetTable(); } } public function loadQueue(bool $refreshTable = true): void { try { $result = $this->getAgent()->send('mail.queue_list'); $this->queueItems = $result['queue'] ?? []; $this->queueLoaded = true; } catch (\Exception $e) { $this->queueItems = []; $this->queueLoaded = true; Notification::make() ->title(__('Failed to load mail queue')) ->body($e->getMessage()) ->danger() ->send(); } if ($refreshTable) { $this->resetTable(); } } public function setViewMode(string $mode): void { $mode = in_array($mode, ['logs', 'queue'], true) ? $mode : 'logs'; if ($this->viewMode === $mode) { return; } $this->viewMode = $mode; if ($mode === 'queue') { $this->loadQueue(false); } else { $this->loadLogs(false); } $this->resetTable(); } public function table(Table $table): Table { return $table ->paginated([25, 50, 100]) ->defaultPaginationPageOption(25) ->records(function (?array $filters, ?string $search, int|string $page, int|string $recordsPerPage, ?string $sortColumn, ?string $sortDirection) { if ($this->viewMode === 'queue') { if (! $this->queueLoaded) { $this->loadQueue(false); } $records = $this->queueItems; } else { if (! $this->logsLoaded) { $this->loadLogs(false); } $records = $this->logs; } $records = $this->filterRecords($records, $search); $records = $this->sortRecords($records, $sortColumn, $sortDirection); return $this->paginateRecords($records, $page, $recordsPerPage); }) ->columns($this->viewMode === 'queue' ? $this->getQueueColumns() : $this->getLogColumns()) ->recordActions($this->viewMode === 'queue' ? $this->getQueueActions() : []) ->emptyStateHeading($this->viewMode === 'queue' ? __('Mail queue is empty') : __('No email logs found')) ->emptyStateDescription($this->viewMode === 'queue' ? __('No deferred messages found.') : __('Mail logs are empty or unavailable.')) ->headerActions([ Action::make('viewLogs') ->label(__('Logs')) ->color($this->viewMode === 'logs' ? 'primary' : 'gray') ->action(fn () => $this->setViewMode('logs')), Action::make('viewQueue') ->label(__('Queue')) ->color($this->viewMode === 'queue' ? 'primary' : 'gray') ->action(fn () => $this->setViewMode('queue')), Action::make('refresh') ->label(__('Refresh')) ->icon('heroicon-o-arrow-path') ->action(function (): void { if ($this->viewMode === 'queue') { $this->loadQueue(); } else { $this->loadLogs(); } }), ]); } protected function getLogColumns(): array { return [ TextColumn::make('timestamp') ->label(__('Time')) ->formatStateUsing(function (array $record): string { $timestamp = (int) ($record['timestamp'] ?? 0); return $timestamp > 0 ? date('Y-m-d H:i:s', $timestamp) : ''; }) ->sortable(), TextColumn::make('queue_id') ->label(__('Queue ID')) ->fontFamily('mono') ->copyable() ->toggleable(), TextColumn::make('component') ->label(__('Component')) ->toggleable(isToggledHiddenByDefault: true), TextColumn::make('from') ->label(__('From')) ->wrap() ->searchable(), TextColumn::make('to') ->label(__('To')) ->wrap() ->searchable(), TextColumn::make('status') ->label(__('Status')) ->badge() ->formatStateUsing(fn (array $record): string => (string) ($record['status'] ?? 'unknown')), TextColumn::make('relay') ->label(__('Relay')) ->toggleable(), TextColumn::make('message') ->label(__('Details')) ->wrap() ->limit(80) ->toggleable(isToggledHiddenByDefault: true), ]; } protected function getQueueColumns(): array { return [ TextColumn::make('id') ->label(__('Queue ID')) ->fontFamily('mono') ->copyable(), TextColumn::make('arrival') ->label(__('Arrival')), TextColumn::make('sender') ->label(__('Sender')) ->wrap() ->searchable(), TextColumn::make('recipients') ->label(__('Recipients')) ->formatStateUsing(function ($state): string { if (is_array($state)) { $recipients = $state; } elseif ($state === null || $state === '') { $recipients = []; } else { $recipients = [(string) $state]; } $recipients = array_values(array_filter(array_map(function ($recipient): ?string { if (is_array($recipient)) { return (string) ($recipient['address'] ?? $recipient['recipient'] ?? $recipient['email'] ?? $recipient[0] ?? ''); } return $recipient === null ? null : (string) $recipient; }, $recipients), static fn (?string $value): bool => $value !== null && $value !== '')); if (empty($recipients)) { return __('Unknown'); } $first = $recipients[0] ?? ''; $count = count($recipients); return $count > 1 ? $first.' +'.($count - 1) : $first; }) ->wrap(), TextColumn::make('size') ->label(__('Size')) ->formatStateUsing(fn ($state): string => is_scalar($state) ? (string) $state : ''), TextColumn::make('status') ->label(__('Status')) ->wrap(), ]; } protected function getQueueActions(): array { return [ Action::make('retry') ->label(__('Retry')) ->icon('heroicon-o-arrow-path') ->color('info') ->action(function (array $record): void { try { $result = $this->getAgent()->send('mail.queue_retry', ['id' => $record['id'] ?? '']); if ($result['success'] ?? false) { Notification::make()->title(__('Message retried'))->success()->send(); $this->loadQueue(); } else { throw new \Exception($result['error'] ?? __('Failed to retry message')); } } catch (\Exception $e) { Notification::make()->title(__('Retry failed'))->body($e->getMessage())->danger()->send(); } }), Action::make('delete') ->label(__('Delete')) ->icon('heroicon-o-trash') ->color('danger') ->requiresConfirmation() ->action(function (array $record): void { try { $result = $this->getAgent()->send('mail.queue_delete', ['id' => $record['id'] ?? '']); if ($result['success'] ?? false) { Notification::make()->title(__('Message deleted'))->success()->send(); $this->loadQueue(); } else { throw new \Exception($result['error'] ?? __('Failed to delete message')); } } catch (\Exception $e) { Notification::make()->title(__('Delete failed'))->body($e->getMessage())->danger()->send(); } }), ]; } protected function filterRecords(array $records, ?string $search): array { $search = trim((string) $search); if ($search === '') { return $records; } $search = Str::lower($search); return array_values(array_filter($records, function (array $record) use ($search): bool { if ($this->viewMode === 'queue') { $recipients = $record['recipients'] ?? []; $haystack = implode(' ', array_filter([ (string) ($record['id'] ?? ''), (string) ($record['sender'] ?? ''), implode(' ', $recipients), (string) ($record['status'] ?? ''), ])); } else { $haystack = implode(' ', array_filter([ (string) ($record['queue_id'] ?? ''), (string) ($record['from'] ?? ''), (string) ($record['to'] ?? ''), (string) ($record['status'] ?? ''), (string) ($record['message'] ?? ''), (string) ($record['component'] ?? ''), ])); } return str_contains(Str::lower($haystack), $search); })); } protected function sortRecords(array $records, ?string $sortColumn, ?string $sortDirection): array { $direction = $sortDirection === 'asc' ? 'asc' : 'desc'; if (! $sortColumn) { return $records; } usort($records, function (array $a, array $b) use ($sortColumn, $direction): int { $aValue = $a[$sortColumn] ?? null; $bValue = $b[$sortColumn] ?? null; if (is_numeric($aValue) && is_numeric($bValue)) { $result = (float) $aValue <=> (float) $bValue; } else { $result = strcmp((string) $aValue, (string) $bValue); } return $direction === 'asc' ? $result : -$result; }); return $records; } protected function paginateRecords(array $records, int|string $page, int|string $recordsPerPage): LengthAwarePaginator { $page = max(1, (int) $page); $perPage = max(1, (int) $recordsPerPage); $total = count($records); $items = array_slice($records, ($page - 1) * $perPage, $perPage); return new LengthAwarePaginator( $items, $total, $perPage, $page, [ 'path' => request()->url(), 'pageName' => $this->getTablePaginationPageName(), ], ); } }