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
| Resource | Model | Nav Group | Description |
|---|---|---|---|
AgentResource | User (role=2) | Users | Agent CRUD, file upload to S3 |
BillRegionResource | BillRegion | — | Bill region configuration |
BillResource | Bills\* | — | Bills management |
DemoCollegeResource | — | — | Demo college setup, house images |
EventResource | Event | Events | Event CRUD, RSVPs, tickets, applications |
ExploreResource | — | — | Explore screen/city widget management |
FreebieResource | Freebie | — | Freebie management |
PerkResource | Perk | — | Perk CRUD, CSV code upload |
PerkPartnersResource | PerkPartner | — | Perk partner management |
PropertyOperatorResource | Property_operator | — | Property operator CRUD, subsidiaries, users |
SearchAreaResource | SearchArea | — | Search area configuration |
StudentsResource | User | — | Student user management |
TicketmasterEntrycodeResource | — | — | Ticketmaster 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.phpKey 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 RSVPsEventTicketExporter— Export event ticketsPerkCodeExporter— Export perk codesTicketmasterEntrycodeExporter— Export Ticketmaster entry codes
Widgets
Dashboard widgets in app/Filament/Widgets/:
SupportWidget— Full-width custom widget rendered fromfilament.widgets.support-widgetBlade view
Resource-specific widgets (e.g., AgentStatsWidget, EventStats) live inside their resource directories.