Membuat form multi-step wizard di Laravel tidak pernah semudah ini. Dengan Filament 4 dan Laravel 12, kamu bisa membangun admin panel property management lengkap dengan wizard form untuk listing properti dalam hitungan jam. Tutorial ini akan memandu kamu step-by-step membuat sistem House Management dengan fitur wizard 6 langkah — mulai dari informasi dasar properti, detail ruangan, fasilitas, harga, upload foto, hingga review dan submit. Cocok untuk developer yang ingin membangun aplikasi rental properti, manajemen kos-kosan, atau platform listing rumah dengan Laravel dan Filament.
Bagian 1: Intro & Setup
Apa itu Wizard di Filament?
Wizard adalah multi-step form yang memecah form panjang menjadi beberapa langkah. Filament 4 punya built-in Wizard component yang powerful — tinggal pakai, tidak perlu bikin dari nol.
KAPAN PAKAI WIZARD:
├── Form terlalu panjang untuk satu halaman
├── Proses butuh tahapan logis (step 1, 2, 3...)
├── User experience lebih baik dengan guidance
└── Validasi per tahap sebelum lanjut
Project: House Management
Kita akan bikin sistem untuk listing properti dengan wizard 6 step:
WIZARD FLOW:
Step 1: Informasi Dasar ──► Step 2: Detail Ruangan ──► Step 3: Fasilitas
│
Step 6: Review & Submit ◄── Step 5: Foto & Dokumen ◄── Step 4: Harga
Quick Setup
Install Laravel 12:
composer create-project laravel/laravel house-management
cd house-management
Setup Database (SQLite untuk simple):
# Edit .env
# DB_CONNECTION=sqlite
# Hapus DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, DB_PASSWORD
# Buat file database
touch database/database.sqlite
Install Filament 4:
composer require filament/filament:"^4.0"
php artisan filament:install --panels
# Ketik: admin
Jalankan Migration & Buat User:
php artisan migrate
php artisan make:filament-user
# Name: Admin
# Email: [email protected]
# Password: password
Test Akses:
php artisan serve
# Buka: <http://localhost:8000/admin>
Login dan pastikan dashboard Filament muncul. Setup selesai ✅
Bagian 2: Migration & Model
Migration 1: Facilities (Master Data)
php artisan make:migration create_facilities_table
<?php
use Illuminate\\Database\\Migrations\\Migration;
use Illuminate\\Database\\Schema\\Blueprint;
use Illuminate\\Support\\Facades\\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('facilities', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('icon')->nullable();
$table->boolean('is_active')->default(true);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('facilities');
}
};
Migration 2: Properties
php artisan make:migration create_properties_table
<?php
use Illuminate\\Database\\Migrations\\Migration;
use Illuminate\\Database\\Schema\\Blueprint;
use Illuminate\\Support\\Facades\\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('properties', function (Blueprint $table) {
$table->id();
// Step 1: Basic Info
$table->string('name');
$table->enum('type', ['house', 'apartment', 'kos', 'villa']);
$table->text('address');
$table->text('description')->nullable();
// Step 2: Room Details
$table->integer('bedrooms')->default(1);
$table->integer('bathrooms')->default(1);
$table->integer('building_size');
$table->integer('land_size')->nullable();
// Step 3: Facilities
$table->enum('furnished', ['unfurnished', 'semi', 'full'])->default('unfurnished');
$table->json('additional_facilities')->nullable();
// Step 4: Pricing
$table->decimal('price_monthly', 12, 2);
$table->integer('minimum_rent')->default(1);
$table->decimal('deposit', 12, 2)->nullable();
$table->date('available_from')->nullable();
// Step 5: Media
$table->json('photos')->nullable();
$table->string('certificate')->nullable();
$table->string('virtual_tour_url')->nullable();
// Status
$table->enum('status', ['draft', 'available', 'rented', 'maintenance'])->default('draft');
$table->boolean('is_verified')->default(false);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('properties');
}
};
Migration 3: Pivot Table
php artisan make:migration create_property_facility_table
<?php
use Illuminate\\Database\\Migrations\\Migration;
use Illuminate\\Database\\Schema\\Blueprint;
use Illuminate\\Support\\Facades\\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('property_facility', function (Blueprint $table) {
$table->id();
$table->foreignId('property_id')->constrained()->cascadeOnDelete();
$table->foreignId('facility_id')->constrained()->cascadeOnDelete();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('property_facility');
}
};
Model: Facility
php artisan make:model Facility
<?php
namespace App\\Models;
use Illuminate\\Database\\Eloquent\\Model;
use Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;
class Facility extends Model
{
protected $fillable = [
'name',
'icon',
'is_active',
];
protected $casts = [
'is_active' => 'boolean',
];
public function properties(): BelongsToMany
{
return $this->belongsToMany(Property::class);
}
}
Model: Property
php artisan make:model Property
<?php
namespace App\\Models;
use Illuminate\\Database\\Eloquent\\Model;
use Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;
class Property extends Model
{
protected $fillable = [
'name',
'type',
'address',
'description',
'bedrooms',
'bathrooms',
'building_size',
'land_size',
'furnished',
'additional_facilities',
'price_monthly',
'minimum_rent',
'deposit',
'available_from',
'photos',
'certificate',
'virtual_tour_url',
'status',
'is_verified',
];
protected $casts = [
'photos' => 'array',
'additional_facilities' => 'array',
'available_from' => 'date',
'price_monthly' => 'decimal:2',
'deposit' => 'decimal:2',
'is_verified' => 'boolean',
];
public function facilities(): BelongsToMany
{
return $this->belongsToMany(Facility::class);
}
}
Seeder: Facilities
php artisan make:seeder FacilitySeeder
<?php
namespace Database\\Seeders;
use App\\Models\\Facility;
use Illuminate\\Database\\Seeder;
class FacilitySeeder extends Seeder
{
public function run(): void
{
$facilities = [
['name' => 'AC', 'icon' => 'heroicon-o-sun'],
['name' => 'WiFi', 'icon' => 'heroicon-o-wifi'],
['name' => 'Parkir Mobil', 'icon' => 'heroicon-o-truck'],
['name' => 'Parkir Motor', 'icon' => 'heroicon-o-truck'],
['name' => 'Keamanan 24 Jam', 'icon' => 'heroicon-o-shield-check'],
['name' => 'CCTV', 'icon' => 'heroicon-o-video-camera'],
['name' => 'Kolam Renang', 'icon' => 'heroicon-o-sparkles'],
['name' => 'Gym', 'icon' => 'heroicon-o-heart'],
['name' => 'Taman', 'icon' => 'heroicon-o-sun'],
['name' => 'Laundry', 'icon' => 'heroicon-o-archive-box'],
['name' => 'Water Heater', 'icon' => 'heroicon-o-fire'],
['name' => 'Dapur', 'icon' => 'heroicon-o-home'],
];
foreach ($facilities as $facility) {
Facility::create([
'name' => $facility['name'],
'icon' => $facility['icon'],
'is_active' => true,
]);
}
}
}
Update DatabaseSeeder:
<?php
namespace Database\\Seeders;
use Illuminate\\Database\\Seeder;
class DatabaseSeeder extends Seeder
{
public function run(): void
{
$this->call([
FacilitySeeder::class,
]);
}
}
Jalankan Migration & Seeder:
php artisan migrate:fresh --seed
Buat ulang user admin:
php artisan make:filament-user
Database siap ✅
Bagian 3: Wizard Resource - Part 1
Generate Resource
php artisan make:filament-resource Property
Setup Wizard Structure
Edit app/Filament/Resources/PropertyResource.php:
<?php
namespace App\\Filament\\Resources;
use App\\Filament\\Resources\\PropertyResource\\Pages;
use App\\Models\\Property;
use Filament\\Forms;
use Filament\\Forms\\Form;
use Filament\\Forms\\Components\\Wizard;
use Filament\\Forms\\Components\\Wizard\\Step;
use Filament\\Resources\\Resource;
use Filament\\Tables;
use Filament\\Tables\\Table;
use Illuminate\\Support\\HtmlString;
use Illuminate\\Support\\Facades\\Blade;
class PropertyResource extends Resource
{
protected static ?string $model = Property::class;
protected static ?string $navigationIcon = 'heroicon-o-home-modern';
protected static ?string $navigationLabel = 'Properti';
public static function form(Form $form): Form
{
return $form
->schema([
Wizard::make([
self::getBasicInfoStep(),
self::getRoomDetailsStep(),
self::getFacilitiesStep(),
self::getPricingStep(),
self::getMediaStep(),
self::getReviewStep(),
])
->skippable()
->persistStepInQueryString()
->columnSpanFull(),
]);
}
// Step methods akan ditambahkan di bawah...
public static function table(Table $table): Table
{
return $table
->columns([
//
]);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\\ListProperties::route('/'),
'create' => Pages\\CreateProperty::route('/create'),
'edit' => Pages\\EditProperty::route('/{record}/edit'),
];
}
}
Step 1: Informasi Dasar
Tambahkan method ini di PropertyResource.php:
private static function getBasicInfoStep(): Step
{
return Step::make('Informasi Dasar')
->icon('heroicon-o-home')
->description('Data utama properti')
->schema([
Forms\\Components\\TextInput::make('name')
->label('Nama Properti')
->placeholder('Contoh: Rumah Minimalis Menteng')
->required()
->maxLength(255)
->columnSpanFull(),
Forms\\Components\\Select::make('type')
->label('Tipe Properti')
->options([
'house' => '🏠 Rumah',
'apartment' => '🏢 Apartemen',
'kos' => '🏘️ Kos',
'villa' => '🏡 Villa',
])
->required()
->native(false),
Forms\\Components\\Select::make('status')
->label('Status')
->options([
'draft' => '📝 Draft',
'available' => '✅ Available',
'rented' => '🔑 Rented',
'maintenance' => '🔧 Maintenance',
])
->default('draft')
->required()
->native(false),
Forms\\Components\\Textarea::make('address')
->label('Alamat Lengkap')
->placeholder('Jl. Contoh No. 123, Kelurahan, Kecamatan, Kota')
->required()
->rows(3)
->columnSpanFull(),
Forms\\Components\\RichEditor::make('description')
->label('Deskripsi Properti')
->placeholder('Jelaskan keunggulan properti ini...')
->toolbarButtons([
'bold',
'italic',
'bulletList',
'orderedList',
])
->columnSpanFull(),
])->columns(2);
}
Step 2: Detail Ruangan
private static function getRoomDetailsStep(): Step
{
return Step::make('Detail Ruangan')
->icon('heroicon-o-square-3-stack-3d')
->description('Spesifikasi ruangan dan ukuran')
->schema([
Forms\\Components\\TextInput::make('bedrooms')
->label('Jumlah Kamar Tidur')
->numeric()
->required()
->default(1)
->minValue(1)
->maxValue(20)
->suffix('kamar'),
Forms\\Components\\TextInput::make('bathrooms')
->label('Jumlah Kamar Mandi')
->numeric()
->required()
->default(1)
->minValue(1)
->maxValue(10)
->suffix('kamar'),
Forms\\Components\\TextInput::make('building_size')
->label('Luas Bangunan')
->numeric()
->required()
->minValue(10)
->suffix('m²')
->helperText('Luas total bangunan'),
Forms\\Components\\TextInput::make('land_size')
->label('Luas Tanah')
->numeric()
->suffix('m²')
->helperText('Kosongkan jika apartemen atau kos'),
Forms\\Components\\Placeholder::make('size_info')
->label('')
->content(new HtmlString('
<div class="text-sm text-gray-500 bg-gray-50 dark:bg-gray-800 p-3 rounded-lg">
<strong>💡 Tips:</strong><br>
• Untuk apartemen/kos, cukup isi luas bangunan<br>
• Untuk rumah/villa, isi keduanya
</div>
'))
->columnSpanFull(),
])->columns(2);
}
Placeholder untuk Step Lainnya (Sementara)
Tambahkan placeholder dulu supaya tidak error:
private static function getFacilitiesStep(): Step
{
return Step::make('Fasilitas')
->icon('heroicon-o-cog-6-tooth')
->description('Pilih fasilitas')
->schema([
Forms\\Components\\Placeholder::make('coming')
->content('Step 3 - akan diisi di bagian berikutnya'),
]);
}
private static function getPricingStep(): Step
{
return Step::make('Harga')
->icon('heroicon-o-banknotes')
->description('Tentukan harga sewa')
->schema([
Forms\\Components\\Placeholder::make('coming')
->content('Step 4 - akan diisi di bagian berikutnya'),
]);
}
private static function getMediaStep(): Step
{
return Step::make('Media')
->icon('heroicon-o-photo')
->description('Upload foto')
->schema([
Forms\\Components\\Placeholder::make('coming')
->content('Step 5 - akan diisi di bagian berikutnya'),
]);
}
private static function getReviewStep(): Step
{
return Step::make('Review')
->icon('heroicon-o-clipboard-document-check')
->description('Periksa data')
->schema([
Forms\\Components\\Placeholder::make('coming')
->content('Step 6 - akan diisi di bagian berikutnya'),
]);
}
Test Wizard
Buka browser: http://localhost:8000/admin/properties/create
Kamu akan lihat wizard dengan 6 step. Step 1 dan 2 sudah bisa diisi, sisanya placeholder.
CHECKPOINT ✅
☑️ Wizard muncul dengan 6 steps
☑️ Step 1: Form info dasar working
☑️ Step 2: Form detail ruangan working
☑️ Navigation Next/Previous working
☑️ Step indicator di atas working
☑️ Skippable (bisa loncat step)
Step 1-2 selesai! Lanjut ke bagian berikutnya untuk Step 3, 4, 5, dan 6.
Bagian 4: Wizard Resource - Part 2
Lanjut ke Step 3, 4, dan 5. Ganti placeholder methods yang tadi dengan code lengkap.
Step 3: Fasilitas
private static function getFacilitiesStep(): Step
{
return Step::make('Fasilitas')
->icon('heroicon-o-cog-6-tooth')
->description('Pilih fasilitas yang tersedia')
->schema([
Forms\\Components\\Select::make('furnished')
->label('Status Furnish')
->options([
'unfurnished' => '📦 Kosongan',
'semi' => '🛋️ Semi Furnished',
'full' => '🏠 Full Furnished',
])
->required()
->native(false)
->helperText('Semi = ada sebagian furniture, Full = lengkap siap huni')
->columnSpanFull(),
Forms\\Components\\CheckboxList::make('facilities')
->label('Fasilitas Utama')
->relationship('facilities', 'name')
->columns(3)
->gridDirection('row')
->bulkToggleable()
->searchable()
->columnSpanFull(),
Forms\\Components\\Repeater::make('additional_facilities')
->label('Fasilitas Tambahan')
->schema([
Forms\\Components\\TextInput::make('name')
->label('Nama')
->placeholder('Contoh: Jacuzzi')
->required(),
Forms\\Components\\TextInput::make('note')
->label('Keterangan')
->placeholder('Contoh: Di kamar utama'),
])
->columns(2)
->defaultItems(0)
->addActionLabel('+ Tambah Fasilitas Lain')
->reorderable()
->collapsible()
->itemLabel(fn (array $state): ?string => $state['name'] ?? null)
->columnSpanFull(),
]);
}
Fitur:
CheckboxListdengan relationship — otomatis sync ke pivot tablebulkToggleable— select/deselect allRepeateruntuk fasilitas custom yang tidak ada di master
Step 4: Harga & Ketersediaan
private static function getPricingStep(): Step
{
return Step::make('Harga')
->icon('heroicon-o-banknotes')
->description('Tentukan harga dan ketersediaan')
->schema([
Forms\\Components\\TextInput::make('price_monthly')
->label('Harga Sewa per Bulan')
->numeric()
->required()
->prefix('Rp')
->minValue(100000)
->maxValue(9999999999)
->live(onBlur: true),
Forms\\Components\\TextInput::make('minimum_rent')
->label('Minimum Sewa')
->numeric()
->required()
->default(1)
->minValue(1)
->maxValue(24)
->suffix('bulan')
->helperText('Berapa bulan minimal penyewa harus kontrak'),
Forms\\Components\\TextInput::make('deposit')
->label('Deposit')
->numeric()
->prefix('Rp')
->helperText('Kosongkan jika tidak ada deposit'),
Forms\\Components\\DatePicker::make('available_from')
->label('Tersedia Mulai Tanggal')
->default(now())
->native(false)
->displayFormat('d M Y')
->helperText('Kapan properti bisa mulai disewa'),
Forms\\Components\\Placeholder::make('price_summary')
->label('Ringkasan Biaya')
->content(function ($get) {
$price = $get('price_monthly') ?? 0;
$deposit = $get('deposit') ?? 0;
$minRent = $get('minimum_rent') ?? 1;
$totalMin = ($price * $minRent) + $deposit;
return new \\Illuminate\\Support\\HtmlString("
<div class='text-sm bg-primary-50 dark:bg-primary-900/20 p-3 rounded-lg'>
<div class='font-medium text-primary-600 dark:text-primary-400'>Estimasi Biaya Awal:</div>
<div class='mt-1'>Sewa {$minRent} bulan: <strong>Rp " . number_format($price * $minRent, 0, ',', '.') . "</strong></div>
<div>Deposit: <strong>Rp " . number_format($deposit, 0, ',', '.') . "</strong></div>
<div class='mt-1 pt-1 border-t border-primary-200 dark:border-primary-700'>
Total: <strong class='text-lg'>Rp " . number_format($totalMin, 0, ',', '.') . "</strong>
</div>
</div>
");
})
->columnSpanFull(),
])->columns(2);
}
Fitur:
live(onBlur: true)— update placeholder saat input berubah- Dynamic
Placeholder— menghitung estimasi biaya otomatis
Step 5: Foto & Dokumen
private static function getMediaStep(): Step
{
return Step::make('Media')
->icon('heroicon-o-photo')
->description('Upload foto dan dokumen pendukung')
->schema([
Forms\\Components\\FileUpload::make('photos')
->label('Foto Properti')
->multiple()
->image()
->imageEditor()
->directory('properties/photos')
->maxFiles(10)
->maxSize(2048)
->reorderable()
->panelLayout('grid')
->helperText('Upload maksimal 10 foto (maks 2MB per file). Foto pertama jadi cover.')
->columnSpanFull(),
Forms\\Components\\FileUpload::make('certificate')
->label('Sertifikat / Dokumen Legalitas')
->directory('properties/certificates')
->acceptedFileTypes(['application/pdf'])
->maxSize(5120)
->helperText('Format PDF, maksimal 5MB (opsional)'),
Forms\\Components\\TextInput::make('virtual_tour_url')
->label('Link Virtual Tour')
->url()
->placeholder('https://...')
->prefixIcon('heroicon-o-video-camera')
->helperText('Link 360° tour atau video YouTube (opsional)'),
])->columns(2);
}
Fitur:
multiple()+reorderable()— upload banyak foto dan bisa diurutkanpanelLayout('grid')— tampilan grid untuk previewimageEditor()— crop dan edit langsung di browser- Accepted file types untuk validasi
Bagian 5: Review Step & Validation
Step 6: Review & Submit
private static function getReviewStep(): Step
{
return Step::make('Review')
->icon('heroicon-o-clipboard-document-check')
->description('Periksa kembali semua data')
->schema([
Forms\\Components\\Section::make('📋 Ringkasan Properti')
->schema([
Forms\\Components\\Placeholder::make('review_name')
->label('Nama Properti')
->content(fn ($get) => $get('name') ?: '-'),
Forms\\Components\\Placeholder::make('review_type')
->label('Tipe')
->content(fn ($get) => match($get('type')) {
'house' => '🏠 Rumah',
'apartment' => '🏢 Apartemen',
'kos' => '🏘️ Kos',
'villa' => '🏡 Villa',
default => '-'
}),
Forms\\Components\\Placeholder::make('review_address')
->label('Alamat')
->content(fn ($get) => $get('address') ?: '-'),
])->columns(3),
Forms\\Components\\Section::make('🏠 Spesifikasi')
->schema([
Forms\\Components\\Placeholder::make('review_rooms')
->label('Kamar')
->content(fn ($get) => ($get('bedrooms') ?? 0) . ' Kamar Tidur, ' . ($get('bathrooms') ?? 0) . ' Kamar Mandi'),
Forms\\Components\\Placeholder::make('review_size')
->label('Luas')
->content(function ($get) {
$building = $get('building_size') ?? 0;
$land = $get('land_size');
return $building . ' m² (bangunan)' . ($land ? ", {$land} m² (tanah)" : '');
}),
Forms\\Components\\Placeholder::make('review_furnished')
->label('Furnished')
->content(fn ($get) => match($get('furnished')) {
'unfurnished' => '📦 Kosongan',
'semi' => '🛋️ Semi Furnished',
'full' => '🏠 Full Furnished',
default => '-'
}),
])->columns(3),
Forms\\Components\\Section::make('💰 Harga')
->schema([
Forms\\Components\\Placeholder::make('review_price')
->label('Harga per Bulan')
->content(fn ($get) => 'Rp ' . number_format($get('price_monthly') ?? 0, 0, ',', '.')),
Forms\\Components\\Placeholder::make('review_minimum')
->label('Minimum Sewa')
->content(fn ($get) => ($get('minimum_rent') ?? 1) . ' bulan'),
Forms\\Components\\Placeholder::make('review_deposit')
->label('Deposit')
->content(fn ($get) => $get('deposit') ? 'Rp ' . number_format($get('deposit'), 0, ',', '.') : 'Tidak ada'),
Forms\\Components\\Placeholder::make('review_available')
->label('Tersedia Mulai')
->content(fn ($get) => $get('available_from') ? \\Carbon\\Carbon::parse($get('available_from'))->format('d M Y') : 'Segera'),
])->columns(4),
Forms\\Components\\Section::make('📸 Media')
->schema([
Forms\\Components\\Placeholder::make('review_photos')
->label('Foto')
->content(fn ($get) => count($get('photos') ?? []) . ' foto diupload'),
Forms\\Components\\Placeholder::make('review_certificate')
->label('Sertifikat')
->content(fn ($get) => $get('certificate') ? '✅ Ada' : '❌ Tidak ada'),
Forms\\Components\\Placeholder::make('review_tour')
->label('Virtual Tour')
->content(fn ($get) => $get('virtual_tour_url') ? '✅ Ada' : '❌ Tidak ada'),
])->columns(3),
Forms\\Components\\Section::make('✅ Konfirmasi')
->schema([
Forms\\Components\\Checkbox::make('terms_accepted')
->label('Saya menyatakan bahwa semua data yang diisi sudah benar dan dapat dipertanggungjawabkan')
->accepted()
->validationMessages([
'accepted' => 'Anda harus menyetujui pernyataan ini untuk melanjutkan',
]),
]),
]);
}
Update Wizard dengan Submit Action
Update method form() di PropertyResource.php:
public static function form(Form $form): Form
{
return $form
->schema([
Wizard::make([
self::getBasicInfoStep(),
self::getRoomDetailsStep(),
self::getFacilitiesStep(),
self::getPricingStep(),
self::getMediaStep(),
self::getReviewStep(),
])
->skippable()
->persistStepInQueryString()
->submitAction(new HtmlString(
Blade::render(<<<BLADE
<x-filament::button
type="submit"
size="lg"
color="success"
icon="heroicon-o-check-circle"
>
Submit Properti
</x-filament::button>
BLADE)
))
->columnSpanFull(),
]);
}
Jangan lupa tambah import di atas file:
use Illuminate\\Support\\HtmlString;
use Illuminate\\Support\\Facades\\Blade;
Bagian 6: Table & Widget
Table Columns
Update method table() di PropertyResource.php:
public static function table(Table $table): Table
{
return $table
->columns([
Tables\\Columns\\ImageColumn::make('photos')
->label('Foto')
->circular()
->stacked()
->limit(3)
->limitedRemainingText(),
Tables\\Columns\\TextColumn::make('name')
->label('Properti')
->searchable()
->sortable()
->description(fn ($record) => match($record->type) {
'house' => '🏠 Rumah',
'apartment' => '🏢 Apartemen',
'kos' => '🏘️ Kos',
'villa' => '🏡 Villa',
default => $record->type
}),
Tables\\Columns\\TextColumn::make('bedrooms')
->label('Kamar')
->formatStateUsing(fn ($record) => $record->bedrooms . ' KT / ' . $record->bathrooms . ' KM')
->badge()
->color('gray'),
Tables\\Columns\\TextColumn::make('building_size')
->label('Luas')
->formatStateUsing(fn ($state) => $state . ' m²')
->sortable(),
Tables\\Columns\\TextColumn::make('price_monthly')
->label('Harga/Bulan')
->money('IDR')
->sortable(),
Tables\\Columns\\TextColumn::make('status')
->label('Status')
->badge()
->color(fn (string $state) => match ($state) {
'draft' => 'gray',
'available' => 'success',
'rented' => 'warning',
'maintenance' => 'danger',
})
->formatStateUsing(fn (string $state) => match ($state) {
'draft' => 'Draft',
'available' => 'Available',
'rented' => 'Rented',
'maintenance' => 'Maintenance',
}),
Tables\\Columns\\IconColumn::make('is_verified')
->label('Verified')
->boolean()
->trueIcon('heroicon-o-check-badge')
->falseIcon('heroicon-o-x-circle'),
Tables\\Columns\\TextColumn::make('created_at')
->label('Dibuat')
->dateTime('d M Y')
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->defaultSort('created_at', 'desc')
->filters([
Tables\\Filters\\SelectFilter::make('type')
->label('Tipe')
->options([
'house' => 'Rumah',
'apartment' => 'Apartemen',
'kos' => 'Kos',
'villa' => 'Villa',
]),
Tables\\Filters\\SelectFilter::make('status')
->options([
'draft' => 'Draft',
'available' => 'Available',
'rented' => 'Rented',
'maintenance' => 'Maintenance',
]),
Tables\\Filters\\SelectFilter::make('furnished')
->options([
'unfurnished' => 'Kosongan',
'semi' => 'Semi Furnished',
'full' => 'Full Furnished',
]),
Tables\\Filters\\TernaryFilter::make('is_verified')
->label('Verified'),
])
->actions([
Tables\\Actions\\Action::make('verify')
->label('Verify')
->icon('heroicon-o-check-badge')
->color('success')
->requiresConfirmation()
->modalHeading('Verifikasi Properti')
->modalDescription('Apakah Anda yakin ingin memverifikasi properti ini?')
->visible(fn ($record) => !$record->is_verified)
->action(fn ($record) => $record->update(['is_verified' => true])),
Tables\\Actions\\EditAction::make(),
Tables\\Actions\\DeleteAction::make(),
])
->bulkActions([
Tables\\Actions\\BulkActionGroup::make([
Tables\\Actions\\DeleteBulkAction::make(),
]),
]);
}
Stats Widget
Buat widget:
php artisan make:filament-widget PropertyStatsWidget --stats-overview
Edit app/Filament/Widgets/PropertyStatsWidget.php:
<?php
namespace App\\Filament\\Widgets;
use App\\Models\\Property;
use Filament\\Widgets\\StatsOverviewWidget as BaseWidget;
use Filament\\Widgets\\StatsOverviewWidget\\Stat;
class PropertyStatsWidget extends BaseWidget
{
protected function getStats(): array
{
$total = Property::count();
$available = Property::where('status', 'available')->count();
$rented = Property::where('status', 'rented')->count();
$pendingVerify = Property::where('is_verified', false)->count();
$totalValue = Property::where('status', 'rented')->sum('price_monthly');
return [
Stat::make('Total Properti', $total)
->description('Semua listing')
->descriptionIcon('heroicon-m-home')
->color('primary'),
Stat::make('Available', $available)
->description('Siap disewa')
->descriptionIcon('heroicon-m-check-circle')
->color('success'),
Stat::make('Rented', $rented)
->description('Rp ' . number_format($totalValue, 0, ',', '.') . '/bulan')
->descriptionIcon('heroicon-m-key')
->color('warning'),
Stat::make('Pending Verify', $pendingVerify)
->description('Butuh verifikasi')
->descriptionIcon('heroicon-m-clock')
->color($pendingVerify > 0 ? 'danger' : 'gray'),
];
}
}
Facility Resource (Simple CRUD)
Untuk manage master data fasilitas:
php artisan make:filament-resource Facility --generate
Edit app/Filament/Resources/FacilityResource.php:
<?php
namespace App\\Filament\\Resources;
use App\\Filament\\Resources\\FacilityResource\\Pages;
use App\\Models\\Facility;
use Filament\\Forms;
use Filament\\Forms\\Form;
use Filament\\Resources\\Resource;
use Filament\\Tables;
use Filament\\Tables\\Table;
class FacilityResource extends Resource
{
protected static ?string $model = Facility::class;
protected static ?string $navigationIcon = 'heroicon-o-cog-6-tooth';
protected static ?string $navigationLabel = 'Fasilitas';
protected static ?string $navigationGroup = 'Master Data';
public static function form(Form $form): Form
{
return $form
->schema([
Forms\\Components\\TextInput::make('name')
->label('Nama Fasilitas')
->required()
->maxLength(255),
Forms\\Components\\TextInput::make('icon')
->label('Icon (Heroicon)')
->placeholder('heroicon-o-wifi')
->helperText('Lihat: heroicons.com'),
Forms\\Components\\Toggle::make('is_active')
->label('Aktif')
->default(true),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\\Columns\\TextColumn::make('name')
->label('Nama')
->searchable(),
Tables\\Columns\\TextColumn::make('icon')
->label('Icon'),
Tables\\Columns\\TextColumn::make('properties_count')
->label('Dipakai')
->counts('properties')
->badge(),
Tables\\Columns\\IconColumn::make('is_active')
->label('Aktif')
->boolean(),
])
->actions([
Tables\\Actions\\EditAction::make(),
Tables\\Actions\\DeleteAction::make(),
]);
}
public static function getPages(): array
{
return [
'index' => Pages\\ListFacilities::route('/'),
'create' => Pages\\CreateFacility::route('/create'),
'edit' => Pages\\EditFacility::route('/{record}/edit'),
];
}
}
Bagian 7: Penutup
Recap
YANG SUDAH DIBUAT:
✅ Wizard 6 Langkah
├── Step 1: Informasi Dasar (nama, tipe, alamat, deskripsi)
├── Step 2: Detail Ruangan (kamar, luas bangunan/tanah)
├── Step 3: Fasilitas (CheckboxList + Repeater)
├── Step 4: Harga (dengan kalkulasi otomatis)
├── Step 5: Media (multiple photos, PDF, virtual tour)
└── Step 6: Review & Submit (summary + terms checkbox)
✅ Wizard Features
├── Skippable steps
├── URL persistence (refresh tetap di step yang sama)
├── Dynamic placeholder (hitung estimasi biaya)
├── Custom submit button
└── Validation dengan terms checkbox
✅ Table & Dashboard
├── Stacked image column
├── Status badges dengan warna
├── Quick verify action
├── Multiple filters
└── Stats widget (total, available, rented, pending)
✅ Master Data
└── Facility CRUD untuk kelola fasilitas
Tips Pengembangan
NEXT STEPS:
📌 Tenant Management
├── Buat model Tenant
├── Relasi Property -> Tenant
└── Tracking history penyewa
📌 Booking System
├── Reservation flow
├── Approval process
└── Calendar view
📌 Payment
├── Invoice generation
├── Payment tracking
└── Integration Midtrans/Xendit
📌 Notifications
├── Email saat listing verified
├── Reminder pembayaran
└── Notif properti available
📌 Multi-Panel
├── Admin panel (current)
├── Landlord panel (pemilik)
└── Public listing page
Rekomendasi Kelas Gratis BuildWithAngga
🎓 KELAS GRATIS:
1. Laravel 12 Fundamental
Dasar Laravel untuk pemula
→ buildwithangga.com/kelas/laravel-12-fundamental
2. Filament Admin Panel
Deep dive Filament lebih lengkap
→ buildwithangga.com/kelas/filament-admin-panel
3. Laravel Livewire
Reactive components (basis Filament)
→ buildwithangga.com/kelas/laravel-livewire
4. Build Rental App
Project lengkap aplikasi rental
→ buildwithangga.com/kelas/rental-app
5. Laravel Payment Gateway
Integrasi Midtrans & Xendit
→ buildwithangga.com/kelas/laravel-payment-gateway
Penutup
Wizard di Filament memudahkan pembuatan form kompleks yang user-friendly. Dengan memecah form panjang menjadi steps yang logis, user experience jauh lebih baik dibanding satu form panjang yang overwhelming.
Project House Management ini bisa jadi fondasi untuk:
- Aplikasi rental properti
- Sistem manajemen kos-kosan
- Platform listing rumah/apartemen
- Booking villa/homestay
Selamat coding! 🏠
Resources:
- Dokumentasi Filament Wizard: filamentphp.com/docs/forms/layout/wizard
- Heroicons: heroicons.com
- Kelas Gratis: buildwithangga.com