Skip to Content
Housr PortalFilament Resources

Filament Resources

The Portal uses Filament 4 for the modern admin panel at /internal-admin. Resources are auto-discovered from app/Filament/Resources/.

Existing Resources

ResourceModelNav GroupDescription
AgentResourceUser (role=2)UsersAgent CRUD, file upload to S3
BillRegionResourceBillRegionBill region configuration
BillResourceBills\*Bills management
DemoCollegeResourceDemo college setup, house images
EventResourceEventEventsEvent CRUD, RSVPs, tickets, applications
ExploreResourceExplore screen/city widget management
FreebieResourceFreebieFreebie management
PerkResourcePerkPerk CRUD, CSV code upload
PerkPartnersResourcePerkPartnerPerk partner management
PropertyOperatorResourceProperty_operatorProperty operator CRUD, subsidiaries, users
SearchAreaResourceSearchAreaSearch area configuration
StudentsResourceUserStudent user management
TicketmasterEntrycodeResourceTicketmaster entry code management

Resource Organization Pattern

Each resource lives in its own directory under app/Filament/Resources/ with a consistent structure:

app/Filament/Resources/Events/ EventResource.php # Resource class (model, nav, pages, relations) Actions/ # Custom Filament actions ToggleEventStatus.php EventUserApplicationStatusAction.php Pages/ # CRUD pages ListEvents.php CreateEvent.php EditEvent.php ViewEvent.php RelationManagers/ # Related model panels RSVPsRelationManager.php TicketsRelationManager.php EventUserApplicationsRelationManager.php Schemas/ # Form and infolist schemas (extracted) EventForm.php EventInfolist.php Tables/ # Table configuration (extracted) EventsTable.php Widgets/ # Resource-specific widgets EventStats.php

Key convention: Forms, tables, and infolists are extracted into separate classes in Schemas/ and Tables/ directories rather than being defined inline in the resource.

Creating a New Resource

1. Create the Resource Class

// app/Filament/Resources/YourFeature/YourFeatureResource.php namespace App\Filament\Resources\YourFeature; use App\Models\YourModel; use Filament\Resources\Resource; use Filament\Schemas\Schema; use Filament\Support\Icons\Heroicon; use Filament\Tables\Table; class YourFeatureResource extends Resource { protected static ?string $model = YourModel::class; protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedRectangleStack; protected static UnitEnum|string|null $navigationGroup = 'Your Group'; public static function form(Schema $schema): Schema { return YourFeatureForm::configure($schema); } public static function table(Table $table): Table { return YourFeatureTable::configure($table); } public static function getPages(): array { return [ 'index' => ListYourFeatures::route('/'), 'create' => CreateYourFeature::route('/create'), 'view' => ViewYourFeature::route('/{record}'), 'edit' => EditYourFeature::route('/{record}/edit'), ]; } }

2. Create the Form Schema

Extract form logic into a dedicated class:

// app/Filament/Resources/YourFeature/Schemas/YourFeatureForm.php namespace App\Filament\Resources\YourFeature\Schemas; use Filament\Forms\Components\TextInput; use Filament\Forms\Components\Select; use Filament\Schemas\Components\Section; use Filament\Schemas\Schema; class YourFeatureForm { public static function configure(Schema $schema): Schema { return $schema->components([ Section::make('Details') ->schema([ TextInput::make('name')->required(), Select::make('status') ->options([ 'active' => 'Active', 'inactive' => 'Inactive', ]) ->required(), ]) ->columnSpanFull(), ]); } }

3. Create the Table Configuration

// app/Filament/Resources/YourFeature/Tables/YourFeatureTable.php namespace App\Filament\Resources\YourFeature\Tables; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Table; class YourFeatureTable { public static function configure(Table $table): Table { return $table ->defaultSort('created_at', 'desc') ->columns([ TextColumn::make('name')->searchable()->sortable(), TextColumn::make('status') ->badge() ->color(fn (string $state): string => match ($state) { 'active' => 'success', 'inactive' => 'danger', default => 'gray', }), TextColumn::make('created_at')->dateTime()->sortable(), ]) ->filters([ SelectFilter::make('status') ->options([ 'active' => 'Active', 'inactive' => 'Inactive', ]), ]); } }

4. Create Page Classes

// app/Filament/Resources/YourFeature/Pages/ListYourFeatures.php namespace App\Filament\Resources\YourFeature\Pages; use App\Filament\Resources\YourFeature\YourFeatureResource; use Filament\Resources\Pages\ListRecords; class ListYourFeatures extends ListRecords { protected static string $resource = YourFeatureResource::class; }

Repeat for CreateYourFeature (extends CreateRecord), EditYourFeature (extends EditRecord), and ViewYourFeature (extends ViewRecord).

5. Add Relation Managers (if needed)

// app/Filament/Resources/YourFeature/RelationManagers/ItemsRelationManager.php namespace App\Filament\Resources\YourFeature\RelationManagers; use Filament\Resources\RelationManagers\RelationManager; use Filament\Tables\Table; use Filament\Tables\Columns\TextColumn; class ItemsRelationManager extends RelationManager { protected static string $relationship = 'items'; public function table(Table $table): Table { return $table ->columns([ TextColumn::make('name'), ]); } }

Then register in the resource:

public static function getRelations(): array { return [ ItemsRelationManager::class, ]; }

Real Examples from the Codebase

Scoped Resource (AgentResource)

The AgentResource uses User as its model but scopes queries to only show agents (role = 2):

public static function getEloquentQuery(): Builder { return parent::getEloquentQuery()->where('role', 2); }

It also conditionally hides itself from navigation in US mode:

public static function shouldRegisterNavigation(): bool { return config('app.USA') === false; }

S3 File Uploads (AgentResource)

File uploads save directly to the public_images S3 disk with a random filename:

FileUpload::make('image') ->image() ->saveUploadedFileUsing(fn(UploadedFile $file) => self::saveUploadedFile($file))

Conditional Form Fields (EventForm)

The event form uses ->live() on the access type selector to conditionally show/hide fields:

Select::make('access_type') ->options([...]) ->live() ->default('instant'), TextInput::make('promo_code') ->visible(fn ($get) => $get('access_type') === 'promo_code'),

Relation Managers with Export (RSVPsRelationManager)

The RSVPs relation manager includes a CSV export action:

->headerActions([ ExportAction::make() ->exporter(EventRSVPExporter::class) ->fileDisk('public_misc') ->fileName(fn () => 'event-rsvps-'.now()->format('Ymd-His').'.csv'), ])

Filament Exports

Export classes live in app/Filament/Exports/:

  • EventRSVPExporter — Export event RSVPs
  • EventTicketExporter — Export event tickets
  • PerkCodeExporter — Export perk codes
  • TicketmasterEntrycodeExporter — Export Ticketmaster entry codes

Widgets

Dashboard widgets in app/Filament/Widgets/:

  • SupportWidget — Full-width custom widget rendered from filament.widgets.support-widget Blade view

Resource-specific widgets (e.g., AgentStatsWidget, EventStats) live inside their resource directories.

Last updated on