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

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' => 'admin@admin.com',
'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...

The full tutorial [22 mins, 4231 words] is only for Premium Members

Login Or Become a Premium Member for $129/year or $29/month
What else you will get:
  • 59 courses (1056 lessons, total 42 h 44 min)
  • 78 long-form tutorials (one new every week)
  • access to project repositories
  • access to private Discord

Recent Premium Tutorials