Tutorial Bikin Dashboard Wizard Filament 4 Laravel 12 Web House Management

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:

  • CheckboxList dengan relationship — otomatis sync ke pivot table
  • bulkToggleable — select/deselect all
  • Repeater untuk 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 diurutkan
  • panelLayout('grid') — tampilan grid untuk preview
  • imageEditor() — 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: