Skip to main content

Black Friday 2025! Only until December 1st: coupon FRIDAY25 for 40% off Yearly/Lifetime membership!

Read more here
Premium Members Only
Join to unlock this tutorial and all of our courses.
Tutorial Premium Tutorial

Filament Appointment Booking: Re-Use Admin Panel Form on Public Page

August 17, 2023
22 min read

Imagine you need a system for booking appointments: doctor, hair salon, or cart racing track. In this tutorial, we will create exactly that, with a 2-in-1 demo: how to build a dynamic form in Filament and how to reuse it outside the adminpanel for non-logged-in users to be able to book an appointment.

Specifically, we will make bookings for cart racing tracks. But the same logic could be applied to a different company. Just use room numbers or doctor names instead of track names.

As usual, the link to the complete repository will be at the end of the tutorial.

Here's the Table of Contents for this detailed step-by-step tutorial.

Section 1. Filament Panel: Reservation Table/Form

  1. Install Filament
  2. Set Up User to Access Admin Panel
  3. Filament CRUD Resource for Tracks
  4. Filament: Dynamic Reservation Form
  5. Filament: Table of Reservations
  6. Filament: Disable Reservation Edit

Section 2. Public Form for Reservation

  1. Install And Configure TailwindCSS
  2. Set Up Main App Layout
  3. Use Service In Filament ReservationResource
  4. Create Livewire ReservationForm Component

Setup: DB Models and Migrations

Before getting to Filament, we need to create the DB structure in Laravel.

  • Tracks will have many Reservations
  • Reservation belongs to a Track and also belongs to a User

Here are our migrations and Models:

Tracks table:

Schema::create('tracks', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->timestamps();
});

app/Models/Track.php

use Illuminate\Database\Eloquent\Relations\HasMany;
 
// ...
 
protected $fillable = ['title'];
 
public function reservations(): HasMany
{
return $this->hasMany(Reservation::class);
}

Reservations table:

use App\Models\Track;
use App\Models\User;
 
// ...
 
Schema::create('reservations', function (Blueprint $table) {
$table->id();
$table->foreignIdFor(User::class)->constrained()->cascadeOnDelete();
$table->foreignIdFor(Track::class)->constrained()->cascadeOnDelete();
$table->datetime('start_time');
$table->datetime('end_time');
$table->timestamps();
});

app/Models/Reservation.php

use Illuminate\Database\Eloquent\Relations\BelongsTo;
 
// ...
 
protected $fillable = [
'user_id',
'track_id',
'start_time',
'end_time'
];
 
public function track(): BelongsTo
{
return $this->belongsTo(Track::class);
}
 
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}

app/Models/User.php

use Illuminate\Database\Eloquent\Relations\HasMany;
 
public function reservations(): HasMany
{
return $this->hasMany(Reservation::class);
}

We also seed the admin user and five racing tracks.

database/seeders/DatabaseSeeder.php

use App\Models\Track;
use App\Models\User;
 
public function run(): void
{
User::create([
'name' => 'Admin',
'email' => '[email protected]',
'password' => bcrypt('password'),
]);
 
$tracks = [
'Track 1',
'Track 2',
'Track 3',
'Track 4',
'Track 5',
];
foreach ($tracks as $track) {
Track::create(['title' => $track]);
}
}

And finally, migrate and seed the database.

php artisan migrate:fresh --seed

Ok, we have the database ready. Now it's time to build the admin panel.


1. Filament Panel: Reservation Table/Form

First, we will build a Filament project so that the administrator can place a booking. In the following Section 2, we will re-use that form to accept reservations from the outside.

Our form will be dynamic: the admin chooses the date, sees the available timeslots, and then picks the radio button selected.

Reservations


1.1. Install Filament

Since Livewire v3 is still in beta, set the minimum-stability in your composer.json to dev.

composer.json

"minimum-stability": "dev"

Install the Filament Panel Builder by running the following commands in your Laravel project directory.

composer require filament/filament:"^3.0-stable" -W
 
php artisan filament:install --panels

--with-all-dependencies (-W): Update also dependencies of packages in the argument list, including those which are root requirements.


1.2. Set Up User to Access Admin Panel

To set up your User Model to access Filament in non-local environments, you must implement the FilamentUser contract and define the canAccessPanel() method.

use Filament\Models\Contracts\FilamentUser;
use Filament\Panel;
 
class User extends Authenticatable implements FilamentUser
{
// ...
 
public function canAccessPanel(Panel $panel): bool
{
return true;
}
}

The canAccessPanel() method returns true or false depending on whether the user can access the $panel. In this example, we let all users access the panel.

When using Filament, you do not need to define any routes manually. Now you can log in by visiting the /admin path.

Filament Login

After logging in, you should see the following dashboard.

Filament Dashboard

Our dashboard is empty, so let's make a section for managing tracks.


1.3. Filament CRUD Resource for Tracks

Let's allow admins to manage Tracks.

In Filament, resources are static classes used to build CRUD interfaces for your Eloquent models. They describe how administrators can interact with data from your panel using tables and forms.

Use the following artisan command to create a new Filament resource for the Track Model.

php artisan make:filament-resource Track

Then in the table() method columns() call, define TextColumn to display title.

app/Filament/Resources/TrackResource.php

use Filament\Tables\Columns\TextColumn;
 
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('title'),
])
// ...
}

That's it, and when you refresh the dashboard, you should see a new Tracks entry in the menu.

List Tracks

You can explore different column types on Filament Table Columns Documentation.

Now let's define input fields for creating and editing Track in the form() method.

app/Filament/Resources/TrackResource.php

use Filament\Forms\Components\TextInput;
 
public static function form(Form $form): Form
{
return $form
->schema([
TextInput::make('title')->required()->maxLength(255),
]);
}

Validation rules can be applied to the TextInput by calling required() and maxLength() methods.

Now we can edit and create new tracks in just a minute.

Edit Track

Filament will try to save model attributes under the same name you defined by default.

Possible form components and their options are on Filament Fields Documentation.


1.4. Filament: Dynamic Reservation Form

Let's create a Reservation resource.

php artisan make:filament-resource Reservation

And define the create form's date and track fields. We use the getAvailableReservations() static method to get available reservations. Note that in the resource, all methods are static.

app/Filament/Resources/ReservationResource.php

use App\Models\Track;
use Carbon\Carbon;
use Carbon\CarbonPeriod;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Radio;
use Filament\Forms\Get;
 
// ...
 
public static function form(Form $form): Form
{
$dateFormat = 'Y-m-d';
 
return $form
->schema([
DatePicker::make('date')
->native(false)
->minDate(now()->format($dateFormat))
->maxDate(now()->addWeeks(2)->format($dateFormat))
->format($dateFormat)
->required()
->live(),
Radio::make('track')
->options(fn (Get $get) => self::getAvailableReservations($get))
->hidden(fn (Get $get) => ! $get('date'))
->required()
->columnSpan(2),
]);
}
 
public static function getAvailableReservations(Get $get): array
{
$date = Carbon::parse($get('date'));
$startPeriod = $date->copy()->hour(14);
$endPeriod = $date->copy()->hour(16);
$times = CarbonPeriod::create($startPeriod, '1 hour', $endPeriod);
$availableReservations = [];
 
$tracks = Track::with([
'reservations' => function ($q) use ($startPeriod, $endPeriod) {
$q->whereBetween('start_time', [$startPeriod, $endPeriod]);
},
])
->get();
 
foreach ($tracks as $track) {
$reservations = $track->reservations->pluck('start_time')->toArray();
 
$availableTimes = $times->copy()->filter(function ($time) use ($reservations) {
return ! in_array($time, $reservations) && ! $time->isPast();
})->toArray();
 
foreach ($availableTimes as $time) {
$key = $track->id . '-' . $time->format('H');
$availableReservations[$key] = $track->title . ' ' . $time->format('H:i');
}
}
 
return $availableReservations;
}

Now let's get through all the logic.

DatePicker::make('date')
->native(false)
->minDate(now()->format($dateFormat))
->maxDate(now()->addWeeks(2)->format($dateFormat))
->format($dateFormat)
->required()
->live(),

By default DatePicker component uses...

Premium Members Only

This advanced tutorial is available exclusively to Laravel Daily Premium members.

Premium membership includes:

Access to all premium tutorials
Video and Text Courses
Private Discord Channel

Comments & Discussion

BM
Bradley Miller ✓ Link copied!

This. is an excellent and very complete example of how to use this. Thank you!

CV
Christian Villaruel ✓ Link copied!

Love this! Is there a way to access the form object with state in livewire component realtime?

L
Lambert ✓ Link copied!

Excellent tutorial!! Combines a lot of (not all so trivial) stuff!!

I added a filament-livewire-form to a new breeze-project, but the styling seems to conflict. Form-borders etc are disappearing. I did exactly like the tutorial, some was already there by Breeze, so I added it. But something is different, so the forms and buttons are disappearig.

Any suggestions?

L
Lambert ✓ Link copied!

Solved! I missed to add @filamentStyles in the layout.

Briljant tutorial!

AI
Alejandro Ismael Sanchez ✓ Link copied!

I am following the tutorial, and it throws this error:

SyntaxError: Unexpected token 'export'

I had to use this code for postcss.config.js:

module.exports = {
    plugins: {
        tailwindcss: {},
        autoprefixer: {},
    },
};
C
caturria ✓ Link copied!

Hi there,

I'm loving your content. I was just wondering if you know of a way to allow some parts of a Filament panel to be accessible to unauthenticated guest users? My thinking was to use middleware that logs in a "guest" pseudouser with extremely limited permissions if no user is authenticated, but this would require some kind of customization to the "sign out" button to make it a "sign in" button, and a few other things. What are your thoughts on this?

PK
Povilas Korop ✓ Link copied!

We've never done it that way, I think you're overcomplicating it. It should be either for authenticated users (and then you play with their permissions), or for guest public users, like in this tutorial example. That "middle option" you're talking about requires very custom solutions and just not worth that effort, in my opinion.

D
dailer ✓ Link copied!

Is there a repo for this? I tried to replicate the form on the frontend but the styling just broke and I can't figure out where.

M
Modestas ✓ Link copied!

Repository link is at the bottom of the article:

https://github.com/LaravelDaily/Filament-Appointment-Reservations-Demo

DL
David Lun ✓ Link copied!

probably you need to run npm i and npm run dev

S
sergioarc ✓ Link copied!

Wonder if there is a way to "share" resources between panels... Let's say I have some "ClientResource" under some "operators" panel... Is it possible to gain access to this "ClientResource" while you are under the "admin" panel ???

M
Modestas ✓ Link copied!

You can manually register the same resource OR create a new file in admin that extends the operator class.

But to be honest, that's the point of panels - they are supposed to be separated and work individually

S
sergioarc ✓ Link copied!

Ok, I agree with you... But anyway, how do you "manually register" the same resource from one panel in some other ???

D
dailer ✓ Link copied!

can this use the filament notifications and buttons as well?

F
fpolli ✓ Link copied!

With multiple panels and infolists, it seems the public parts of the app can also be made with Filament. Create the admin panel like normal, then a public panel that uses mostly infolists in its pages, and set the roles and permissions accordingly on each panel. No need to have a separate app and deal directly with livewire components.

D
dailer ✓ Link copied!

How to add dark theme?

We'd Love Your Feedback

Tell us what you like or what we can improve

Feel free to share anything you like or dislike about this page or the platform in general.