Improve SystemAdmin resources with proper fields, filters, and sorting#120
Improve SystemAdmin resources with proper fields, filters, and sorting#120ManukMinasyan merged 4 commits intomainfrom
Conversation
…rting - default sort by created_at desc on all 8 resources - show created_at/updated_at by default (not hidden) - remove ghost fields (address, phone on companies; user_id, assignee_id on tasks) - add creator, company, contact relationship columns where applicable - add TrashedFilter on soft-deletable models - add team and creation source filters across CRM resources - make relationship selects searchable in forms - add navigation badges to all resources
There was a problem hiding this comment.
Pull request overview
This PR enhances the SystemAdmin Filament resources to make list pages more useful and consistent (sorting, visible timestamps, relationship columns), while removing stale/ghost fields and adding common filters like soft-delete handling.
Changes:
- Adds consistent default sorting (
created_at desc), showscreated_at/updated_atby default, and introduces navigation badges across resources. - Improves tables and forms with relationship-based columns/selects (e.g., team, creator, owner, account owner) and adds
TrashedFilterwhere applicable. - Improves filtering/search UX (team/source filters, searchable relationship selects) and updates User/Team resources (verified icon, copyable email, team owner/slug fields).
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| app-modules/SystemAdmin/src/Filament/Resources/UserResource.php | Adds navigation badge, improves email/password handling, verified icon column, default sort, and filter. |
| app-modules/SystemAdmin/src/Filament/Resources/TeamResource.php | Adds owner relationship select, slug column, personal-team filter, default sort, and navigation badge. |
| app-modules/SystemAdmin/src/Filament/Resources/TaskResource.php | Removes ghost fields, adds relationship columns, TrashedFilter, team/source filters, default sort, and navigation badge. |
| app-modules/SystemAdmin/src/Filament/Resources/SystemAdministrators/Tables/SystemAdministratorsTable.php | Sets default sort and makes timestamps visible by default. |
| app-modules/SystemAdmin/src/Filament/Resources/PeopleResource.php | Adds relationship columns, team/company filters, TrashedFilter, default sort, and navigation badge. |
| app-modules/SystemAdmin/src/Filament/Resources/OpportunityResource.php | Adds missing form fields + relationship columns/filters, TrashedFilter, default sort, and navigation badge. |
| app-modules/SystemAdmin/src/Filament/Resources/NoteResource.php | Adds title + relationship columns/filters, TrashedFilter, default sort, and navigation badge. |
| app-modules/SystemAdmin/src/Filament/Resources/CompanyResource.php | Removes ghost fields, adds relationship columns/filters, TrashedFilter, default sort, and navigation badge. |
|
|
||
| public static function getNavigationBadge(): ?string | ||
| { | ||
| return self::getModel()::query()->count() > 0 ? (string) self::getModel()::query()->count() : null; |
There was a problem hiding this comment.
getNavigationBadge() runs count() twice, which results in two DB queries on every navigation render. Store the count in a local variable (single query) and return null when it is zero.
| return self::getModel()::query()->count() > 0 ? (string) self::getModel()::query()->count() : null; | |
| $count = self::getModel()::query()->count(); | |
| return $count > 0 ? (string) $count : null; |
|
|
||
| public static function getNavigationBadge(): ?string | ||
| { | ||
| return self::getModel()::query()->count() > 0 ? (string) self::getModel()::query()->count() : null; |
There was a problem hiding this comment.
getNavigationBadge() runs count() twice, which results in two DB queries on every navigation render. Store the count in a local variable (single query) and return null when it is zero.
| return self::getModel()::query()->count() > 0 ? (string) self::getModel()::query()->count() : null; | |
| $count = self::getModel()::query()->count(); | |
| if ($count === 0) { | |
| return null; | |
| } | |
| return (string) $count; |
|
|
||
| public static function getNavigationBadge(): ?string | ||
| { | ||
| return self::getModel()::query()->count() > 0 ? (string) self::getModel()::query()->count() : null; |
There was a problem hiding this comment.
getNavigationBadge() runs count() twice, which results in two DB queries on every navigation render. Store the count in a local variable (single query) and return null when it is zero.
| return self::getModel()::query()->count() > 0 ? (string) self::getModel()::query()->count() : null; | |
| $count = self::getModel()::query()->count(); | |
| return $count > 0 ? (string) $count : null; |
| return $table | ||
| ->defaultSort('created_at', 'desc') | ||
| ->columns([ |
There was a problem hiding this comment.
These resource changes introduce new default sorting and filters, but there are no SystemAdmin-panel Filament tests verifying the sysadmin pages render and the new sort/filter behavior works. Add Pest/Livewire tests for the sysadmin resources similar to tests/Feature/Filament/App/Resources/*ResourceTest.php (at least: render index, default sort, trashed filter).
| TextInput::make('name') | ||
| ->required() | ||
| ->maxLength(255), | ||
| TextInput::make('slug') |
There was a problem hiding this comment.
slug is NOT NULL + UNIQUE in the teams table, but the form input is optional and has no uniqueness/format validation. This can allow clearing the slug (or entering duplicates) and then failing at save time with a DB error; add required() and unique(ignoreRecord: true) (and ideally validate against Team::SLUG_REGEX).
| TextInput::make('slug') | |
| TextInput::make('slug') | |
| ->required() | |
| ->rules(['regex:' . Team::SLUG_REGEX]) | |
| ->unique(ignoreRecord: true) |
|
|
||
| public static function getNavigationBadge(): ?string | ||
| { | ||
| return self::getModel()::query()->count() > 0 ? (string) self::getModel()::query()->count() : null; |
There was a problem hiding this comment.
getNavigationBadge() runs count() twice, which results in two DB queries on every navigation render. Store the count in a local variable (single query) and return null when it is zero.
| return self::getModel()::query()->count() > 0 ? (string) self::getModel()::query()->count() : null; | |
| $count = self::getModel()::query()->count(); | |
| return $count > 0 ? (string) $count : null; |
|
|
||
| public static function getNavigationBadge(): ?string | ||
| { | ||
| return self::getModel()::query()->count() > 0 ? (string) self::getModel()::query()->count() : null; |
There was a problem hiding this comment.
getNavigationBadge() runs count() twice, which results in two DB queries on every navigation render. Store the count in a local variable (single query) and return null when it is zero.
| return self::getModel()::query()->count() > 0 ? (string) self::getModel()::query()->count() : null; | |
| $count = self::getModel()::query()->count(); | |
| return $count > 0 ? (string) $count : null; |
|
|
||
| public static function getNavigationBadge(): ?string | ||
| { | ||
| return self::getModel()::query()->count() > 0 ? (string) self::getModel()::query()->count() : null; |
There was a problem hiding this comment.
getNavigationBadge() runs count() twice, which results in two DB queries on every navigation render. Store the count in a local variable (single query) and return null when it is zero.
| return self::getModel()::query()->count() > 0 ? (string) self::getModel()::query()->count() : null; | |
| $count = self::getModel()::query()->count(); | |
| return $count > 0 ? (string) $count : null; |
|
|
||
| public static function getNavigationBadge(): ?string | ||
| { | ||
| return self::getModel()::query()->count() > 0 ? (string) self::getModel()::query()->count() : null; |
There was a problem hiding this comment.
getNavigationBadge() runs count() twice, which results in two DB queries on every navigation render. Store the count in a local variable (single query) and return null when it is zero.
| return self::getModel()::query()->count() > 0 ? (string) self::getModel()::query()->count() : null; | |
| $count = self::getModel()::query()->count(); | |
| return $count === 0 ? null : (string) $count; |
Replace CRM-focused widgets (business overview, sales analytics, team performance) with growth and adoption metrics: platform stats with sparklines, signup trend chart, record distribution doughnut, and top teams table. Add time period filter (7d/30d/90d/12m) via header modal.
- Add required, regex, and unique validation to TeamResource slug field - Fix double-count queries in navigation badges - Add #[Override] attribute to CompanyResource::form() - Import FQCNs in SignupTrendChartWidget and TopTeamsTableWidget - Extract ENTITY_CLASSES/ENTITY_TABLES constants in widgets - Add resource-level Filament tests for all sysadmin resources
- Narrow Dashboard::getColumns() return type to array - Use instanceof Closure check in PlatformGrowthStatsWidget - Replace toArray() with all() on collections - Privatize pollingInterval in TopTeamsTableWidget
Summary
created_at descon all 8 SystemAdmin resourcescreated_at/updated_atcolumns by default (removedisToggledHiddenByDefault)address,phoneon Companies;user_id,assignee_idon Tasks)creator.name,company.name,contact.name,accountOwner.name,owner.namewhere applicableTrashedFilteron all soft-deletable models (Company, People, Opportunity, Task, Note)Test plan