xavicabot / filament-appointments
FilamentPHP 4/5 Appointment Picker (Tucalendi-style) + Admin resources + Google Calendar availability sync
Package info
github.com/xavicabot/filament-appointments
pkg:composer/xavicabot/filament-appointments
Requires
- php: ^8.2
- filament/filament: ^4.0|^5.0
- google/apiclient: ^2.0
- illuminate/support: ^11.28|^12.0
Requires (Dev)
- mockery/mockery: ^1.6
- orchestra/testbench: ^9.0|^10.0
- phpunit/phpunit: ^11.5|^12.0
README
A complete appointment booking system for FilamentPHP. Includes a visual time slot picker, weekly schedule management, manual blocks, booking with double-booking prevention, email confirmations, Google Calendar busy-time sync, and automatic Google Meet link generation.
v2.x — Requires PHP 8.2+, Laravel 11.28+/12, and FilamentPHP 4 or 5.
For FilamentPHP 3 support, use v1.x (
composer require xavicabot/filament-appointments:^1.0).
Installation
1. Install the package
composer require xavicabot/filament-appointments
2. Publish and run the migrations
php artisan vendor:publish --tag=filament-appointments-migrations php artisan migrate
This creates the following tables: fa_rules, fa_blocks, fa_bookings, fa_connections, fa_sources, fa_busy_cache.
3. Publish the config file
php artisan vendor:publish --tag=filament-appointments-config
This creates config/filament-appointments.php.
Setting Up Your Panel
Register the Plugin
Add the plugin to your Filament PanelProvider:
use XaviCabot\FilamentAppointments\FilamentAppointmentsPlugin; class AdminPanelProvider extends PanelProvider { public function panel(Panel $panel): Panel { return $panel ->plugins([ FilamentAppointmentsPlugin::make(), ]); } }
This adds three resources to your sidebar under the "Appointments" navigation group:
- Appointment Rules — Define weekly availability schedules
- Appointment Blocks — Block specific dates/times (holidays, vacations, etc.)
- Calendar Connections — Manage Google Calendar integrations
Prepare Your User Model
Add the HasAppointments trait to the model that provides appointments (e.g., advisors, doctors, professionals):
use XaviCabot\FilamentAppointments\Support\HasAppointments; use XaviCabot\FilamentAppointments\Support\HasBookings; class User extends Authenticatable { use HasAppointments, HasBookings; }
HasAppointments— Marks the model as a schedule owner. ProvidesUser::withAppointments()scope (users that have active rules) and$user->toSlotOwner().HasBookings— Allows the model to book appointments. Provides$user->bookings()relationship and email methods for confirmations.
If the people who book appointments are different from those who provide them (e.g., Patient books with Doctor), add
HasAppointmentsto Doctor andHasBookingsto Patient.
Set the Owner Model in Config
Open config/filament-appointments.php and set your owner model:
'owner_model' => \App\Models\User::class, 'owner_label' => 'name',
This allows the admin resources to show a dropdown of users when creating rules and blocks, instead of raw numeric IDs.
Creating Availability Schedules
Go to Appointment Rules in your Filament panel and create rules for each day of the week. Each rule defines:
| Field | Description | Example |
|---|---|---|
| Owner | The professional providing appointments | Dr. Smith |
| Weekday | Day of the week (Monday–Sunday) | Monday |
| Start time | When the schedule starts | 09:00 |
| End time | When the schedule ends | 13:00 |
| Interval | Minutes between each slot | 30 |
| Active | Enable/disable the rule | Yes |
Example: A rule for Monday, 09:00–13:00 with 30-min intervals generates: 09:00, 09:30, 10:00, 10:30, 11:00, 11:30, 12:00, 12:30.
You can create multiple rules for the same day (e.g., morning 09:00–13:00 + afternoon 15:00–18:00).
Blocking Dates
Go to Appointment Blocks to manually mark dates or time ranges as unavailable:
- All-day block: Set the date only (leave start/end time empty) — blocks the entire day
- Partial block: Set date + start/end time — blocks only that time range
- Reason: Optional note (e.g., "Public holiday", "Vacation")
Using the Appointment Picker
The AppointmentPicker is a Filament form field that shows a date input and a dropdown grid of available time slots.
Basic Example
use XaviCabot\FilamentAppointments\Forms\Components\AppointmentPicker; use XaviCabot\FilamentAppointments\Support\SlotOwner; AppointmentPicker::make('time_slot') ->label('Pick a time') ->owner(fn () => SlotOwner::forUser(auth()->user())) ->minDate(now()) ->maxDate(now()->addDays(30)) ->required()
With an Advisor Selector
When users need to choose who they're booking with:
use Filament\Forms\Get; use XaviCabot\FilamentAppointments\Forms\Components\AppointmentPicker; use XaviCabot\FilamentAppointments\Support\SlotOwner; // Advisor dropdown Forms\Components\Select::make('advisor_id') ->label('Advisor') ->options(User::withAppointments()->pluck('name', 'id')) ->required() ->live(), // Time slot picker — reacts to selected advisor AppointmentPicker::make('time_slot') ->label('Pick a time') ->owner(fn (Get $get) => $get('advisor_id') ? new SlotOwner('user', (int) $get('advisor_id')) : null ) ->minDate(now()) ->maxDate(now()->addDays(30)) ->required() ->visible(fn (Get $get) => filled($get('advisor_id'))),
Field Value
The selected value is stored as a string in the format "YYYY-MM-DD HH:MM" (e.g., "2026-02-16 09:30").
To parse it:
$parts = explode(' ', $data['time_slot'], 2); $date = $parts[0]; // "2026-02-16" $time = $parts[1]; // "09:30"
Available Methods
| Method | Description |
|---|---|
owner(callable) |
Closure returning a SlotOwner. Determines whose schedule is shown. |
minDate(string|DateTime|Closure) |
Earliest selectable date. |
maxDate(string|DateTime|Closure) |
Latest selectable date. |
Creating Bookings
Use AppointmentService to create bookings with double-booking protection:
use XaviCabot\FilamentAppointments\Services\AppointmentService; use XaviCabot\FilamentAppointments\Support\SlotOwner; $service = app(AppointmentService::class); $booking = $service->createBooking( owner: new SlotOwner('user', $advisorId), date: '2026-02-20', startTime: '10:00', client: auth()->user(), );
This automatically:
- Determines the slot duration from the matching rule
- Prevents double-booking (database-level lock)
- Creates a Google Meet link if the owner has a connected Google account
- Sends a confirmation email to the client
- Returns an
AppointmentBookingmodel
Full Page Example
Here's a complete Filament page that lets users book appointments:
use XaviCabot\FilamentAppointments\Forms\Components\AppointmentPicker; use XaviCabot\FilamentAppointments\Services\AppointmentService; use XaviCabot\FilamentAppointments\Support\SlotOwner; // In your form method: $schema = [ Forms\Components\Select::make('advisor_id') ->label('Advisor') ->options(User::withAppointments()->pluck('name', 'id')) ->required() ->live(), AppointmentPicker::make('time_slot') ->label('Pick a time') ->owner(fn (Get $get) => $get('advisor_id') ? new SlotOwner('user', (int) $get('advisor_id')) : null ) ->minDate(now()) ->maxDate(now()->addDays(30)) ->required() ->visible(fn (Get $get) => filled($get('advisor_id'))), Forms\Components\Textarea::make('notes') ->label('Notes (optional)') ->rows(2), ]; // In your submit method: public function submit(): void { $data = $this->form->getState(); $parts = explode(' ', $data['time_slot'], 2); $date = $parts[0]; $time = $parts[1] ?? '00:00'; $owner = new SlotOwner('user', (int) $data['advisor_id']); $booking = app(AppointmentService::class) ->createBooking($owner, $date, $time, auth()->user()); Notification::make() ->title('Appointment booked!') ->success() ->send(); }
Email Confirmations
By default, bookings are created with status confirmed. You can require email confirmation first.
Enable Confirmation Flow
In config/filament-appointments.php:
'bookings' => [ 'require_confirmation' => true, 'confirmation_ttl_hours' => 24, ],
How It Works
- Booking is created with status
pending - Client receives an email with a signed URL button
- Client clicks the button — booking status changes to
confirmed - If the link expires (after
confirmation_ttl_hours), the booking can be auto-cancelled
The email includes the appointment date/time, a confirmation button, the expiration notice, and a Google Meet link (if available).
Auto-Cancel Expired Bookings
Schedule the artisan command to automatically cancel unconfirmed bookings:
// Laravel 11+ (routes/console.php) Schedule::command('fa:expire-appointments')->hourly(); // Laravel 10 (app/Console/Kernel.php) $schedule->command('fa:expire-appointments')->hourly();
Or run manually:
php artisan fa:expire-appointments
Google Calendar Integration
The package can sync with Google Calendar to:
- Mark slots as unavailable when the owner has events in Google Calendar (freeBusy API)
- Create Google Calendar events with Meet links when bookings are made
Step 1: Create Google OAuth Credentials
- Go to Google Cloud Console
- Create a project (or use an existing one)
- Enable the Google Calendar API
- Go to Credentials > Create Credentials > OAuth 2.0 Client ID
- Application type: Web application
- Add an authorized redirect URI:
https://your-domain.com/filament-appointments/google/callback
Step 2: Add Environment Variables
Add to your .env:
GOOGLE_CLIENT_ID=your-google-client-id GOOGLE_CLIENT_SECRET=your-google-client-secret GOOGLE_REDIRECT_URI=https://your-domain.com/filament-appointments/google/callback
The redirect URI must match exactly what you configured in Google Cloud Console.
Step 3: Connect an Account from Filament
- Go to Calendar Connections in your Filament panel
- Click "Connect Google"
- Select the user to connect
- Complete the Google OAuth consent screen
- Once connected, click "Sync" to fetch the list of Google Calendars
- Toggle "Included" for each calendar you want to check for busy times
Two-Way Sync
The integration works in both directions:
Google Calendar → App (busy-time detection)
When slots are loaded for a date, the package automatically checks the owner's Google Calendar for existing events:
- Fetches the owner's connected calendars (only those marked as "Included")
- Calls the Google Calendar freeBusy API for that date
- Any slot that overlaps with a Google Calendar event is marked as unavailable
- Results are cached to avoid excessive API calls
So if the owner has a meeting in Google Calendar from 10:00 to 11:00, the slots at 10:00 and 10:30 will automatically appear as unavailable in the appointment picker.
The cache TTL is configurable:
// config/filament-appointments.php 'google' => [ 'cache_ttl_seconds' => 120, // Cache busy windows for 2 minutes ],
Sync is near real-time: there is a maximum delay equal to the cache TTL (2 minutes by default). After that, any change in Google Calendar is reflected in the available slots.
App → Google Calendar (event creation + Meet link)
When a booking is created and the owner has a connected Google account:
- A Google Calendar event is automatically created on the owner's primary calendar — this blocks that time slot in Google Calendar too
- A Google Meet conference link is generated for the event
- The Meet URL is stored in
$booking->metadata['meet_link'] - The Meet link is included in the confirmation email sent to the client
This means the owner's Google Calendar stays in sync: if someone books an appointment through your app, it appears as a calendar event with a Meet link, and that time is blocked for future Google Calendar scheduling.
If no Google connection exists, the booking proceeds normally without a calendar event or Meet link.
Configuration Reference
Full config/filament-appointments.php:
return [ // URL prefix for all package routes (slots endpoint, Google OAuth, etc.) 'route_prefix' => 'filament-appointments', // Auto-register admin resources (Rules, Blocks, Calendar Connections) // Set to false if you want to register them manually or not at all 'register_resources' => true, // The Eloquent model that owns schedules // Used in admin dropdowns to list available owners 'owner_model' => \App\Models\User::class, // Which attribute to display in owner dropdowns 'owner_label' => 'name', // Timezone for slot generation 'timezone' => 'Europe/Madrid', // Custom slot resolver class (advanced) 'resolver' => \XaviCabot\FilamentAppointments\Support\SlotResolver::class, // Booking behavior 'bookings' => [ // If true, bookings start as "pending" and require email confirmation 'require_confirmation' => false, // Hours before unconfirmed bookings are auto-cancelled 'confirmation_ttl_hours' => 24, ], // Google Calendar settings 'google' => [ // How long to cache busy windows per owner/date (seconds) 'cache_ttl_seconds' => 120, ], ];
Customizing Views
Publish the views to customize the look of the appointment picker, emails, and confirmation page:
php artisan vendor:publish --tag=filament-appointments-views
This copies the views to resources/views/vendor/filament-appointments/:
| View | Description |
|---|---|
forms/components/appointment-picker.blade.php |
The slot picker (Alpine.js + Tailwind) |
emails/appointment-confirmed.blade.php |
Email sent when a booking is confirmed |
emails/appointment-pending.blade.php |
Email sent when a booking requires confirmation |
bookings/confirmed.blade.php |
Page shown after clicking the confirmation link |
Translations
The package includes translations in 5 languages: English, Spanish, French, German, and Italian.
The language is determined by your Laravel app locale (config/app.php > locale).
To override translations, publish them:
php artisan vendor:publish --tag=filament-appointments-translations
Or add your own files in:
resources/lang/vendor/filament-appointments/{locale}/messages.php
Disabling Admin Resources
If you don't want the package to register its admin resources automatically (e.g., you want to manage rules via your own UI):
// config/filament-appointments.php 'register_resources' => false,
License
MIT