Courses

Filament Adminpanel for Booking.com API Project

Manage Properties and Apartments

In this lesson, we will manage apartments. In the table, we will automatically calculate and show the average rating. And this resource will have two relation managers for properties and rooms.

apartment tabs


Apartments Table

First, we need a Resource for Apartment.

php artisan make:filament-resource ApartmentResource

Next, let's add a form and a table. For calculating the average rating, Filament has aggrregating relationships helpers.

app/Filament/Resources/ApartmentResource.php:

class ApartmentResource extends Resource
{
protected static ?string $model = Apartment::class;
 
protected static ?string $navigationIcon = 'heroicon-o-collection';
 
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('name')
->required()
->maxLength(255),
Forms\Components\Select::make('apartment_type_id')
->preload()
->required()
->searchable()
->relationship('apartment_type', 'name'),
Forms\Components\Select::make('property_id')
->preload()
->required()
->searchable()
->relationship('property', 'name'),
Forms\Components\TextInput::make('capacity_adults')
->integer()
->required()
->minValue(0),
Forms\Components\TextInput::make('capacity_children')
->integer()
->required()
->minValue(0),
Forms\Components\TextInput::make('size')
->integer()
->required()
->minValue(0),
Forms\Components\TextInput::make('bathrooms')
->integer()
->required()
->minValue(0),
]);
}
 
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('name'),
Tables\Columns\TextColumn::make('apartment_type.name'),
Tables\Columns\TextColumn::make('property.name'),
Tables\Columns\TextColumn::make('size'),
Tables\Columns\TextColumn::make('bookings_avg_rating')
->label('Rating')
->placeholder(0)
->avg('bookings', 'rating')
->formatStateUsing(fn (?string $state): ?string => number_format($state, 2)),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\DeleteBulkAction::make(),
]);
}
// ...
}

apartments table


Showing Property for Apartments

Every apartment belongs to a property. So now, let's add a Relation Manager for the Properties.

php artisan make:filament-relation-manager ApartmentResource property name

app/Filament/Resources/ApartmentResource.php:

class ApartmentResource extends Resource
{
// ...
public static function getRelations(): array
{
return [
RelationManagers\PropertyRelationManager::class,
];
}
// ...
}

For the apartment, now it shows properties, but only with the name field.

property relation added

So, let's add more fields to the table and the form.

app/Filament/Resources/ApartmentResource/RelationManagers/PropertyRelationManager.php:

use App\Rules\LatitudeRule;
use App\Rules\LongitudeRule;
 
class PropertyRelationManager extends RelationManager
{
protected static string $relationship = 'property';
 
protected static ?string $recordTitleAttribute = 'name';
 
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('name')
->required(),
Forms\Components\TextInput::make('address_street')
->required(),
Forms\Components\TextInput::make('address_postcode')
->required(),
Forms\Components\Select::make('city_id')
->relationship('city', 'name')
->preload()
->required()
->searchable(),
Forms\Components\TextInput::make('lat')
->required()
->rules([new LatitudeRule()]),
Forms\Components\TextInput::make('long')
->required()
->rules([new LongitudeRule()]),
Forms\Components\Select::make('owner_id')
->relationship('owner', 'name')
->required()
->searchable(),
]);
}
 
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('name'),
Tables\Columns\TextColumn::make('owner.name'),
Tables\Columns\TextColumn::make('address'),
Tables\Columns\TextColumn::make('city.name'),
])
->filters([
//
])
->headerActions([
Tables\Actions\CreateAction::make(),
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
])
->bulkActions([
Tables\Actions\DeleteBulkAction::make(),
]);
}
}

To get the property owner, we need to add this relation to the Property model.

app/Models/Property.php:

class Property extends Model implements HasMedia
{
// ...
public function owner(): BelongsTo
{
return $this->belongsTo(User::class, 'owner_id');
}
}

Now we have table with more information.

property table

And form with more fields.

property form


Image upload

Next, we need to add image upload for the properties. In the Re-creating Booking.com API with Laravel and PHPUnit course, we used spatie/laravel-medialibrary package to handle images.

Luckily, Filament has a plugin to upload files and use the same Media Library package. Let's install this plugin and use it.

composer require filament/spatie-laravel-media-library-plugin:"^2.0"

app/Filament/Resources/ApartmentResource/RelationManagers/PropertyRelationManager.php:

class PropertyRelationManager extends RelationManager
{
protected static string $relationship = 'property';
 
protected static ?string $recordTitleAttribute = 'name';
 
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('name')
->required(),
Forms\Components\TextInput::make('address_street')
->required(),
Forms\Components\TextInput::make('address_postcode')
->required(),
Forms\Components\Select::make('city_id')
->relationship('city', 'name')
->preload()
->required()
->searchable(),
Forms\Components\TextInput::make('lat')
->required()
->rules([new LatitudeRule()]),
Forms\Components\TextInput::make('long')
->required()
->rules([new LongitudeRule()]),
Forms\Components\Select::make('owner_id')
->relationship('owner', 'name')
->required()
->searchable(),
Forms\Components\SpatieMediaLibraryFileUpload::make('photo')
->image()
->maxSize(5000)
->multiple()
->columnSpanFull()
->collection('avatars')
->conversion('thumbnail'),
]);
}
// ...
}

That's how easily we added file upload.

property file upload

Next, let's add rooms relation manager.

php artisan make:filament-relation-manager ApartmentResource rooms name

app/Filament/Resources/ApartmentResource.php:

class ApartmentResource extends Resource
{
// ...
public static function getRelations(): array
{
return [
RelationManagers\PropertyRelationManager::class,
RelationManagers\RoomsRelationManager::class,
];
}
// ...
}

This adds a second tab in the relation manager.

rooms relation manager

Next, add additional field to form and table.

app/Filament/Resources/ApartmentResource/RelationManagers/RoomsRelationManager.php:

class RoomsRelationManager extends RelationManager
{
protected static string $relationship = 'rooms';
 
protected static ?string $recordTitleAttribute = 'name';
 
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('name')
->required()
->maxLength(255),
Forms\Components\Select::make('room_type_id')
->preload()
->required()
->searchable()
->relationship('room_type', 'name'),
]);
}
 
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('name'),
Tables\Columns\TextColumn::make('room_type.name'),
])
->filters([
//
])
->headerActions([
Tables\Actions\CreateAction::make(),
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
])
->bulkActions([
Tables\Actions\DeleteBulkAction::make(),
]);
}
}

Also, we can move relation managers to tabs.

app/Filament/Resources/ApartmentResource/Pages/EditApartment.php:

class EditApartment extends EditRecord
{
protected static string $resource = ApartmentResource::class;
 
protected function getActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
 
public function hasCombinedRelationManagerTabsWithForm(): bool
{
return true;
}
}

apartment tabs

avatar

In app/Filament/Resources/ApartmentResource.php:

there is mistake one line => RelationManagers\PropertiesRelationManager::class,

should be => RelationManagers\PropertyRelationManager::class,

avatar

Thank you, dear Editor of our team :) Fixed now!

avatar
You can use Markdown
avatar

Hi Povilas. There is a little problem that we can't create a new Property if we haven't any Apartments now. And we can't add an Apartment without Property. I think the main entity here is a Property but not an Apartment.

avatar

I think it worked well for me: first create a property, then apartments inside of it. What error are you getting when creating a property?

avatar

The problem is I can't get a Property add form if I haven't any Apartments. This is 2 examples what I say: https://disk.yandex.ru/i/OwP2oG3Omb2DDQ https://disk.yandex.ru/i/6rsX_IvwRp88fw How to create a Property in this case? :)

avatar

Oh now I understood the problem. according to the logic of the application, admins don't create apartments and properties, users do that via api, admins only edit them via filament adminpanel.

It would not be logical if admins added properties for someone.

avatar

Yes, that's right, agree with you. Thanx

avatar
You can use Markdown
avatar

Hi Povilas, Nice course.

Just wanted to say that it's better to remove the "preload()" for Property dropdown from ApartmentResource.php (Edit Apartment) => Forms\Components\Select::make('property_id')

I used the Performance Seeds from related course and I have thousands of properties (99k) and didn't know why the Edit Apartments page was loading in 30 seconds. I noticed the preload(), it loads all 99k properties on that dropdown, so it's better to remove it. :)

Thank you for all courses and especially for 'Booking' ones!

avatar

Hi, thanks for the kind words and a suggestion. We will take a look if that makes sense as an edit here or maybe a separate article as a warning

avatar

Sadly hasCombinedRelationManagerTabsWithForm is no longer an option in Filament v3.

avatar
You can use Markdown
avatar

PropertyRelationManager seems a bit confusing because in our system, an Apartment belongs to a Property rather than having a belongs to many relationship. I would directly include the action for creating a Property within the select.

Forms\Components\Select::make('property_id')
	->preload()
	->required()
	->searchable()
	->relationship('property', 'name')
	->createOptionForm([
		//..
	]),
avatar
You can use Markdown
avatar
You can use Markdown