exempt(ActionSurfaceSlot::ListHeader, 'Directory groups list intentionally has no header actions; sync is started from the sync-runs surface.') ->satisfy(ActionSurfaceSlot::InspectAffordance, ActionSurfaceInspectAffordance::ClickableRow->value) ->exempt(ActionSurfaceSlot::ListRowMoreMenu, 'No secondary row actions are provided on this read-only list.') ->exempt(ActionSurfaceSlot::ListBulkMoreGroup, 'Bulk actions are intentionally omitted for directory groups.') ->exempt(ActionSurfaceSlot::ListEmptyState, 'Empty-state CTA is intentionally omitted; groups appear after sync.'); } public static function form(Schema $schema): Schema { return $schema; } public static function infolist(Schema $schema): Schema { return $schema ->schema([ Section::make('Group') ->schema([ TextEntry::make('display_name')->label('Name'), TextEntry::make('entra_id')->label('Entra ID')->copyable(), TextEntry::make('type') ->badge() ->state(fn (EntraGroup $record): string => static::groupTypeLabel(static::groupType($record))), TextEntry::make('security_enabled') ->label('Security') ->badge() ->formatStateUsing(BadgeRenderer::label(BadgeDomain::BooleanEnabled)) ->color(BadgeRenderer::color(BadgeDomain::BooleanEnabled)) ->icon(BadgeRenderer::icon(BadgeDomain::BooleanEnabled)) ->iconColor(BadgeRenderer::iconColor(BadgeDomain::BooleanEnabled)), TextEntry::make('mail_enabled') ->label('Mail') ->badge() ->formatStateUsing(BadgeRenderer::label(BadgeDomain::BooleanEnabled)) ->color(BadgeRenderer::color(BadgeDomain::BooleanEnabled)) ->icon(BadgeRenderer::icon(BadgeDomain::BooleanEnabled)) ->iconColor(BadgeRenderer::iconColor(BadgeDomain::BooleanEnabled)), TextEntry::make('last_seen_at')->label('Last seen')->dateTime()->placeholder('—'), ]) ->columns(2) ->columnSpanFull(), Section::make('Raw groupTypes') ->schema([ ViewEntry::make('group_types') ->label('') ->view('filament.infolists.entries.snapshot-json') ->state(fn (EntraGroup $record) => $record->group_types ?? []) ->columnSpanFull(), ]) ->columnSpanFull(), ]); } public static function table(Table $table): Table { return $table ->defaultSort('display_name') ->modifyQueryUsing(function (Builder $query): Builder { $tenantId = Tenant::current()?->getKey(); return $query->when($tenantId, fn (Builder $q) => $q->where('tenant_id', $tenantId)); }) ->recordUrl(static fn (EntraGroup $record): ?string => static::canView($record) ? static::getUrl('view', ['record' => $record]) : null) ->columns([ Tables\Columns\TextColumn::make('display_name') ->label('Name') ->searchable(), Tables\Columns\TextColumn::make('entra_id') ->label('Entra ID') ->copyable() ->toggleable(), Tables\Columns\TextColumn::make('type') ->label('Type') ->badge() ->state(fn (EntraGroup $record): string => static::groupTypeLabel(static::groupType($record))) ->color(fn (EntraGroup $record): string => static::groupTypeColor(static::groupType($record))), Tables\Columns\TextColumn::make('last_seen_at') ->label('Last seen') ->since(), ]) ->filters([ SelectFilter::make('stale') ->label('Stale') ->options([ '1' => 'Stale', '0' => 'Fresh', ]) ->query(function (Builder $query, array $data): Builder { $value = $data['value'] ?? null; if ($value === null || $value === '') { return $query; } $stalenessDays = (int) config('directory_groups.staleness_days', 30); $cutoff = now('UTC')->subDays(max(1, $stalenessDays)); if ((string) $value === '1') { return $query->where(function (Builder $q) use ($cutoff): void { $q->whereNull('last_seen_at') ->orWhere('last_seen_at', '<', $cutoff); }); } return $query->where('last_seen_at', '>=', $cutoff); }), SelectFilter::make('group_type') ->label('Type') ->options([ 'security' => 'Security', 'microsoft365' => 'Microsoft 365', 'mail' => 'Mail-enabled', 'unknown' => 'Unknown', ]) ->query(function (Builder $query, array $data): Builder { $value = (string) ($data['value'] ?? ''); if ($value === '') { return $query; } return match ($value) { 'microsoft365' => $query->whereJsonContains('group_types', 'Unified'), 'security' => $query ->where('security_enabled', true) ->where(function (Builder $q): void { $q->whereNull('group_types') ->orWhereJsonDoesntContain('group_types', 'Unified'); }), 'mail' => $query ->where('mail_enabled', true) ->where(function (Builder $q): void { $q->whereNull('group_types') ->orWhereJsonDoesntContain('group_types', 'Unified'); }), 'unknown' => $query ->where(function (Builder $q): void { $q->whereNull('group_types') ->orWhereJsonDoesntContain('group_types', 'Unified'); }) ->where('security_enabled', false) ->where('mail_enabled', false), default => $query, }; }), ]) ->actions([]) ->bulkActions([]); } public static function getEloquentQuery(): Builder { return parent::getEloquentQuery()->latest('id'); } public static function getPages(): array { return [ 'index' => Pages\ListEntraGroups::route('/'), 'view' => Pages\ViewEntraGroup::route('/{record}'), ]; } private static function groupType(EntraGroup $record): string { $groupTypes = $record->group_types; if (is_array($groupTypes) && in_array('Unified', $groupTypes, true)) { return 'microsoft365'; } if ($record->security_enabled) { return 'security'; } if ($record->mail_enabled) { return 'mail'; } return 'unknown'; } private static function groupTypeLabel(string $type): string { return match ($type) { 'microsoft365' => 'Microsoft 365', 'security' => 'Security', 'mail' => 'Mail-enabled', default => 'Unknown', }; } private static function groupTypeColor(string $type): string { return match ($type) { 'microsoft365' => 'info', 'security' => 'success', 'mail' => 'warning', default => 'gray', }; } }