Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Advanced Roster For Filament Laravel Package

occtherapist/advanced-roster-for-filament

Advanced Roster for Filament brings flexible staff rostering to Laravel admin panels. Create and manage shifts, assign team members, and view schedules in a clean Filament interface, making workforce planning simpler without leaving your app.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a Filament plugin (occtherapist/advanced-roster-for-filament) that adds a roster/scheduling view to a panel. v0.3.0 is the release where the roster stops being read-only and becomes queryable — filters are now a first-class extension point, so this is the version to adopt if your users have ever asked "can I just see my shifts?".

Minimal steps

composer require occtherapist/advanced-roster-for-filament:^0.3
php artisan vendor:publish --tag="advanced-roster-for-filament-config"
php artisan migrate   # if the roster ships its own tables/columns

Register the plugin on the panel you actually want the roster in — not globally:

// app/Providers/Filament/AdminPanelProvider.php
public function panel(Panel $panel): Panel
{
    return $panel
        ->plugin(AdvancedRosterPlugin::make())
        // ...
}

Where to look first

  1. The README's filter section — it now carries a full custom role/location filter example, which is the fastest way to learn the RosterFilter contract's shape.
  2. config/advanced-roster-for-filament.php — confirm the features.filters key actually landed in the published config (true by default). If your config was published on v0.2, it won't be there.
  3. The RosterFilter contract itself (vendor/occtherapist/...) — read the method signatures rather than trusting the README snippet; the contract is what you'll implement against, and at 0.x it can shift between minors.

First use case

Open the roster as a non-admin user and confirm the funnel icon appears in the toolbar and that clicking it opens the modal. Apply the built-in assignee filter to yourself, reload the page, and verify the filter persisted. That single loop exercises the registry, the modal, per-user persistence, and the active-filter badge — if any of those four break, you've found your integration problem before writing a line of custom code.

Implementation Patterns

Custom filters via the contract. The registry pattern means you rarely touch the plugin internals — you implement RosterFilter and register it. The natural shape:

final class LocationFilter implements RosterFilter
{
    public function key(): string { return 'location'; }

    public function label(): string { return 'Location'; }

    public function apply(Builder $query, mixed $value): Builder
    {
        return $query->whereIn('location_id', Arr::wrap($value));
    }

    // Likely also: options(), type()/formComponent(), defaultValue()
}

Treat the exact method list as "read the interface" — but the mental model is stable: identity (key/label), presentation (options or a form schema), and a query mutation. Keep apply() pure and side-effect free so filters compose predictably when several are active at once.

Register filters where they're scoped, not globally. Prefer registering per-panel or per-resource over a service provider's boot() if the plugin exposes that hook, so a tenant-facing panel and an internal admin panel can expose different filter sets. If registration is global only, guard the filter's options() by auth()->user()->can(...) rather than conditionally registering.

Custom filters are your authorization boundary. The built-in assignee filter narrows to a user, but it doesn't grant anything. If you add a "Team" or "Role" filter, derive the option list from what the current user can already see — apply() shouldn't be the place you discover a user asked for someone else's roster.

Gate the feature per environment/panel. features.filters is a config flag, so use it as a kill switch. When onboarding a new panel or doing a staged rollout, flip it off in config/advanced-roster-for-filament.php (or via config() in a service provider for env-specific behaviour) and the toolbar UI disappears without needing a deploy of view code. Note the flag gates the UI; a filter already persisted for a user should be assumed to still exist in storage.

Persistence is per-user — design for it. Because applied filters survive a reload, your users will treat a filtered roster as their default view. Two consequences worth planning around: document that behaviour in your own release notes (people will report "the roster is broken, shifts are missing" when it's just a stale filter), and make sure reset is obvious — v0.3.0 added a clear-all in the modal, so lean on that rather than building your own "clear filters" action.

Compose with Filament's own table/filter machinery carefully. If the roster exposes a Filament table underneath, resist adding both a Filament Filter and an RosterFilter for the same concept — you'll get two badges and two sources of truth. Pick one; the plugin's registry is the right choice if you want the modal/badge/persistence behaviour for free.

Testing. Filters are the easiest part of a scheduling UI to unit test: instantiate the filter, pass a builder with a couple of seeded rows, assert the result set. Cover the "no value" case explicitly — an empty filter must be a no-op rather than whereIn([]) (which silently returns nothing and looks like data loss).

Gotchas and Tips

Published config won't merge itself. The most likely v0.3.0 upgrade failure is an existing config/advanced-roster-for-filament.php from v0.2 that has no features.filters key. Depending on how the package reads it (config('...features.filters') vs a merged default), you'll either get the default silently or a null that reads as disabled. Diff the package's config file against yours, and run php artisan config:clear after any edit — cached config is the cause of most "I enabled it and nothing happened" reports.

0.x means ^0.3 is not a safe pin. Composer treats 0.3.x minors as potentially breaking-adjacent in spirit, but ^0.3 still allows 0.4… actually it does not — ^0.3 resolves to >=0.3.0 <0.4.0. Good. Still, pin with a committed composer.lock and read the changelog on every bump; the RosterFilter contract is new and is exactly the kind of surface that gets a signature tweak.

Badge counts filters, not conditions. The badge shows how many filters are applied, not how many rows are hidden. Users will misread it as "3 shifts". If that ambiguity matters in your UI, add a hint near the toolbar rather than patching the badge.

Stale persistence after schema changes. Per-user filter state is stored somewhere (session or a user meta column — check which). If you rename a filter key(), previously persisted state referencing the old key may be silently dropped (fine) or throw on resolution (not fine). Either way, users lose their saved view. Treat filter keys as a mini public API: rename deliberately, and consider a migration or a key-versioning prefix.

Empty option lists look like bugs. A role/location filter whose options() returns [] for a given user renders a modal with an empty select and no explanation. Return a disabled state or omit the filter entirely for that user, and check for this in a low-privilege test account — it's the #1 thing that gets filed as "filter is broken".

Query performance hides until the roster is real. Each active filter is another where on a query that may already join shifts → users → locations. Seed a realistic dataset (thousands of roster rows, hundreds of users) before you call the filter work done; an unindexed whereIn on a joined column will be invisible on your 10-row seeder. Check the query log with three filters applied.

Where to extend vs. where to stop. The registry is the supported extension point — use it. Avoid subclassing plugin internals or overriding views to add behaviour, because the toolbar/modal is precisely the part that changed in this release and will change again. If you need something the contract can't express (e.g. a filter that changes the roster's layout rather than its rows), that's a feature request, not a patch.

Debugging checklist for "filter does nothing": (1) is features.filters true and config cleared; (2) is the filter actually registered on this panel; (3) does key() collide with the built-in assignee filter; (4) is a persisted value overriding what you're clicking; (5) does apply() return the builder rather than a fresh query. In that order — it's almost always 1 or 5.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log