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

Filament Fullcalendar Laravel Package

saade/filament-fullcalendar

Filament FullCalendar is a highly customizable calendar widget for Filament, powered by FullCalendar. It supports viewing, creating, editing, and deleting events through Filament Actions, with a native Filament theme.

View on GitHub
Deep Wiki
Context7

Getting Started

Check your version line first. ^3.x is the stable path for Filament 3. The ^5.x line targets FullCalendar 7, is still beta (5.0.0-beta4 at time of writing), and is a big feature release rather than a compatibility bump — the maintainer signalled v5.0.0 stable roughly a week after beta1. Don't adopt it through a loose constraint by accident; pin the exact beta.

# Stable, Filament 3
composer require saade/filament-fullcalendar:"^3.0"

# v5 beta line (FullCalendar 7) — opt in explicitly
composer require saade/filament-fullcalendar:"5.0.0-beta4"
php artisan filament:assets

Coming from v4, upgrade with the shipped script, not by hand. v5 carries an automated Rector-based upgrader that rewrites widgets and their tests without booting the app.

composer require saade/filament-fullcalendar:"^5.0@beta"   # may error — expected, see below
composer require rector/rector:^2.5 --dev --no-scripts
vendor/bin/filament-fullcalendar-v5 --dry-run
vendor/bin/filament-fullcalendar-v5

The first command failing is normal: package discovery boots your existing v4 widgets against v5 classes before they've been rewritten. --no-scripts on the Rector install stops discovery running again and lets the upgrader do its work. It handles handler signatures, record APIs, deprecated actions, getFormSchema(), and test call shapes, and can preserve the v4 create-button behaviour. It is an assistant, not the upgrade guide — when it finishes it prints a checklist of manual follow-ups; work through it before touching your calendar.

Where to look first: the README still lags the code. Open vendor/saade/filament-fullcalendar/src/Widgets/FullCalendarWidget.php, the panel plugin class (FilamentFullCalendarPlugin), and — on v5 — the model contract/trait you put on your Eloquent models. Those three are the real API surface: fluent config methods, the $fetchInfo payload shape, filter/tab registration, action registration, resource and drag-attribute setters all live there.

Register the panel plugin. Global options — scheduler licence, theme, and in v5 much of the calendar wiring — hang off FilamentFullCalendarPlugin, not the widget. Wire it in the panel provider before anything else, otherwise later fluent calls have nowhere to land.

->plugins([
    FilamentFullCalendarPlugin::make()
        ->schedulerLicenseKey(config('services.fullcalendar.licence')),
])

First use case: an existing model with a start/end date (bookings, appointments, shifts) rendered as a read-only month view, then progressively unlock filters, tabs, drag/drop and create/edit.

php artisan make:filament-widget BookingCalendar

On v5 the shortest path is to let the model describe itself — put the package's calendar-event contract/trait on Booking (title + start/end attributes) and return the query. On the 3.x line you map into EventData by hand:

use Saade\FilamentFullCalendar\Widgets\FullCalendarWidget;
use Saade\FilamentFullCalendar\Data\EventData;

class BookingCalendar extends FullCalendarWidget
{
    public function events(array $fetchInfo): array
    {
        return Booking::query()
            ->where('starts_at', '<', $fetchInfo['end'])
            ->where('ends_at', '>', $fetchInfo['start'])
            ->get()
            ->map(fn (Booking $b) => EventData::make()
                ->id($b->id)
                ->title($b->title)
                ->start($b->starts_at)
                ->end($b->ends_at)
                ->allDay($b->all_day)
                ->toArray())
            ->toArray();
    }
}

Register it on a page (getHeaderWidgets()), on the dashboard (->widgets([...])), or as a Resource page widget when the calendar belongs next to a record.

Implementation Patterns

Range-filter in the event feed, always. FullCalendar re-invokes the feed on every navigation, view switch, and refresh, passing start/end/timezone in $fetchInfo. This is your pagination — treat it as a hard requirement, not an optimisation. Order by start, cap with ->limit() for pathological ranges, and index the date columns.

Extract the query into a dedicated class (BookingCalendarFeed::for($start, $end)) so you can unit-test event shape and range scoping without booting Livewire. This matters more on v5, where the widget class does much more work.

Let models be events (v5). Rather than transforming everything into arrays, implement the package's calendar-event contract on each model so it declares its own title and start/end attributes. One calendar can then mix models — the plugin tracks which record each event belongs to and uses the right model, policy, label and actions when the event is opened. For calendars that mirror a Resource, reuse the resource's form and infolist instead of duplicating schemas.

Filters and tabs are first-class (v5) — use them before reaching for JS. Filters are ordinary Filament form fields, rendered in a dropdown, modal, slide-over, above or below the calendar; tabs behave like resource list tabs with badges and query modification. Both can remember state between visits. This is the correct home for "filter by employee / status / department / location", and it deletes the custom Livewire wrappers people used to write around the widget.

// Illustrative shape — check FullCalendarWidget for exact signatures
public function filters(): array
{
    return [
        SelectFilter::make('employee_id')->options(Employee::pluck('name', 'id')),
        SelectFilter::make('status')->options(BookingStatus::class),
    ];
}

Actions are just Filament actions. Create/view/edit/delete now run through Filament's action system: custom actions sit alongside the defaults, authorization goes through model policies, and the relevant record is injected automatically. After an action mutates something, the calendar refreshes itself — you no longer hand-roll $this->refreshEvents() after every save. Keep action bodies as thin Eloquent calls.

Persist drag/drop by declaring attributes. Tell the widget which model attributes represent the event start and end and moves/resizes save themselves. If you want a review step, require confirmation and let the user confirm the new dates inside the edit action first. This replaces the old onEventDrop/onEventResize persistence dance — and the old failure mode where the UI moved and the record silently didn't.

Resources get a real PHP API (v5). Return rooms/employees/machines/vehicles straight from the widget, persist the relationship when an event moves between resources, and refresh resource data independently of events. Combine with drag-in-from-outside for the classic "unscheduled list beside the calendar" workflow — users drag a task onto a day or a person and it becomes a placed event.

External calendars (v5). iCalendar feeds and Google Calendar can be shown alongside your own events without importing them. They stay separate and read-only. Private iCal URLs are fetched by your application and never handed to the browser — keep them in config/env, not in config() output.

Bring other calendars in, don't merge them in PHP. Resist unioning external feeds into your events() payload; the built-in external-calendar support keeps them isolated, cacheable, and clearly read-only.

Programmatic control without JavaScript (v5). PHP methods exist to move to a date, change view, navigate forward/backward, scroll to a time, change options, and refresh events or resources. Use these from header actions and custom toolbar tools instead of emitting JS.

Toolbar and header. Give the widget a heading, description and header actions like any Filament component, and compose the calendar toolbar from navigation buttons, view switches, filters, Filament actions and your own tools.

Config split two ways: fluent methods (->selectable(), ->editable(), ->timezone(), ->locale(), ->firstDay(), ->headerToolbar()) for common toggles, and a config(): array override for anything unwrapped (custom eventContent, slotMinTime, eventDisplay, businessHours). Merge semantics are shallow — mirror the package's defaults when you override the whole array.

public function config(): array
{
    return [
        'firstDay' => 1,
        'headerToolbar' => [
            'left'   => 'dayGridMonth,timeGridWeek,listWeek',
            'center' => 'title',
            'right'  => 'prev,next today',
        ],
        'slotMinTime' => '07:00:00',
    ];
}

Colour via Filament colours, not hex. v5 routes event colours through Filament's colour system, so named colours follow your palette and stay readable in dark mode. Use a PHP enum on the model/extendedProps, not per-widget hardcoded hex.

Themes (v5). Register a JS module exporting a FullCalendar 7 theme, then select it by name. Registration is lazy — pass a closure so Vite::asset() resolves at render time rather than panel boot, keeping config:cache and manifest-less CI alive.

use Illuminate\Support\Facades\Vite;

FilamentFullCalendarPlugin::make()
    ->registerTheme('brutalist', fn (): string => Vite::asset('resources/js/calendar/brutalist.js'))
    ->theme('brutalist')

Themes resolve by name and may extend a built-in (daygrid, timegrid, list) or another registered theme, overriding only what they set. Put brand palette, slot heights and header layout there rather than fanning them across per-widget config() overrides that merge shallowly and drift.

Gate interaction flags to real behaviour. ->selectable() without a create action and draggable events without persistence attributes or a confirmation flow produce a UI that looks interactive and discards the change. Branch the fluent calls on auth()->user()->can(...) — config is rebuilt per render.

Multi-source calendars: prefer v5's multi-model support or the external-calendar feature over unioning unrelated tables in one events(). If you must union, tag extendedProps['source'] and branch in the click handler; two widgets is cleaner when the forms differ.

Custom event click → custom page: override the click handling to redirect() to your ViewBooking resource page instead of a modal. Still a valid way to sidestep modal validation/timezone friction, though v5's action system makes the modal path considerably less painful.

Gotchas and Tips

  • Beta discipline. Pin the exact build (5.0.0-beta4) and read the notes before each bump. beta4 fixes a beta3 regression where clicking, dragging or resizing on a calendar without resource views threw a browser error and modals never opened — the symptom looks exactly like a broken click handler in your own code. Smoke-test both with and without resource views on every upgrade.
  • The upgrade script is not the upgrade guide. Run --dry-run first, commit before running for real, and treat the printed checklist as a to-do list, not a warning to ignore. Anything it flags — bespoke handlers, custom actions, non-standard test assertions — needs eyes.
  • The failing composer require is expected. If you skip --no-scripts on the Rector install, package discovery re-boots your v4 widgets against v5 classes and the upgrader never gets a clean run. Install, upgrade, then let discovery happen.
  • FullCalendar 7. Raw JS options in config() are the most likely thing to shift between majors. Before pasting an FC6 snippet (eventContent, eventDisplay, slot*, view names), check the FC7 docs. When in doubt, the theme module is the better home for view-level options.
  • Theme assets are your problem. A registered theme is a Vite asset — npm run build before deploy, and expect Unable to locate file in Vite manifest if you forget. In dev it points at the dev server; a stale bundle gives a blank or unstyled calendar with no PHP error.
  • Unregistered theme names fail silently. A typo'd ->theme() falls back or renders unstyled. Check the network tab for a 404 on the module and confirm the name matches the first argument to registerTheme().
  • External calendars are read-only and can fail independently. Google/iCal outages, rate limits and bad feeds degrade the calendar without touching your events — surface fetch failures rather than letting them look like "no events". Never place a private iCal URL in a client-visible config() array.
  • Filters/tabs remember state. That's the feature, but it's also why a colleague "can't see any bookings" — a persisted filter from a previous visit. Clear state when debugging, and assert defaults in tests.
  • Timezone is the #1 source of off-by-hours bugs. The calendar renders in the timezone() you set (or the browser's). Feed events the same offset — if you store UTC and set ->timezone('Europe/London'), events drift. Store local wall-clock time, or convert explicitly in the feed and back on write.
  • Event id comes back as a string. JSON-sourced; cast before querying integer PKs and use loose comparison in tests.
  • Version pinning: ^2.x Filament 2, ^3.x Filament 3, ^5.0@beta for the FullCalendar 7 line. A mismatch surfaces as "class not found" or a Livewire render exception, not a dependency error.
  • Assets after upgrades: run php artisan filament:assets (or filament:upgrade) and php artisan view:clear after composer update, or you get a stale JS bundle and a blank calendar with no PHP error.
  • Blank calendar checklist: widget registered on the page? ->canView() false? exception inside the feed swallowed into a 500 on the Livewire fetch (check network, not console)? model missing start/end attributes or the calendar contract? theme module 404ing? a remembered filter excluding everything?
  • Drag/drop appears to revert: the model isn't declaring its start/end attributes, the confirmation flow was dismissed, or your persistence hook throws after the UI already moved. FullCalendar reverts optimistically on failure — log inside the hook.
  • Resource views are premium. resourceTimeline / resourceTimeGrid need schedulerLicenseKey() on the panel plugin and the JS bundle present. Don't promise resource scheduling before confirming it renders — and note these are the views whose absence triggered the beta3 click bug.
  • Dark mode and branding. Calendar chrome follows your Filament theme; per-event colours need to come from the Filament colour system to stay readable in both modes. A custom FC theme styles the calendar, not your event badges.
  • Tests: assertSee can't assert on a JS canvas. Test the feed/query class directly, use Livewire::test(BookingCalendar::class) for wiring, cover drag/resize through the persistence path rather than the drag, and assert filter/tab effects on the resulting query. If the upgrader rewrote your test calls, re-read them — it rewrote shapes, not intentions.
  • Extension point: when the README runs out, the widget's config() override is the escape hatch for any raw FullCalendar option, and the theme module is the escape hatch for anything view-wide. Everything the package exposes is ultimately merged into that array.
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.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core
nodisrupt/laravel-agent
sierratecnologia/media-manager