Pengenalan Filament 4 dan Bikin Dashboard Warung Online dengan Laravel

Bikin admin panel dari nol itu makan waktu. Serius.

Coba hitung berapa banyak yang harus dikerjakan: authentication system, CRUD untuk setiap tabel, datatable dengan sorting dan filtering, form validation, file upload, role management. Untuk satu project aja bisa habis 1-2 minggu cuma untuk admin panel — belum termasuk fitur utama aplikasi.

Nah, bagaimana kalau semua itu bisa selesai dalam hitungan jam?

Kenalkan: Filament.

Bagian 1: Kenapa Filament 4?

Apa Itu Filament?

Filament adalah admin panel builder untuk Laravel. Bukan cuma template atau starter kit, tapi full-fledged framework untuk membangun admin interface dengan cepat dan elegan.

FILAMENT TECH STACK:

├── Tailwind CSS — Styling yang modern
├── Alpine.js — Interaktivitas ringan
├── Laravel — Backend framework kita
└── Livewire — Reactive components tanpa nulis JavaScript

Ini yang disebut TALL Stack.

Yang bikin Filament special adalah "batteries included" approach. Semua yang kamu butuhkan untuk admin panel sudah tersedia out of the box:

  • Forms — Text input, select, file upload, rich editor, dan puluhan component lain
  • Tables — Sorting, filtering, searching, pagination, bulk actions
  • Notifications — Toast notifications yang cantik
  • Actions — Modal confirmations, slide-overs
  • Widgets — Stats, charts, dan custom widgets
  • Navigation — Sidebar dengan grouping dan badges

Dan yang terbaik? Filament itu open source dan gratis.

Filament vs Bikin Manual

Mungkin kamu mikir: "Kenapa tidak bikin sendiri aja? Kan lebih flexible."

Fair point. Tapi mari kita bandingkan:

MANUAL CODING:
├── ✅ Full control atas semua aspek
├── ✅ Tidak ada dependency tambahan
├── ❌ Butuh waktu lama (minggu, bukan jam)
├── ❌ Harus maintain sendiri semua code
├── ❌ Reinvent the wheel setiap project
└── ❌ Inconsistent UI antar project

FILAMENT:
├── ✅ Rapid development (jam, bukan minggu)
├── ✅ Konsisten dan well-tested
├── ✅ Active community (30k+ GitHub stars)
├── ✅ Regular updates dan security patches
├── ✅ Extensible kalau butuh custom
├── ❌ Learning curve di awal
└── ❌ Opinionated (harus ikuti convention)

Untuk 90% kebutuhan admin panel, Filament lebih dari cukup. Dan kalau butuh yang sangat custom? Filament tetap extensible — kamu bisa override hampir semua aspeknya.

Apa yang Baru di Filament 4?

Filament 4 baru release dan membawa banyak improvement:

FILAMENT 4 HIGHLIGHTS:

🚀 Performance
├── Lebih cepat dari versi sebelumnya
├── Optimized Livewire requests
└── Better caching

🎨 Developer Experience
├── Simplified API
├── Better error messages
├── Improved documentation
└── More intuitive conventions

🎯 New Features
├── Improved theming system
├── Better table features
├── Enhanced form components
├── Cluster navigation
└── Dan banyak lagi...

Kalau kamu sudah pernah pakai Filament 3, versi 4 ini terasa lebih polished. Kalau baru pertama kali, ini waktu yang tepat untuk mulai.

Project yang Akan Kita Buat

Di tutorial ini, kita akan bikin Dashboard Warung Online — admin panel untuk mengelola warung kelontong.

FITUR YANG AKAN DIBUAT:

📁 Master Data
├── Manage Kategori (CRUD)
│   └── Makanan, Minuman, Snack, dll
└── Manage Produk (CRUD)
    └── Dengan relasi ke kategori
    └── Upload gambar produk
    └── Tracking stok

🛒 Transaksi
└── Manage Transaksi (CRUD)
    └── Data customer
    └── Status tracking (pending, processing, completed)
    └── Total amount

📊 Dashboard
└── Widget Statistik
    └── Total kategori
    └── Total produk aktif
    └── Transaksi hari ini
    └── Pendapatan hari ini

Kenapa warung? Karena relatable untuk konteks Indonesia, dan strukturnya cukup representatif untuk belajar konsep-konsep penting: relasi antar tabel, CRUD dengan filter, status management, dan basic reporting.

Tech Stack:

  • Laravel 11
  • Filament 4
  • SQLite (simple, no setup tambahan)
  • Tailwind CSS (via Filament)

Siap? Mari kita mulai dari setup project.


Bagian 2: Setup Project dan Install Filament

Requirements

Sebelum mulai, pastikan kamu sudah punya:

REQUIREMENTS:

├── PHP 8.2 atau lebih baru
├── Composer (package manager PHP)
├── Node.js & NPM (untuk compile assets)
└── Code editor (VS Code recommended)

Cek versi yang terinstall:

# Cek PHP version (minimal 8.2)
php -v

# Cek Composer
composer -V

# Cek Node.js (minimal 18)
node -v

Kalau belum install, bisa download dari website resmi masing-masing. Untuk PHP, recommended pakai Laravel Herd (Mac) atau Laragon (Windows) supaya setup lebih gampang.

Step 1: Install Laravel 11

Buka terminal dan jalankan:

# Buat project Laravel baru
composer create-project laravel/laravel warung-online

# Masuk ke folder project
cd warung-online

Test apakah Laravel sudah terinstall dengan benar:

# Jalankan development server
php artisan serve

Buka browser dan akses http://localhost:8000. Kalau muncul halaman welcome Laravel, berarti instalasi berhasil.

Tekan Ctrl+C di terminal untuk stop server.

Step 2: Setup Database

Untuk tutorial ini, kita pakai SQLite supaya simple — tidak perlu install MySQL atau PostgreSQL.

Buka file .env di root project dan ubah konfigurasi database:

# Sebelum (default MySQL)
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=

# Sesudah (SQLite)
DB_CONNECTION=sqlite
# Hapus atau comment baris DB_HOST sampai DB_PASSWORD

Buat file database SQLite:

# Di Mac/Linux
touch database/database.sqlite

# Di Windows (PowerShell)
New-Item database/database.sqlite -ItemType File

Note: Kalau kamu prefer MySQL, tidak masalah. Sesuaikan saja .env dengan credentials database kamu.

Step 3: Install Filament 4

Sekarang bagian yang exciting — install Filament:

# Install Filament via Composer
composer require filament/filament:"^4.0"

Tunggu sampai selesai download. Setelah itu, install panel admin:

# Install admin panel
php artisan filament:install --panels

Saat ditanya panel ID, ketik admin dan enter:

 ┌ What is the ID? ─────────────────────────────────────────┐
 │ admin                                                    │
 └──────────────────────────────────────────────────────────┘

Command ini akan generate AdminPanelProvider.php di folder app/Providers/Filament/.

Step 4: Jalankan Migration

Filament butuh beberapa tabel untuk user management. Jalankan migration:

php artisan migrate

Output yang diharapkan:

INFO  Running migrations.

2014_10_12_000000_create_users_table .............. 5ms DONE
2014_10_12_100000_create_password_reset_tokens_table  3ms DONE
2019_08_19_000000_create_failed_jobs_table ........ 2ms DONE
2019_12_14_000001_create_personal_access_tokens_table  4ms DONE

Step 5: Buat User Admin

Filament butuh user untuk login ke admin panel. Buat user pertama:

php artisan make:filament-user

Isi data yang diminta:

 ┌ Name ────────────────────────────────────────────────────┐
 │ Admin                                                    │
 └──────────────────────────────────────────────────────────┘

 ┌ Email address ───────────────────────────────────────────┐
 │ [email protected]                                         │
 └──────────────────────────────────────────────────────────┘

 ┌ Password ────────────────────────────────────────────────┐
 │ ••••••••                                                 │
 └──────────────────────────────────────────────────────────┘

INFO  Success! [email protected] may now log in at <http://localhost:8000/admin/login>

Step 6: Akses Admin Panel

Jalankan server dan buka admin panel:

php artisan serve

Buka browser dan akses: http://localhost:8000/admin

Login dengan credentials yang tadi dibuat:

🎉 Selamat! Kamu sudah punya admin panel Filament yang working.

Struktur Folder Filament

Sebelum lanjut, kenalan dulu dengan struktur folder yang di-generate Filament:

app/
├── Filament/
│   ├── Resources/          <- CRUD resources akan di sini
│   │   └── (kosong)
│   └── Widgets/            <- Dashboard widgets
│       └── (kosong)
│
├── Models/
│   └── User.php            <- Model user (sudah ada)
│
└── Providers/
    └── Filament/
        └── AdminPanelProvider.php  <- Konfigurasi panel

Folder Resources adalah tempat kita akan bikin CRUD untuk setiap model. Folder Widgets untuk komponen dashboard seperti statistik dan chart.

Checkpoint ✅

Di tahap ini, pastikan:

☑️ Laravel 11 terinstall
☑️ Filament 4 terinstall
☑️ Database SQLite sudah dibuat
☑️ Migration berhasil dijalankan
☑️ User admin sudah dibuat
☑️ Bisa login ke <http://localhost:8000/admin>
☑️ Melihat dashboard kosong Filament

Kalau semua sudah oke, lanjut ke bagian berikutnya — bikin struktur database untuk Warung Online.


Bagian 3: Database Design - Migration

Sekarang kita akan design dan bikin struktur database untuk Warung Online. Ini fondasi penting sebelum bikin CRUD di Filament.

ERD (Entity Relationship Diagram)

Ini struktur database yang akan kita buat:

DATABASE DESIGN WARUNG ONLINE:

┌─────────────────┐       ┌─────────────────┐
│   categories    │       │    products     │
├─────────────────┤       ├─────────────────┤
│ id              │───┐   │ id              │
│ name            │   │   │ category_id     │◄──┘
│ slug            │   └──►│ name            │
│ description     │       │ slug            │
│ is_active       │       │ description     │
│ created_at      │       │ price           │
│ updated_at      │       │ stock           │
└─────────────────┘       │ image           │
                          │ is_active       │
                          │ created_at      │
                          │ updated_at      │
                          └─────────────────┘
                                   │
                                   │ (referenced by)
                                   ▼
┌─────────────────┐       ┌───────────────────┐
│  transactions   │       │ transaction_items │
├─────────────────┤       ├───────────────────┤
│ id              │───┐   │ id                │
│ code            │   │   │ transaction_id    │◄──┘
│ customer_name   │   └──►│ product_id        │───► products
│ customer_phone  │       │ quantity          │
│ total_amount    │       │ price             │
│ status          │       │ subtotal          │
│ notes           │       │ created_at        │
│ created_at      │       │ updated_at        │
│ updated_at      │       └───────────────────┘
└─────────────────┘

RELASI:
├── Category hasMany Products
├── Product belongsTo Category
├── Transaction hasMany TransactionItems
├── TransactionItem belongsTo Transaction
└── TransactionItem belongsTo Product

Ada 4 tabel utama:

  1. categories — Kategori produk (Makanan, Minuman, dll)
  2. products — Daftar produk dengan harga dan stok
  3. transactions — Header transaksi (customer info, total, status)
  4. transaction_items — Detail item per transaksi

Migration 1: Categories

Buat migration untuk tabel categories:

php artisan make:migration create_categories_table

Buka file yang di-generate di database/migrations/ dan edit:

<?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('categories', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('slug')->unique();
            $table->text('description')->nullable();
            $table->boolean('is_active')->default(true);
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('categories');
    }
};

Penjelasan field:

  • name — Nama kategori (wajib)
  • slug — URL-friendly version dari nama, harus unique
  • description — Deskripsi opsional
  • is_active — Untuk soft-toggle kategori tanpa hapus

Migration 2: Products

php artisan make:migration create_products_table

Edit file migration:

<?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('products', function (Blueprint $table) {
            $table->id();
            $table->foreignId('category_id')->constrained()->cascadeOnDelete();
            $table->string('name');
            $table->string('slug')->unique();
            $table->text('description')->nullable();
            $table->decimal('price', 12, 2);
            $table->integer('stock')->default(0);
            $table->string('image')->nullable();
            $table->boolean('is_active')->default(true);
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('products');
    }
};

Penjelasan field:

  • category_id — Foreign key ke categories, cascade delete (kalau kategori dihapus, produk ikut terhapus)
  • price — Decimal dengan presisi 12 digit, 2 decimal places (max 9,999,999,999.99)
  • stock — Jumlah stok, default 0
  • image — Path ke file gambar, opsional

Migration 3: Transactions

php artisan make:migration create_transactions_table

Edit file migration:

<?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('transactions', function (Blueprint $table) {
            $table->id();
            $table->string('code')->unique();
            $table->string('customer_name');
            $table->string('customer_phone')->nullable();
            $table->decimal('total_amount', 12, 2)->default(0);
            $table->enum('status', ['pending', 'processing', 'completed', 'cancelled'])
                  ->default('pending');
            $table->text('notes')->nullable();
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('transactions');
    }
};

Penjelasan field:

  • code — Kode transaksi unique (contoh: TRX-20250112-0001)
  • customer_name — Nama pembeli
  • customer_phone — Nomor HP opsional
  • total_amount — Total nilai transaksi
  • status — Status dengan 4 opsi: pending, processing, completed, cancelled

Migration 4: Transaction Items

php artisan make:migration create_transaction_items_table

Edit file migration:

<?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('transaction_items', function (Blueprint $table) {
            $table->id();
            $table->foreignId('transaction_id')->constrained()->cascadeOnDelete();
            $table->foreignId('product_id')->constrained()->cascadeOnDelete();
            $table->integer('quantity');
            $table->decimal('price', 12, 2);
            $table->decimal('subtotal', 12, 2);
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('transaction_items');
    }
};

Penjelasan field:

  • transaction_id — Relasi ke transaksi parent
  • product_id — Relasi ke produk yang dibeli
  • quantity — Jumlah item
  • price — Harga saat transaksi (snapshot, bisa beda dari harga current)
  • subtotal — quantity × price

Jalankan Semua Migration

php artisan migrate

Output yang diharapkan:

INFO  Running migrations.

2025_01_12_100001_create_categories_table ......... 3ms DONE
2025_01_12_100002_create_products_table ........... 4ms DONE
2025_01_12_100003_create_transactions_table ....... 3ms DONE
2025_01_12_100004_create_transaction_items_table .. 3ms DONE

Setup Models

Sekarang bikin Model untuk setiap tabel. Laravel sudah punya User model, kita perlu bikin 4 model lagi.

Model Category:

php artisan make:model Category

Edit app/Models/Category.php:

<?php

namespace App\\Models;

use Illuminate\\Database\\Eloquent\\Model;
use Illuminate\\Database\\Eloquent\\Relations\\HasMany;

class Category extends Model
{
    protected $fillable = [
        'name',
        'slug',
        'description',
        'is_active',
    ];

    protected $casts = [
        'is_active' => 'boolean',
    ];

    public function products(): HasMany
    {
        return $this->hasMany(Product::class);
    }
}

Model Product:

php artisan make:model Product

Edit app/Models/Product.php:

<?php

namespace App\\Models;

use Illuminate\\Database\\Eloquent\\Model;
use Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;
use Illuminate\\Database\\Eloquent\\Relations\\HasMany;

class Product extends Model
{
    protected $fillable = [
        'category_id',
        'name',
        'slug',
        'description',
        'price',
        'stock',
        'image',
        'is_active',
    ];

    protected $casts = [
        'price' => 'decimal:2',
        'is_active' => 'boolean',
    ];

    public function category(): BelongsTo
    {
        return $this->belongsTo(Category::class);
    }

    public function transactionItems(): HasMany
    {
        return $this->hasMany(TransactionItem::class);
    }
}

Model Transaction:

php artisan make:model Transaction

Edit app/Models/Transaction.php:

<?php

namespace App\\Models;

use Illuminate\\Database\\Eloquent\\Model;
use Illuminate\\Database\\Eloquent\\Relations\\HasMany;

class Transaction extends Model
{
    protected $fillable = [
        'code',
        'customer_name',
        'customer_phone',
        'total_amount',
        'status',
        'notes',
    ];

    protected $casts = [
        'total_amount' => 'decimal:2',
    ];

    public function items(): HasMany
    {
        return $this->hasMany(TransactionItem::class);
    }
}

Model TransactionItem:

php artisan make:model TransactionItem

Edit app/Models/TransactionItem.php:

<?php

namespace App\\Models;

use Illuminate\\Database\\Eloquent\\Model;
use Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;

class TransactionItem extends Model
{
    protected $fillable = [
        'transaction_id',
        'product_id',
        'quantity',
        'price',
        'subtotal',
    ];

    protected $casts = [
        'price' => 'decimal:2',
        'subtotal' => 'decimal:2',
    ];

    public function transaction(): BelongsTo
    {
        return $this->belongsTo(Transaction::class);
    }

    public function product(): BelongsTo
    {
        return $this->belongsTo(Product::class);
    }
}

Verifikasi Setup

Cek apakah semua sudah benar dengan Tinker:

php artisan tinker

Di dalam Tinker, coba:

// Cek tabel sudah ada
>>> Schema::hasTable('categories')
=> true

>>> Schema::hasTable('products')
=> true

>>> Schema::hasTable('transactions')
=> true

>>> Schema::hasTable('transaction_items')
=> true

// Keluar dari Tinker
>>> exit

Checkpoint ✅

Di tahap ini, pastikan:

☑️ 4 migration files sudah dibuat
☑️ Migration berhasil dijalankan
☑️ 4 model sudah dibuat (Category, Product, Transaction, TransactionItem)
☑️ Relationships sudah didefinisikan di setiap model
☑️ Fillable dan casts sudah di-setup

Database sudah siap! Selanjutnya kita akan isi dengan data dummy menggunakan Seeder supaya ada data untuk testing di dashboard.

Bagian 4: Seeder - Data Dummy

Database sudah siap, tapi masih kosong. Sekarang kita isi dengan data dummy supaya ada yang bisa ditampilkan di dashboard nanti.

Seeder adalah cara Laravel untuk mengisi database dengan data awal. Sangat berguna untuk testing dan development.

CategorySeeder

Buat seeder untuk kategori:

php artisan make:seeder CategorySeeder

Edit file database/seeders/CategorySeeder.php:

<?php

namespace Database\\Seeders;

use App\\Models\\Category;
use Illuminate\\Database\\Seeder;
use Illuminate\\Support\\Str;

class CategorySeeder extends Seeder
{
    public function run(): void
    {
        $categories = [
            [
                'name' => 'Makanan',
                'description' => 'Berbagai macam makanan siap saji dan bahan makanan',
            ],
            [
                'name' => 'Minuman',
                'description' => 'Minuman dingin, panas, dan kemasan',
            ],
            [
                'name' => 'Snack',
                'description' => 'Cemilan dan makanan ringan',
            ],
            [
                'name' => 'Kebutuhan Rumah',
                'description' => 'Sabun, deterjen, dan kebutuhan rumah tangga',
            ],
            [
                'name' => 'Rokok',
                'description' => 'Berbagai merk rokok',
            ],
            [
                'name' => 'Pulsa & Token',
                'description' => 'Pulsa elektrik dan token listrik',
            ],
        ];

        foreach ($categories as $category) {
            Category::create([
                'name' => $category['name'],
                'slug' => Str::slug($category['name']),
                'description' => $category['description'],
                'is_active' => true,
            ]);
        }
    }
}

Kita bikin 6 kategori yang umum ada di warung Indonesia. Slug di-generate otomatis dari nama menggunakan Str::slug().

ProductSeeder

Sekarang produk-produknya:

php artisan make:seeder ProductSeeder

Edit file database/seeders/ProductSeeder.php:

<?php

namespace Database\\Seeders;

use App\\Models\\Product;
use App\\Models\\Category;
use Illuminate\\Database\\Seeder;
use Illuminate\\Support\\Str;

class ProductSeeder extends Seeder
{
    public function run(): void
    {
        $products = [
            // Makanan
            ['category' => 'Makanan', 'name' => 'Indomie Goreng', 'price' => 3500, 'stock' => 100],
            ['category' => 'Makanan', 'name' => 'Indomie Kuah Soto', 'price' => 3500, 'stock' => 80],
            ['category' => 'Makanan', 'name' => 'Mie Sedaap Goreng', 'price' => 3500, 'stock' => 60],
            ['category' => 'Makanan', 'name' => 'Nasi Bungkus', 'price' => 10000, 'stock' => 20],
            ['category' => 'Makanan', 'name' => 'Telur Ayam (per butir)', 'price' => 2500, 'stock' => 200],

            // Minuman
            ['category' => 'Minuman', 'name' => 'Aqua 600ml', 'price' => 4000, 'stock' => 150],
            ['category' => 'Minuman', 'name' => 'Teh Botol Sosro', 'price' => 5000, 'stock' => 80],
            ['category' => 'Minuman', 'name' => 'Es Teh Pucuk Harum', 'price' => 4000, 'stock' => 90],
            ['category' => 'Minuman', 'name' => 'Kopi Kapal Api Sachet', 'price' => 2000, 'stock' => 100],
            ['category' => 'Minuman', 'name' => 'Susu Ultra 250ml', 'price' => 6000, 'stock' => 40],
            ['category' => 'Minuman', 'name' => 'Pocari Sweat 350ml', 'price' => 8000, 'stock' => 35],

            // Snack
            ['category' => 'Snack', 'name' => 'Chitato 68gr', 'price' => 12000, 'stock' => 30],
            ['category' => 'Snack', 'name' => 'Qtela Singkong', 'price' => 10000, 'stock' => 35],
            ['category' => 'Snack', 'name' => 'Oreo 137gr', 'price' => 10000, 'stock' => 40],
            ['category' => 'Snack', 'name' => 'Taro Net', 'price' => 3000, 'stock' => 60],
            ['category' => 'Snack', 'name' => 'Chiki Balls', 'price' => 2500, 'stock' => 70],
            ['category' => 'Snack', 'name' => 'Beng Beng', 'price' => 3000, 'stock' => 50],

            // Kebutuhan Rumah
            ['category' => 'Kebutuhan Rumah', 'name' => 'Rinso Sachet', 'price' => 1500, 'stock' => 100],
            ['category' => 'Kebutuhan Rumah', 'name' => 'Sabun Lifebuoy', 'price' => 4000, 'stock' => 50],
            ['category' => 'Kebutuhan Rumah', 'name' => 'Sunlight Sachet', 'price' => 1000, 'stock' => 120],
            ['category' => 'Kebutuhan Rumah', 'name' => 'Pasta Gigi Pepsodent', 'price' => 8000, 'stock' => 30],
            ['category' => 'Kebutuhan Rumah', 'name' => 'Baygon Semprot', 'price' => 35000, 'stock' => 15],

            // Rokok
            ['category' => 'Rokok', 'name' => 'Gudang Garam Filter 12', 'price' => 28000, 'stock' => 50],
            ['category' => 'Rokok', 'name' => 'Djarum Super 12', 'price' => 25000, 'stock' => 40],
            ['category' => 'Rokok', 'name' => 'Sampoerna Mild 16', 'price' => 32000, 'stock' => 35],
            ['category' => 'Rokok', 'name' => 'Surya Pro Mild', 'price' => 22000, 'stock' => 45],

            // Pulsa & Token
            ['category' => 'Pulsa & Token', 'name' => 'Pulsa 10.000', 'price' => 12000, 'stock' => 999],
            ['category' => 'Pulsa & Token', 'name' => 'Pulsa 25.000', 'price' => 27000, 'stock' => 999],
            ['category' => 'Pulsa & Token', 'name' => 'Pulsa 50.000', 'price' => 52000, 'stock' => 999],
            ['category' => 'Pulsa & Token', 'name' => 'Token Listrik 50.000', 'price' => 52000, 'stock' => 999],
            ['category' => 'Pulsa & Token', 'name' => 'Token Listrik 100.000', 'price' => 102000, 'stock' => 999],
        ];

        foreach ($products as $product) {
            $category = Category::where('name', $product['category'])->first();

            if ($category) {
                Product::create([
                    'category_id' => $category->id,
                    'name' => $product['name'],
                    'slug' => Str::slug($product['name']),
                    'price' => $product['price'],
                    'stock' => $product['stock'],
                    'is_active' => true,
                ]);
            }
        }
    }
}

Total ada 31 produk dengan harga dan stok yang realistis untuk warung Indonesia. Setiap produk terhubung ke kategorinya masing-masing.

TransactionSeeder

Sekarang data transaksi:

php artisan make:seeder TransactionSeeder

Edit file database/seeders/TransactionSeeder.php:

<?php

namespace Database\\Seeders;

use App\\Models\\Transaction;
use App\\Models\\TransactionItem;
use App\\Models\\Product;
use Illuminate\\Database\\Seeder;
use Carbon\\Carbon;

class TransactionSeeder extends Seeder
{
    public function run(): void
    {
        $customers = [
            ['name' => 'Budi Santoso', 'phone' => '081234567890'],
            ['name' => 'Siti Rahayu', 'phone' => '082345678901'],
            ['name' => 'Ahmad Hidayat', 'phone' => '083456789012'],
            ['name' => 'Dewi Lestari', 'phone' => '084567890123'],
            ['name' => 'Eko Prasetyo', 'phone' => '085678901234'],
            ['name' => 'Rina Wati', 'phone' => '086789012345'],
            ['name' => 'Joko Widodo', 'phone' => '087890123456'],
            ['name' => 'Sri Mulyani', 'phone' => '088901234567'],
        ];

        $statuses = ['pending', 'processing', 'completed', 'completed', 'completed'];
        $products = Product::all();

        // Buat 20 transaksi
        for ($i = 0; $i < 20; $i++) {
            $customer = $customers[array_rand($customers)];
            $status = $statuses[array_rand($statuses)];

            // Random date dalam 7 hari terakhir
            $createdAt = Carbon::now()->subDays(rand(0, 7))->subHours(rand(0, 23));

            $transaction = Transaction::create([
                'code' => 'TRX-' . $createdAt->format('Ymd') . '-' . str_pad($i + 1, 4, '0', STR_PAD_LEFT),
                'customer_name' => $customer['name'],
                'customer_phone' => $customer['phone'],
                'status' => $status,
                'total_amount' => 0,
                'created_at' => $createdAt,
                'updated_at' => $createdAt,
            ]);

            // Random 1-5 items per transaksi
            $itemCount = rand(1, 5);
            $selectedProducts = $products->random($itemCount);
            $totalAmount = 0;

            foreach ($selectedProducts as $product) {
                $quantity = rand(1, 3);
                $subtotal = $product->price * $quantity;
                $totalAmount += $subtotal;

                TransactionItem::create([
                    'transaction_id' => $transaction->id,
                    'product_id' => $product->id,
                    'quantity' => $quantity,
                    'price' => $product->price,
                    'subtotal' => $subtotal,
                    'created_at' => $createdAt,
                    'updated_at' => $createdAt,
                ]);
            }

            // Update total amount
            $transaction->update(['total_amount' => $totalAmount]);
        }
    }
}

Seeder ini bikin 20 transaksi dengan:

  • Random customer dari list 8 nama
  • Random status (lebih banyak completed)
  • Random tanggal dalam 7 hari terakhir
  • 1-5 item per transaksi dengan quantity random

DatabaseSeeder

Terakhir, gabungkan semua seeder di DatabaseSeeder:

<?php

namespace Database\\Seeders;

use Illuminate\\Database\\Seeder;

class DatabaseSeeder extends Seeder
{
    public function run(): void
    {
        $this->call([
            CategorySeeder::class,
            ProductSeeder::class,
            TransactionSeeder::class,
        ]);
    }
}

Urutan penting! Category harus duluan karena Product butuh category_id.

Jalankan Seeder

Sekarang reset database dan jalankan seeder:

# Fresh migration + seeding
php artisan migrate:fresh --seed

Output yang diharapkan:

Dropping all tables ...................... 15ms DONE

INFO  Running migrations.

2014_10_12_000000_create_users_table .............. 4ms DONE
2014_10_12_100000_create_password_reset_tokens_table  2ms DONE
2019_08_19_000000_create_failed_jobs_table ........ 2ms DONE
2019_12_14_000001_create_personal_access_tokens_table  3ms DONE
2025_01_12_100001_create_categories_table ......... 2ms DONE
2025_01_12_100002_create_products_table ........... 3ms DONE
2025_01_12_100003_create_transactions_table ....... 2ms DONE
2025_01_12_100004_create_transaction_items_table .. 2ms DONE

INFO  Seeding database.

Database\\Seeders\\CategorySeeder ................... RUNNING
Database\\Seeders\\CategorySeeder .............. 12ms DONE
Database\\Seeders\\ProductSeeder .................... RUNNING
Database\\Seeders\\ProductSeeder ............... 45ms DONE
Database\\Seeders\\TransactionSeeder ................ RUNNING
Database\\Seeders\\TransactionSeeder ........... 89ms DONE

Note: Karena kita pakai migrate:fresh, user admin yang tadi dibuat akan terhapus. Buat lagi dengan php artisan make:filament-user.

Verifikasi Data

Cek apakah data sudah masuk:

php artisan tinker

>>> use App\\Models\\Category;
>>> use App\\Models\\Product;
>>> use App\\Models\\Transaction;

>>> Category::count()
=> 6

>>> Product::count()
=> 31

>>> Transaction::count()
=> 20

>>> Transaction::where('status', 'completed')->sum('total_amount')
=> "523500.00"  // Nilai akan berbeda karena random

>>> exit

Checkpoint ✅

☑️ CategorySeeder dibuat (6 kategori)
☑️ ProductSeeder dibuat (31 produk)
☑️ TransactionSeeder dibuat (20 transaksi)
☑️ DatabaseSeeder sudah memanggil ketiga seeder
☑️ migrate:fresh --seed berhasil
☑️ Data terverifikasi via Tinker
☑️ User admin dibuat ulang

Data sudah siap! Sekarang saatnya bikin CRUD dengan Filament.


Bagian 5: Filament Resources - CRUD Dashboard

Ini bagian paling seru — bikin CRUD dengan Filament. Kamu akan lihat betapa cepatnya bikin admin panel yang fully functional.

Generate Resource: Category

Filament punya command untuk generate resource lengkap dengan form dan table:

php artisan make:filament-resource Category --generate

Flag --generate akan auto-generate form fields dan table columns berdasarkan struktur database.

Sekarang edit file yang di-generate di app/Filament/Resources/CategoryResource.php:

<?php

namespace App\\Filament\\Resources;

use App\\Filament\\Resources\\CategoryResource\\Pages;
use App\\Models\\Category;
use Filament\\Forms;
use Filament\\Forms\\Form;
use Filament\\Resources\\Resource;
use Filament\\Tables;
use Filament\\Tables\\Table;
use Illuminate\\Support\\Str;

class CategoryResource extends Resource
{
    protected static ?string $model = Category::class;

    protected static ?string $navigationIcon = 'heroicon-o-tag';

    protected static ?string $navigationGroup = 'Master Data';

    protected static ?int $navigationSort = 1;

    public static function form(Form $form): Form
    {
        return $form
            ->schema([
                Forms\\Components\\Section::make('Informasi Kategori')
                    ->description('Data kategori produk')
                    ->schema([
                        Forms\\Components\\TextInput::make('name')
                            ->label('Nama Kategori')
                            ->required()
                            ->maxLength(255)
                            ->live(onBlur: true)
                            ->afterStateUpdated(function (string $operation, $state, Forms\\Set $set) {
                                if ($operation !== 'create') {
                                    return;
                                }
                                $set('slug', Str::slug($state));
                            }),

                        Forms\\Components\\TextInput::make('slug')
                            ->label('Slug')
                            ->required()
                            ->maxLength(255)
                            ->unique(ignoreRecord: true)
                            ->dehydrated()
                            ->helperText('URL-friendly version dari nama'),

                        Forms\\Components\\Textarea::make('description')
                            ->label('Deskripsi')
                            ->rows(3)
                            ->columnSpanFull(),

                        Forms\\Components\\Toggle::make('is_active')
                            ->label('Aktif')
                            ->default(true)
                            ->helperText('Kategori non-aktif tidak akan ditampilkan'),
                    ])->columns(2),
            ]);
    }

    public static function table(Table $table): Table
    {
        return $table
            ->columns([
                Tables\\Columns\\TextColumn::make('name')
                    ->label('Nama')
                    ->searchable()
                    ->sortable(),

                Tables\\Columns\\TextColumn::make('slug')
                    ->label('Slug')
                    ->searchable()
                    ->toggleable(isToggledHiddenByDefault: true),

                Tables\\Columns\\TextColumn::make('products_count')
                    ->label('Jumlah Produk')
                    ->counts('products')
                    ->sortable()
                    ->badge()
                    ->color('primary'),

                Tables\\Columns\\IconColumn::make('is_active')
                    ->label('Aktif')
                    ->boolean(),

                Tables\\Columns\\TextColumn::make('created_at')
                    ->label('Dibuat')
                    ->dateTime('d M Y')
                    ->sortable()
                    ->toggleable(isToggledHiddenByDefault: true),
            ])
            ->filters([
                Tables\\Filters\\TernaryFilter::make('is_active')
                    ->label('Status Aktif')
                    ->placeholder('Semua')
                    ->trueLabel('Aktif')
                    ->falseLabel('Non-aktif'),
            ])
            ->actions([
                Tables\\Actions\\EditAction::make(),
                Tables\\Actions\\DeleteAction::make(),
            ])
            ->bulkActions([
                Tables\\Actions\\BulkActionGroup::make([
                    Tables\\Actions\\DeleteBulkAction::make(),
                ]),
            ]);
    }

    public static function getRelations(): array
    {
        return [
            //
        ];
    }

    public static function getPages(): array
    {
        return [
            'index' => Pages\\ListCategories::route('/'),
            'create' => Pages\\CreateCategory::route('/create'),
            'edit' => Pages\\EditCategory::route('/{record}/edit'),
        ];
    }
}

Yang menarik di sini:

  1. Auto Slug Generation — Ketika user ketik nama, slug otomatis ter-generate
  2. Navigation Group — Kategori dikelompokkan di bawah "Master Data"
  3. Products Count — Menampilkan jumlah produk per kategori dengan badge
  4. Toggleable Columns — Beberapa kolom bisa di-hide/show oleh user
  5. Ternary Filter — Filter untuk status aktif/non-aktif

Buka browser dan akses http://localhost:8000/admin/categories. Kamu akan lihat datatable dengan 6 kategori!

Generate Resource: Product

php artisan make:filament-resource Product --generate

Edit app/Filament/Resources/ProductResource.php:

<?php

namespace App\\Filament\\Resources;

use App\\Filament\\Resources\\ProductResource\\Pages;
use App\\Models\\Product;
use Filament\\Forms;
use Filament\\Forms\\Form;
use Filament\\Resources\\Resource;
use Filament\\Tables;
use Filament\\Tables\\Table;
use Illuminate\\Support\\Str;

class ProductResource extends Resource
{
    protected static ?string $model = Product::class;

    protected static ?string $navigationIcon = 'heroicon-o-cube';

    protected static ?string $navigationGroup = 'Master Data';

    protected static ?int $navigationSort = 2;

    public static function form(Form $form): Form
    {
        return $form
            ->schema([
                Forms\\Components\\Section::make('Informasi Produk')
                    ->schema([
                        Forms\\Components\\Select::make('category_id')
                            ->label('Kategori')
                            ->relationship('category', 'name')
                            ->required()
                            ->searchable()
                            ->preload()
                            ->createOptionForm([
                                Forms\\Components\\TextInput::make('name')
                                    ->required(),
                                Forms\\Components\\TextInput::make('slug')
                                    ->required(),
                            ]),

                        Forms\\Components\\TextInput::make('name')
                            ->label('Nama Produk')
                            ->required()
                            ->maxLength(255)
                            ->live(onBlur: true)
                            ->afterStateUpdated(function (string $operation, $state, Forms\\Set $set) {
                                if ($operation !== 'create') {
                                    return;
                                }
                                $set('slug', Str::slug($state));
                            }),

                        Forms\\Components\\TextInput::make('slug')
                            ->label('Slug')
                            ->required()
                            ->maxLength(255)
                            ->unique(ignoreRecord: true),

                        Forms\\Components\\Textarea::make('description')
                            ->label('Deskripsi')
                            ->rows(3)
                            ->columnSpanFull(),
                    ])->columns(2),

                Forms\\Components\\Section::make('Harga & Stok')
                    ->schema([
                        Forms\\Components\\TextInput::make('price')
                            ->label('Harga')
                            ->required()
                            ->numeric()
                            ->prefix('Rp')
                            ->maxValue(9999999999),

                        Forms\\Components\\TextInput::make('stock')
                            ->label('Stok')
                            ->required()
                            ->numeric()
                            ->default(0)
                            ->minValue(0),

                        Forms\\Components\\Toggle::make('is_active')
                            ->label('Aktif')
                            ->default(true),
                    ])->columns(3),

                Forms\\Components\\Section::make('Gambar Produk')
                    ->schema([
                        Forms\\Components\\FileUpload::make('image')
                            ->label('Foto Produk')
                            ->image()
                            ->directory('products')
                            ->maxSize(2048)
                            ->imageEditor()
                            ->columnSpanFull(),
                    ])->collapsible(),
            ]);
    }

    public static function table(Table $table): Table
    {
        return $table
            ->columns([
                Tables\\Columns\\ImageColumn::make('image')
                    ->label('Foto')
                    ->circular()
                    ->defaultImageUrl(fn () => '<https://ui-avatars.com/api/?name=No+Image&background=e2e8f0>'),

                Tables\\Columns\\TextColumn::make('name')
                    ->label('Nama')
                    ->searchable()
                    ->sortable()
                    ->description(fn (Product $record): string => $record->category->name),

                Tables\\Columns\\TextColumn::make('price')
                    ->label('Harga')
                    ->money('IDR')
                    ->sortable(),

                Tables\\Columns\\TextColumn::make('stock')
                    ->label('Stok')
                    ->sortable()
                    ->badge()
                    ->color(fn (int $state): string => match(true) {
                        $state <= 10 => 'danger',
                        $state <= 30 => 'warning',
                        default => 'success',
                    }),

                Tables\\Columns\\IconColumn::make('is_active')
                    ->label('Aktif')
                    ->boolean(),

                Tables\\Columns\\TextColumn::make('created_at')
                    ->label('Dibuat')
                    ->dateTime('d M Y')
                    ->sortable()
                    ->toggleable(isToggledHiddenByDefault: true),
            ])
            ->filters([
                Tables\\Filters\\SelectFilter::make('category')
                    ->relationship('category', 'name')
                    ->label('Kategori')
                    ->preload(),

                Tables\\Filters\\TernaryFilter::make('is_active')
                    ->label('Status Aktif'),

                Tables\\Filters\\Filter::make('low_stock')
                    ->label('Stok Rendah')
                    ->query(fn ($query) => $query->where('stock', '<=', 10)),
            ])
            ->actions([
                Tables\\Actions\\EditAction::make(),
                Tables\\Actions\\DeleteAction::make(),
            ])
            ->bulkActions([
                Tables\\Actions\\BulkActionGroup::make([
                    Tables\\Actions\\DeleteBulkAction::make(),
                ]),
            ]);
    }

    public static function getRelations(): array
    {
        return [
            //
        ];
    }

    public static function getPages(): array
    {
        return [
            'index' => Pages\\ListProducts::route('/'),
            'create' => Pages\\CreateProduct::route('/create'),
            'edit' => Pages\\EditProduct::route('/{record}/edit'),
        ];
    }
}

Fitur menarik:

  1. Relationship Select — Dropdown kategori langsung dari relasi dengan opsi create inline
  2. Money Formatting — Harga otomatis diformat ke Rupiah
  3. Conditional Badge Color — Stok rendah berwarna merah, sedang kuning, aman hijau
  4. Image Upload — Dengan image editor built-in
  5. Custom Filter — Filter untuk produk dengan stok rendah
  6. Description under name — Menampilkan kategori di bawah nama produk

Generate Resource: Transaction

php artisan make:filament-resource Transaction --generate

Edit app/Filament/Resources/TransactionResource.php:

<?php

namespace App\\Filament\\Resources;

use App\\Filament\\Resources\\TransactionResource\\Pages;
use App\\Models\\Transaction;
use Filament\\Forms;
use Filament\\Forms\\Form;
use Filament\\Resources\\Resource;
use Filament\\Tables;
use Filament\\Tables\\Table;
use Filament\\Infolists;
use Filament\\Infolists\\Infolist;

class TransactionResource extends Resource
{
    protected static ?string $model = Transaction::class;

    protected static ?string $navigationIcon = 'heroicon-o-shopping-cart';

    protected static ?string $navigationGroup = 'Transaksi';

    protected static ?string $navigationLabel = 'Daftar Transaksi';

    public static function form(Form $form): Form
    {
        return $form
            ->schema([
                Forms\\Components\\Section::make('Info Transaksi')
                    ->schema([
                        Forms\\Components\\TextInput::make('code')
                            ->label('Kode Transaksi')
                            ->default(function () {
                                return 'TRX-' . date('Ymd') . '-' . str_pad(
                                    Transaction::whereDate('created_at', today())->count() + 1,
                                    4, '0', STR_PAD_LEFT
                                );
                            })
                            ->disabled()
                            ->dehydrated()
                            ->required(),

                        Forms\\Components\\Select::make('status')
                            ->label('Status')
                            ->options([
                                'pending' => '⏳ Pending',
                                'processing' => '🔄 Diproses',
                                'completed' => '✅ Selesai',
                                'cancelled' => '❌ Dibatalkan',
                            ])
                            ->default('pending')
                            ->required()
                            ->native(false),
                    ])->columns(2),

                Forms\\Components\\Section::make('Info Customer')
                    ->schema([
                        Forms\\Components\\TextInput::make('customer_name')
                            ->label('Nama Customer')
                            ->required()
                            ->maxLength(255),

                        Forms\\Components\\TextInput::make('customer_phone')
                            ->label('No. HP')
                            ->tel()
                            ->maxLength(20),

                        Forms\\Components\\Textarea::make('notes')
                            ->label('Catatan')
                            ->rows(2)
                            ->columnSpanFull(),
                    ])->columns(2),

                Forms\\Components\\Section::make('Total')
                    ->schema([
                        Forms\\Components\\TextInput::make('total_amount')
                            ->label('Total Transaksi')
                            ->numeric()
                            ->prefix('Rp')
                            ->default(0)
                            ->disabled()
                            ->dehydrated(),
                    ])->hiddenOn('create'),
            ]);
    }

    public static function table(Table $table): Table
    {
        return $table
            ->columns([
                Tables\\Columns\\TextColumn::make('code')
                    ->label('Kode')
                    ->searchable()
                    ->sortable()
                    ->copyable()
                    ->copyMessage('Kode disalin!'),

                Tables\\Columns\\TextColumn::make('customer_name')
                    ->label('Customer')
                    ->searchable()
                    ->description(fn (Transaction $record): string => $record->customer_phone ?? '-'),

                Tables\\Columns\\TextColumn::make('items_count')
                    ->label('Items')
                    ->counts('items')
                    ->badge()
                    ->color('info'),

                Tables\\Columns\\TextColumn::make('total_amount')
                    ->label('Total')
                    ->money('IDR')
                    ->sortable()
                    ->weight('bold'),

                Tables\\Columns\\TextColumn::make('status')
                    ->label('Status')
                    ->badge()
                    ->color(fn (string $state): string => match ($state) {
                        'pending' => 'gray',
                        'processing' => 'warning',
                        'completed' => 'success',
                        'cancelled' => 'danger',
                    })
                    ->formatStateUsing(fn (string $state): string => match ($state) {
                        'pending' => 'Pending',
                        'processing' => 'Diproses',
                        'completed' => 'Selesai',
                        'cancelled' => 'Dibatalkan',
                    }),

                Tables\\Columns\\TextColumn::make('created_at')
                    ->label('Tanggal')
                    ->dateTime('d M Y H:i')
                    ->sortable(),
            ])
            ->defaultSort('created_at', 'desc')
            ->filters([
                Tables\\Filters\\SelectFilter::make('status')
                    ->options([
                        'pending' => 'Pending',
                        'processing' => 'Diproses',
                        'completed' => 'Selesai',
                        'cancelled' => 'Dibatalkan',
                    ]),

                Tables\\Filters\\Filter::make('created_at')
                    ->form([
                        Forms\\Components\\DatePicker::make('from')
                            ->label('Dari Tanggal'),
                        Forms\\Components\\DatePicker::make('until')
                            ->label('Sampai Tanggal'),
                    ])
                    ->query(function ($query, array $data) {
                        return $query
                            ->when($data['from'], fn ($q, $date) => $q->whereDate('created_at', '>=', $date))
                            ->when($data['until'], fn ($q, $date) => $q->whereDate('created_at', '<=', $date));
                    }),
            ])
            ->actions([
                Tables\\Actions\\ViewAction::make(),
                Tables\\Actions\\EditAction::make(),
            ])
            ->bulkActions([
                Tables\\Actions\\BulkActionGroup::make([
                    Tables\\Actions\\DeleteBulkAction::make(),
                ]),
            ]);
    }

    public static function getRelations(): array
    {
        return [
            //
        ];
    }

    public static function getPages(): array
    {
        return [
            'index' => Pages\\ListTransactions::route('/'),
            'create' => Pages\\CreateTransaction::route('/create'),
            'view' => Pages\\ViewTransaction::route('/{record}'),
            'edit' => Pages\\EditTransaction::route('/{record}/edit'),
        ];
    }
}

Buat juga halaman View untuk transaksi:

php artisan make:filament-page ViewTransaction --resource=TransactionResource --type=ViewRecord

Fitur menarik:

  1. Auto Generate Code — Kode transaksi otomatis di-generate
  2. Status Badge dengan Emoji — Visual yang jelas untuk setiap status
  3. Copyable Column — Kode bisa di-copy dengan satu klik
  4. Date Range Filter — Filter transaksi berdasarkan rentang tanggal
  5. Items Count — Menampilkan jumlah item per transaksi

Widget Statistik Dashboard

Sekarang bikin widget untuk menampilkan statistik di dashboard:

php artisan make:filament-widget StatsOverview --stats-overview

Edit app/Filament/Widgets/StatsOverview.php:

<?php

namespace App\\Filament\\Widgets;

use App\\Models\\Category;
use App\\Models\\Product;
use App\\Models\\Transaction;
use Filament\\Widgets\\StatsOverviewWidget as BaseWidget;
use Filament\\Widgets\\StatsOverviewWidget\\Stat;

class StatsOverview extends BaseWidget
{
    protected function getStats(): array
    {
        $todayRevenue = Transaction::whereDate('created_at', today())
            ->where('status', 'completed')
            ->sum('total_amount');

        $yesterdayRevenue = Transaction::whereDate('created_at', today()->subDay())
            ->where('status', 'completed')
            ->sum('total_amount');

        $revenueChange = $yesterdayRevenue > 0
            ? round((($todayRevenue - $yesterdayRevenue) / $yesterdayRevenue) * 100, 1)
            : 0;

        return [
            Stat::make('Total Kategori', Category::where('is_active', true)->count())
                ->description('Kategori aktif')
                ->descriptionIcon('heroicon-m-tag')
                ->color('primary'),

            Stat::make('Total Produk', Product::where('is_active', true)->count())
                ->description(Product::where('stock', '<=', 10)->count() . ' stok rendah')
                ->descriptionIcon('heroicon-m-cube')
                ->color('success'),

            Stat::make('Transaksi Hari Ini', Transaction::whereDate('created_at', today())->count())
                ->description(Transaction::where('status', 'pending')->count() . ' pending')
                ->descriptionIcon('heroicon-m-shopping-cart')
                ->color('warning'),

            Stat::make('Pendapatan Hari Ini', 'Rp ' . number_format($todayRevenue, 0, ',', '.'))
                ->description($revenueChange >= 0 ? "+{$revenueChange}%" : "{$revenueChange}%")
                ->descriptionIcon($revenueChange >= 0 ? 'heroicon-m-arrow-trending-up' : 'heroicon-m-arrow-trending-down')
                ->color($revenueChange >= 0 ? 'success' : 'danger'),
        ];
    }
}

Widget ini menampilkan:

  • Total kategori aktif
  • Total produk aktif + warning stok rendah
  • Transaksi hari ini + pending count
  • Pendapatan hari ini + perbandingan dengan kemarin

Hasil Akhir

Sekarang refresh admin panel di browser. Kamu akan lihat:

DASHBOARD WARUNG ONLINE:

📊 Dashboard
├── Stats: Kategori | Produk | Transaksi | Pendapatan

📁 Master Data
├── Categories (6 data)
│   └── CRUD dengan auto-slug, products count, toggle filter
└── Products (31 data)
    └── CRUD dengan kategori dropdown, image upload, stok badge

🛒 Transaksi
└── Transactions (20 data)
    └── CRUD dengan status badge, date filter, copyable code

Dalam waktu kurang dari 2 jam, kita sudah punya admin panel yang:

  • ✅ Full CRUD untuk 3 entitas
  • ✅ Relasi antar tabel
  • ✅ Filter dan search
  • ✅ File upload
  • ✅ Status management
  • ✅ Dashboard statistik

That's the power of Filament! 🚀


Bagian 6: Penutup dan Rekomendasi Kelas BuildWithAngga

Apa yang Sudah Kita Buat

Mari recap perjalanan kita:

✅ COMPLETED:

1. Setup Project
   ├── Laravel 11 fresh installation
   ├── Filament 4 installation
   └── SQLite database setup

2. Database Design
   ├── 4 migrations (categories, products, transactions, transaction_items)
   ├── 4 models dengan relationships
   └── ERD yang terstruktur

3. Data Seeding
   ├── 6 kategori warung
   ├── 31 produk realistis
   └── 20 transaksi dengan items

4. Filament Resources
   ├── CategoryResource dengan auto-slug dan products count
   ├── ProductResource dengan image upload dan stok badge
   ├── TransactionResource dengan status badge dan date filter
   └── StatsOverview widget dengan pendapatan tracking

Dari nol sampai fully functional admin panel dalam waktu sekitar 1-2 jam. Coba bayangkan berapa lama kalau bikin manual dari scratch?

Next Steps untuk Pengembangan

Kalau mau lanjut develop project ini, beberapa ide:

FITUR YANG BISA DITAMBAHKAN:

📦 Transaction Items Management
├── Repeater untuk add items di form transaksi
├── Auto-calculate total
└── Stok auto-decrease saat transaksi complete

📊 Advanced Reporting
├── Chart widget (pendapatan per hari/minggu/bulan)
├── Top selling products
└── Export ke Excel/PDF

🔐 User Management
├── Multi-user dengan role (admin, kasir)
├── Activity log
└── Permission per resource

🎨 Customization
├── Custom theme/branding
├── Multi-panel (admin panel + kasir panel)
└── Custom dashboard per role

⚡ Performance
├── Caching untuk stats
├── Queue untuk heavy operations
└── Optimize database queries

Rekomendasi Kelas Gratis di BuildWithAngga

Mau belajar lebih dalam? Berikut kelas-kelas gratis yang recommended di BuildWithAngga:

🎓 KELAS GRATIS RECOMMENDED:

1. Laravel 11 Fundamental
   ├── Dasar-dasar Laravel untuk pemula
   ├── Routing, Controller, Model, View
   ├── Eloquent ORM dan Database
   └── 🔗 buildwithangga.com/kelas/laravel-11-fundamental

2. Filament Admin Panel
   ├── Deep dive Filament lebih lengkap
   ├── Custom forms dan tables
   ├── Widgets dan dashboard
   └── 🔗 buildwithangga.com/kelas/filament-admin-panel

3. Laravel Livewire
   ├── Reactive components tanpa JavaScript
   ├── Real-time features
   ├── Complement untuk Filament
   └── 🔗 buildwithangga.com/kelas/laravel-livewire

4. Laravel API Development
   ├── Bikin REST API
   ├── Authentication dengan Sanctum
   ├── Untuk integrasi mobile/frontend
   └── 🔗 buildwithangga.com/kelas/laravel-api

5. Database Design Mastery
   ├── Design database yang optimal
   ├── Normalization dan denormalization
   ├── Query optimization
   └── 🔗 buildwithangga.com/kelas/database-design

Semua kelas di atas GRATIS dan bisa diakses kapan saja. Perfect untuk melengkapi skill Laravel dan Filament kamu!

Closing

Filament benar-benar game-changer untuk Laravel developer. Dengan investment waktu belajar yang relatif kecil, kamu bisa deliver admin panel yang professional dan feature-rich dalam hitungan jam, bukan minggu.

Project Warung Online ini baru permukaan dari apa yang bisa Filament lakukan. Dengan fondasi yang sudah kita buat, kamu bisa extend sesuai kebutuhan — tambah fitur inventory management, laporan keuangan, integrasi payment gateway, dan masih banyak lagi.

Yang paling penting: mulai dari yang simple, iterate, dan terus belajar.

Selamat coding! 🚀


Resources: