DEV3 Technologies home

Filament resource list with status tabs and filters

A Filament 4 list page with query-backed status tabs showing live counts, plus select and toggle table filters on the same resource.

FilamentTablesfilamenttabsfilterslaravel

Code

<?php

namespace App\Filament\Resources\Invoices;

use App\Filament\Resources\Invoices\Pages\ListInvoices;
use App\Models\Invoice;
use Filament\Actions\EditAction;
use Filament\Resources\Resource;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\Filter;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;

class InvoiceResource extends Resource
{
    protected static ?string $model = Invoice::class;

    public static function table(Table $table): Table
    {
        return $table
            ->defaultSort('due_at')
            ->columns([
                TextColumn::make('number')->searchable()->sortable(),
                TextColumn::make('client.name')->searchable()->toggleable(),
                TextColumn::make('total')->money('usd', divideBy: 100)->sortable(),
                TextColumn::make('status')->badge()->color(fn (string $state): string => match ($state) {
                    'paid' => 'success',
                    'sent' => 'info',
                    'overdue' => 'danger',
                    default => 'gray',
                }),
                TextColumn::make('due_at')->date()->sortable(),
            ])
            ->filters([
                SelectFilter::make('client_id')
                    ->label('Client')
                    ->relationship('client', 'name')
                    ->searchable()
                    ->preload(),
                Filter::make('large')
                    ->label('Over 1,000')
                    ->toggle()
                    ->query(fn (Builder $query): Builder => $query->where('total', '>', 100_000)),
            ])
            ->deferFilters(false) // apply filters instantly, no "Apply" button
            ->recordActions([EditAction::make()]);
    }

    public static function getPages(): array
    {
        return ['index' => ListInvoices::route('/')];
    }
}

Needs: filament/filament@^4.0

How it works

What it solves

Billing or order lists usually need two ways to slice the same table: a quick status switch at the top and finer filters for everything else. This pattern wires both into one Filament 4 resource, so staff can jump to "Overdue" in one click and still narrow by client or amount.

How it works

getTabs() on the list page returns Tab objects. Each tab adds a modifyQueryUsing constraint to the base table query, and the active tab is kept in the URL. Badge numbers come from a single grouped query that is plucked into a status-to-count map, so rendering five tabs costs one extra query, not five.

The resource's table() adds a relationship SelectFilter for the client and a toggle Filter with a closure query for large totals. Filters stack on top of the active tab, so "Overdue" plus "Over 1,000" works with no extra code. deferFilters(false) makes filters apply as soon as they change.

Adapting it

Swap the model, status values and column set. If the status is an enum, use $counts[Status::Paid->value]. Add a tab with a different constraint (for example due_at this week) and the badge for it can use its own count().

Pitfalls

  • Badge counts ignore the table's other filters and search, they describe the whole status, not the filtered view.
  • If you scope the resource through getEloquentQuery() (tenant, soft deletes), apply the same scope to the count query or the badges will not match the rows.
  • getDefaultActiveTab() must return a key that exists in getTabs().

How to use it

  1. Install Filament 4 and create a resource: php artisan make:filament-resource Invoice.
  2. Replace the generated resource table() and the list page with the two files, adjusting namespaces to your app.
  3. Make sure the invoices table has number, client_id, total (integer cents), status and due_at.
  4. Open the invoices list in the panel: the tabs sit above the table and the filters in the toolbar.
  5. To add a tab, return another Tab::make(...) with a modifyQueryUsing closure.

Licence: DEV3 Snippet Licence. Free to use, change and ship in personal and commercial projects. Attribution is appreciated, not required. Please do not redistribute snippets as a collection.

Related snippets