Tuesday, 4 August 2026

Laravel 13 + Filament 5 Inventory Management System - Complete Tutorial Series (2026)

Are you looking to build a real-world Inventory Management System using Laravel 13 and Filament 5? You are at the right place. In this step-by-step tutorial series, we will build a complete, production-ready Inventory Management System from scratch - including product management, stock tracking, purchases, sales, multi-warehouse support, and reports.

This is a free video + source code tutorial series, and this blog post will be updated with source code and explanation as soon as every new part is published on our YouTube channel Webslesson.

Bookmark this page - we will keep adding new parts, source code, and downloadable files here as the series continues.



What You Will Learn in This Series


  • How to install Laravel 13 and Filament 5 from scratch
  • How to design a real-world database schema for an inventory system
  • How to build Filament Resources for products, categories, purchases, sales, and stock
  • How to manage multi-warehouse stock tracking
  • How to generate reports and dashboards using Filament Widgets
  • How to deploy the final project live



Technologies Used


TechnologyPurpose
Laravel 13Backend framework
Filament 5Admin panel builder
MySQLDatabase
Livewire 4Reactive UI components (used internally by Filament 5)



Part 1 - Installation & Setup Guide


Video Title: Installation & Setup Guide | Laravel Filament 5 Complete Course 2026 | Beginner to Advanced – Part 1

In the first part of this series, we cover the complete setup process needed before we start building the actual project. Here is everything covered in this part, along with the exact commands used.

Step 1: Create a New Laravel 13 Project


composer create-project laravel/laravel inventory-management
cd inventory-management

Step 2: Install Filament 5


composer require filament/filament:"^5.0"

Step 3: Install the Filament Admin Panel


php artisan filament:install --panels

This command creates a new panel provider file at:

app/Providers/Filament/AdminPanelProvider.php

It also sets up the default panel route, which you can access at /admin once your server is running.

Step 4: Run the Default Laravel Migration


Laravel already ships with a few default migration files (users table, cache table, jobs table). We run these first to set up the base authentication system:

php artisan migrate

Step 5: Create the Admin User


To log in to the Filament admin panel, we need at least one user account:

php artisan make:filament-user

This will ask for a name, email, and password in the terminal, and create your first admin account.

Step 6: Start the Development Server


php artisan serve

Now open your browser and visit:

http://127.0.0.1:8000/admin

Log in using the admin credentials you just created, and you will see the default Filament dashboard - ready for us to start building on top of it.


Part 2 - Database Design


Video Title: Database Design | Laravel Filament 5 Complete Course 2026 | Inventory Management System – Part 2

In part 2, we design the complete database structure for our Inventory Management System. A solid database design is the foundation of the entire project, so we planned this carefully before writing any Filament code.





Database Tables Used in This Project


Our Inventory Management System uses the following 12 tables:

  1. categories - product categories, with parent/child support
  2. units - measuring units (pcs, kg, box, etc.)
  3. suppliers - vendors we purchase stock from
  4. customers - people we sell products to
  5. warehouses - physical stock locations
  6. products - the core product table
  7. purchases - purchase order records
  8. purchase_items - products inside each purchase order
  9. sales - sale/invoice records
  10. sale_items - products inside each sale
  11. stocks - current stock quantity per product per warehouse
  12. stock_adjustments - manual stock corrections (damage, loss, etc.)

Step 1: Create Migration Files


Run the following commands in order, since some tables depend on others through foreign keys:

php artisan make:migration create_categories_table
php artisan make:migration create_units_table
php artisan make:migration create_suppliers_table
php artisan make:migration create_customers_table
php artisan make:migration create_warehouses_table
php artisan make:migration create_products_table
php artisan make:migration create_purchases_table
php artisan make:migration create_purchase_items_table
php artisan make:migration create_sales_table
php artisan make:migration create_sale_items_table
php artisan make:migration create_stocks_table
php artisan make:migration create_stock_adjustments_table

Each command generates a migration file inside:

database/migrations/

Step 2: Categories Table


File path: database/migrations/xxxx_xx_xx_xxxxxx_create_categories_table.php

Schema::create('categories', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('slug')->unique();
    $table->foreignId('parent_id')->nullable()->constrained('categories')->nullOnDelete();
    $table->boolean('status')->default(true);
    $table->timestamps();
});

Step 3: Units Table


File path: database/migrations/xxxx_xx_xx_xxxxxx_create_units_table.php

Schema::create('units', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('short_code');
    $table->timestamps();
});

Step 4: Suppliers Table


File path: database/migrations/xxxx_xx_xx_xxxxxx_create_suppliers_table.php

Schema::create('suppliers', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('email')->nullable();
    $table->string('phone')->nullable();
    $table->text('address')->nullable();
    $table->boolean('status')->default(true);
    $table->timestamps();
});

Step 5: Customers Table


File path: database/migrations/xxxx_xx_xx_xxxxxx_create_customers_table.php

Schema::create('customers', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('email')->nullable();
    $table->string('phone')->nullable();
    $table->text('address')->nullable();
    $table->boolean('status')->default(true);
    $table->timestamps();
});

Step 6: Warehouses Table


File path: database/migrations/xxxx_xx_xx_xxxxxx_create_warehouses_table.php

Schema::create('warehouses', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('location')->nullable();
    $table->boolean('status')->default(true);
    $table->timestamps();
});

Step 7: Products Table


File path: database/migrations/xxxx_xx_xx_xxxxxx_create_products_table.php

Schema::create('products', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('slug')->unique();
    $table->string('sku')->unique();
    $table->foreignId('category_id')->constrained('categories')->cascadeOnDelete();
    $table->foreignId('unit_id')->constrained('units')->cascadeOnDelete();
    $table->decimal('cost_price', 12, 2)->default(0);
    $table->decimal('selling_price', 12, 2)->default(0);
    $table->string('image')->nullable();
    $table->text('description')->nullable();
    $table->boolean('status')->default(true);
    $table->timestamps();
});

Step 8: Purchases Table


File path: database/migrations/xxxx_xx_xx_xxxxxx_create_purchases_table.php

Schema::create('purchases', function (Blueprint $table) {
    $table->id();
    $table->foreignId('supplier_id')->constrained('suppliers')->cascadeOnDelete();
    $table->foreignId('warehouse_id')->constrained('warehouses')->cascadeOnDelete();
    $table->string('invoice_no')->unique();
    $table->date('purchase_date');
    $table->decimal('total_amount', 12, 2)->default(0);
    $table->decimal('paid_amount', 12, 2)->default(0);
    $table->decimal('due_amount', 12, 2)->default(0);
    $table->string('status')->default('pending');
    $table->timestamps();
});

Step 9: Purchase Items Table


File path: database/migrations/xxxx_xx_xx_xxxxxx_create_purchase_items_table.php

Schema::create('purchase_items', function (Blueprint $table) {
    $table->id();
    $table->foreignId('purchase_id')->constrained('purchases')->cascadeOnDelete();
    $table->foreignId('product_id')->constrained('products')->cascadeOnDelete();
    $table->integer('quantity');
    $table->decimal('unit_cost', 12, 2);
    $table->decimal('subtotal', 12, 2);
    $table->timestamps();
});

Step 10: Sales Table


File path: database/migrations/xxxx_xx_xx_xxxxxx_create_sales_table.php

Schema::create('sales', function (Blueprint $table) {
    $table->id();
    $table->foreignId('customer_id')->constrained('customers')->cascadeOnDelete();
    $table->foreignId('warehouse_id')->constrained('warehouses')->cascadeOnDelete();
    $table->string('invoice_no')->unique();
    $table->date('sale_date');
    $table->decimal('total_amount', 12, 2)->default(0);
    $table->decimal('paid_amount', 12, 2)->default(0);
    $table->decimal('due_amount', 12, 2)->default(0);
    $table->string('status')->default('pending');
    $table->timestamps();
});

Step 11: Sale Items Table


File path: database/migrations/xxxx_xx_xx_xxxxxx_create_sale_items_table.php

Schema::create('sale_items', function (Blueprint $table) {
    $table->id();
    $table->foreignId('sale_id')->constrained('sales')->cascadeOnDelete();
    $table->foreignId('product_id')->constrained('products')->cascadeOnDelete();
    $table->integer('quantity');
    $table->decimal('unit_price', 12, 2);
    $table->decimal('subtotal', 12, 2);
    $table->timestamps();
});

Step 12: Stocks Table


File path: database/migrations/xxxx_xx_xx_xxxxxx_create_stocks_table.php

Schema::create('stocks', function (Blueprint $table) {
    $table->id();
    $table->foreignId('product_id')->constrained('products')->cascadeOnDelete();
    $table->foreignId('warehouse_id')->constrained('warehouses')->cascadeOnDelete();
    $table->integer('quantity')->default(0);
    $table->timestamps();

    $table->unique(['product_id', 'warehouse_id']);
});

Step 13: Stock Adjustments Table


File path: database/migrations/xxxx_xx_xx_xxxxxx_create_stock_adjustments_table.php

Schema::create('stock_adjustments', function (Blueprint $table) {
    $table->id();
    $table->foreignId('product_id')->constrained('products')->cascadeOnDelete();
    $table->foreignId('warehouse_id')->constrained('warehouses')->cascadeOnDelete();
    $table->enum('type', ['add', 'subtract']);
    $table->integer('quantity');
    $table->string('reason')->nullable();
    $table->foreignId('adjusted_by')->nullable()->constrained('users')->nullOnDelete();
    $table->timestamps();
});

Step 14: Run the Migration


Once all migration files are ready, run:

php artisan migrate

This creates all 12 tables in your database in the correct order.

Step 15: Create Eloquent Models


php artisan make:model Category
php artisan make:model Unit
php artisan make:model Supplier
php artisan make:model Customer
php artisan make:model Warehouse
php artisan make:model Product
php artisan make:model Purchase
php artisan make:model PurchaseItem
php artisan make:model Sale
php artisan make:model SaleItem
php artisan make:model Stock
php artisan make:model StockAdjustment

Each model is generated inside:

app/Models/

Category Model (app/Models/Category.php):

class Category extends Model
{
    protected $fillable = ['name', 'slug', 'parent_id', 'status'];
    protected $casts = ['status' => 'boolean'];

    public function parent()
    {
        return $this->belongsTo(Category::class, 'parent_id');
    }

    public function children()
    {
        return $this->hasMany(Category::class, 'parent_id');
    }

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

Unit Model (app/Models/Unit.php):

class Unit extends Model
{
    protected $fillable = ['name', 'short_code'];

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

Supplier Model (app/Models/Supplier.php):

class Supplier extends Model
{
    protected $fillable = ['name', 'email', 'phone', 'address', 'status'];
    protected $casts = ['status' => 'boolean'];
    public function purchases()
    {
        return $this->hasMany(Purchase::class);
    }
}

Customer Model (app/Models/Customer.php):

class Customer extends Model
{
    protected $fillable = ['name', 'email', 'phone', 'address', 'status'];
    protected $casts = ['status' => 'boolean'];
    public function sales()
    {
        return $this->hasMany(Sale::class);
    }
}

Warehouse Model (app/Models/Warehouse.php):

class Warehouse extends Model
{
    protected $fillable = ['name', 'location', 'status'];
    protected $casts = ['status' => 'boolean'];
    public function stocks()
    {
        return $this->hasMany(Stock::class);
    }

    public function purchases()
    {
        return $this->hasMany(Purchase::class);
    }

    public function sales()
    {
        return $this->hasMany(Sale::class);
    }
}

Product Model (app/Models/Product.php):

class Product extends Model
{
    protected $fillable = [
        'name', 'slug', 'sku', 'category_id', 'unit_id',
        'cost_price', 'selling_price', 'image', 'description', 'status',
    ];
    protected $casts = [
        'status' => 'boolean',
        'cost_price' => 'decimal:2',
        'selling_price' => 'decimal:2',
    ];
    public function category()
    {
        return $this->belongsTo(Category::class);
    }
    public function unit()
    {
        return $this->belongsTo(Unit::class);
    }
    public function stocks()
    {
        return $this->hasMany(Stock::class);
    }
    public function purchaseItems()
    {
        return $this->hasMany(PurchaseItem::class);
    }
    public function saleItems()
    {
        return $this->hasMany(SaleItem::class);
    }
    public function stockAdjustments()
    {
        return $this->hasMany(StockAdjustment::class);
    }
    // Helper: total stock across all warehouses
    public function totalStock()
    {
        return $this->stocks()->sum('quantity');
    }
}

Purchase Model (app/Models/Purchase.php):

class Purchase extends Model
{
    protected $fillable = [
        'supplier_id', 'warehouse_id', 'invoice_no', 'purchase_date',
        'total_amount', 'paid_amount', 'due_amount', 'status',
    ];
    protected $casts = [
        'purchase_date' => 'date',
        'total_amount' => 'decimal:2',
        'paid_amount' => 'decimal:2',
        'due_amount' => 'decimal:2',
    ];
    public function supplier()
    {
        return $this->belongsTo(Supplier::class);
    }
    public function warehouse()
    {
        return $this->belongsTo(Warehouse::class);
    }
    public function items()
    {
        return $this->hasMany(PurchaseItem::class);
    }
}

PurchaseItem Model (app/Models/PurchaseItem.php):

class PurchaseItem extends Model
{
    protected $fillable = ['purchase_id', 'product_id', 'quantity', 'unit_cost', 'subtotal'];
    protected $casts = [
        'unit_cost' => 'decimal:2',
        'subtotal' => 'decimal:2',
    ];
    public function purchase()
    {
        return $this->belongsTo(Purchase::class);
    }
    public function product()
    {
        return $this->belongsTo(Product::class);
    }
}

Sale Model (app/Models/Sale.php):

class Sale extends Model
{
    protected $fillable = [
        'customer_id', 'warehouse_id', 'invoice_no', 'sale_date',
        'total_amount', 'paid_amount', 'due_amount', 'status',
    ];
    protected $casts = [
        'sale_date' => 'date',
        'total_amount' => 'decimal:2',
        'paid_amount' => 'decimal:2',
        'due_amount' => 'decimal:2',
    ];
    public function customer()
    {
        return $this->belongsTo(Customer::class);
    }
    public function warehouse()
    {
        return $this->belongsTo(Warehouse::class);
    }
    public function items()
    {
        return $this->hasMany(SaleItem::class);
    }
}

SaleItem Model (app/Models/SaleItem.php):

class SaleItem extends Model
{
    protected $fillable = ['sale_id', 'product_id', 'quantity', 'unit_price', 'subtotal'];
    protected $casts = [
        'unit_price' => 'decimal:2',
        'subtotal' => 'decimal:2',
    ];
    public function sale()
    {
        return $this->belongsTo(Sale::class);
    }
    public function product()
    {
        return $this->belongsTo(Product::class);
    }
}

Stock Model (app/Models/Stock.php):

class Stock extends Model
{
    protected $fillable = ['product_id', 'warehouse_id', 'quantity'];
    public function product()
    {
        return $this->belongsTo(Product::class);
    }
    public function warehouse()
    {
        return $this->belongsTo(Warehouse::class);
    }
}

StockAdjustment Model (app/Models/StockAdjustment.php):

class StockAdjustment extends Model
{
    protected $fillable = [
        'product_id', 'warehouse_id', 'type', 'quantity', 'reason', 'adjusted_by',
    ];
    public function product()
    {
        return $this->belongsTo(Product::class);
    }
    public function warehouse()
    {
        return $this->belongsTo(Warehouse::class);
    }
    public function adjustedBy()
    {
        return $this->belongsTo(User::class, 'adjusted_by');
    }
}


Part 4 — Category Management (Filament Resource, CRUD, Parent/Child Subcategory)


Video Title: Category Management with Filament Resource, CRUD & Subcategory | Laravel 13 + Filament 5 Inventory System – Part 4

In part 4, we build our very first real feature using a Filament Resource — the Category Management module. This gives us full create, read, update, and delete functionality, parent/child subcategory support, and a status toggle, all without writing a single controller or blade file.

Step 1: Generate the Filament Resource

php artisan make:filament-resource Category --generate

This command creates the following files automatically (Filament 5 uses a new subfolder structure):

app/Filament/Resources/Categories/CategoryResource.php
app/Filament/Resources/Categories/Schemas/CategoryForm.php
app/Filament/Resources/Categories/Tables/CategoriesTable.php
app/Filament/Resources/Categories/Pages/ListCategories.php
app/Filament/Resources/Categories/Pages/CreateCategory.php
app/Filament/Resources/Categories/Pages/EditCategory.php

Step 2: Enable Resource Discovery (Required in Filament 5)


Filament 5 does not auto-detect resources anymore. Add this to AdminPanelProvider.php so your resources appear in the sidebar:

->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources')
->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages')
->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets')

Step 3: CategoryResource.php


File path: app/Filament/Resources/Categories/CategoryResource.php

<?php

namespace App\Filament\Resources\Categories;

use App\Filament\Resources\Categories\Pages\CreateCategory;
use App\Filament\Resources\Categories\Pages\EditCategory;
use App\Filament\Resources\Categories\Pages\ListCategories;
use App\Filament\Resources\Categories\Schemas\CategoryForm;
use App\Filament\Resources\Categories\Tables\CategoriesTable;
use App\Models\Category;
use BackedEnum;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Tables\Table;
use UnitEnum;

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

    protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-tag';

    protected static string|UnitEnum|null $navigationGroup = 'Master Data';

    protected static ?string $navigationLabel = 'Categories';

    protected static ?int $navigationSort = 1;

    protected static ?string $recordTitleAttribute = 'name';

    public static function form(Schema $schema): Schema
    {
        return CategoryForm::configure($schema);
    }

    public static function table(Table $table): Table
    {
        return CategoriesTable::configure($table);
    }

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

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

Step 4: CategoryForm.php


File path: app/Filament/Resources/Categories/Schemas/CategoryForm.php

<?php

namespace App\Filament\Resources\Categories\Schemas;

use App\Models\Category;
use Filament\Schemas\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Schemas\Schema;
use Illuminate\Support\Str;

class CategoryForm
{
    public static function configure(Schema $schema): Schema
    {
        return $schema
            ->components([
                Section::make('Category Information')
                    ->schema([
                        Select::make('parent_id')
                            ->label('Parent Category')
                            ->placeholder('Select parent category (optional)')
                            ->options(
                                Category::whereNull('parent_id')
                                    ->pluck('name', 'id')
                            )
                            ->nullable()
                            ->searchable()
                            ->preload()
                            ->columnSpanFull(),

                        TextInput::make('name')
                            ->label('Category Name')
                            ->required()
                            ->maxLength(255)
                            ->live(onBlur: true)
                            ->afterStateUpdated(function (string $operation, $state, Set $set) {
                                if ($operation === 'create') {
                                    $set('slug', Str::slug($state));
                                }
                            }),

                        TextInput::make('slug')
                            ->label('Slug')
                            ->required()
                            ->maxLength(255)
                            ->unique(Category::class, 'slug', ignoreRecord: true)
                            ->helperText('Slug is auto generated from category name'),

                        Toggle::make('status')
                            ->label('Active Status')
                            ->default(true)
                            ->onColor('success')
                            ->offColor('danger')
                            ->columnSpanFull(),
                    ])
                    ->columns(2),
            ]);
    }
}

Step 5: CategoriesTable.php


File path: app/Filament/Resources/Categories/Tables/CategoriesTable.php

<?php

namespace App\Filament\Resources\Categories\Tables;

use App\Models\Category;
use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\ToggleColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;

class CategoriesTable
{
    public static function configure(Table $table): Table
    {
        return $table
            ->columns([
                TextColumn::make('name')
                    ->label('Category Name')
                    ->searchable()
                    ->sortable(),

                TextColumn::make('parent.name')
                    ->label('Parent Category')
                    ->default('—')
                    ->searchable()
                    ->sortable(),

                TextColumn::make('slug')
                    ->label('Slug')
                    ->searchable(),

                TextColumn::make('children_count')
                    ->label('Subcategories')
                    ->counts('children')
                    ->badge()
                    ->color('info'),

                ToggleColumn::make('status')
                    ->label('Status')
                    ->onColor('success')
                    ->offColor('danger'),

                TextColumn::make('created_at')
                    ->label('Created At')
                    ->dateTime('d M Y')
                    ->sortable()
                    ->toggleable(isToggledHiddenByDefault: true),
            ])
            ->filters([
                SelectFilter::make('status')
                    ->label('Status')
                    ->options([
                        '1' => 'Active',
                        '0' => 'Inactive',
                    ]),

                SelectFilter::make('parent_id')
                    ->label('Parent Category')
                    ->options(
                        Category::whereNull('parent_id')
                            ->pluck('name', 'id')
                    )
                    ->placeholder('All Categories'),
            ])
            ->actions([
                EditAction::make(),
                DeleteAction::make(),
            ])
            ->bulkActions([
                BulkActionGroup::make([
                    DeleteBulkAction::make(),
                ]),
            ])
            ->defaultSort('created_at', 'desc');
    }
}

Step 6: Category Model (Updated)


File path: app/Models/Category.php

class Category extends Model
{
    protected $fillable = ['name', 'slug', 'parent_id', 'status'];
    protected $casts = ['status' => 'boolean'];

    public function parent()
    {
        return $this->belongsTo(Category::class, 'parent_id');
    }

    public function children()
    {
        return $this->hasMany(Category::class, 'parent_id');
    }

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

Filament 5 namespace note: If you are upgrading old Filament 3 code, remember that EditAction, DeleteAction, BulkActionGroup, and DeleteBulkAction moved to Filament\Actions, while Section and Set moved to Filament\Schemas\Components. Also, $navigationIcon and $navigationGroup property types must exactly match the parent Resource class: string|BackedEnum|null and string|UnitEnum|null respectively.


Part 5 — Unit Management (Filament 5 Resource, CRUD)


Video Title: Build Unit Module with Filament 5 Resource | Laravel 13 Inventory System 2026 – Part 5

In part 5, we build the Unit Management module using the same Filament 5 Resource pattern we learned in part 4. Units are simpler than categories since there is no parent/child relationship — this makes it a great part to reinforce the workflow while learning it a little faster. Every product in our system will later be linked to one of these units (piece, kilogram, box, litre, etc).

Step 1: Generate the Filament Resource

php artisan make:filament-resource Unit --generate

This creates the following files (same Filament 5 subfolder structure used in Part 4):

app/Filament/Resources/Units/UnitResource.php
app/Filament/Resources/Units/Schemas/UnitForm.php
app/Filament/Resources/Units/Tables/UnitsTable.php
app/Filament/Resources/Units/Pages/ListUnits.php
app/Filament/Resources/Units/Pages/CreateUnit.php
app/Filament/Resources/Units/Pages/EditUnit.php

Step 2: Update the Unit Model


File path: app/Models/Unit.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Unit extends Model
{
    protected $fillable = [
        'name',
        'short_code',
    ];

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

Step 3: UnitResource.php


File path: app/Filament/Resources/Units/UnitResource.php

<?php

namespace App\Filament\Resources\Units;

use App\Filament\Resources\Units\Pages\CreateUnit;
use App\Filament\Resources\Units\Pages\EditUnit;
use App\Filament\Resources\Units\Pages\ListUnits;
use App\Filament\Resources\Units\Schemas\UnitForm;
use App\Filament\Resources\Units\Tables\UnitsTable;
use App\Models\Unit;
use BackedEnum;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Tables\Table;
use UnitEnum;

class UnitResource extends Resource
{
    protected static ?string $model = Unit::class;

    protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-scale';

    protected static string|UnitEnum|null $navigationGroup = 'Master Data';

    protected static ?string $navigationLabel = 'Units';

    protected static ?int $navigationSort = 2;

    protected static ?string $recordTitleAttribute = 'name';

    public static function form(Schema $schema): Schema
    {
        return UnitForm::configure($schema);
    }

    public static function table(Table $table): Table
    {
        return UnitsTable::configure($table);
    }

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

    public static function getPages(): array
    {
        return [
            'index'  => ListUnits::route('/'),
            'create' => CreateUnit::route('/create'),
            'edit'   => EditUnit::route('/{record}/edit'),
        ];
    }
}

Note: $navigationSort = 2 is set here because Categories already uses sort number 1. This ensures Units always appears right below Categories inside the Master Data group in the sidebar.


Step 4: UnitForm.php


File path: app/Filament/Resources/Units/Schemas/UnitForm.php

<?php

namespace App\Filament\Resources\Units\Schemas;

use App\Models\Unit;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Schemas\Schema;
use Illuminate\Support\Str;

class UnitForm
{
    public static function configure(Schema $schema): Schema
    {
        return $schema
            ->components([
                Section::make('Unit Information')
                    ->schema([
                        TextInput::make('name')
                            ->label('Unit Name')
                            ->placeholder('e.g. Piece, Kilogram, Box')
                            ->required()
                            ->maxLength(255),

                        TextInput::make('short_code')
                            ->label('Short Code')
                            ->placeholder('e.g. pcs, kg, box')
                            ->required()
                            ->maxLength(20)
                            ->unique(Unit::class, 'short_code', ignoreRecord: true)
                            ->live(onBlur: true)
                            ->afterStateUpdated(function ($state, Set $set) {
                                $set('short_code', Str::upper($state));
                            })
                            ->helperText('This will be shown next to product quantities'),
                    ])
                    ->columns(2),
            ]);
    }
}

A nice touch in this form: the short code field automatically converts to uppercase as the user types, using live() combined with afterStateUpdated() and Laravel's Str::upper() helper — so typing kg instantly becomes KG.

Step 5: UnitsTable.php


File path: app/Filament/Resources/Units/Tables/UnitsTable.php

<?php

namespace App\Filament\Resources\Units\Tables;

use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;

class UnitsTable
{
    public static function configure(Table $table): Table
    {
        return $table
            ->columns([
                TextColumn::make('name')
                    ->label('Unit Name')
                    ->searchable()
                    ->sortable(),

                TextColumn::make('short_code')
                    ->label('Short Code')
                    ->badge()
                    ->color('info')
                    ->searchable(),

                TextColumn::make('products_count')
                    ->label('Products Using This Unit')
                    ->counts('products')
                    ->badge()
                    ->color('success'),

                TextColumn::make('created_at')
                    ->label('Created At')
                    ->dateTime('d M Y')
                    ->sortable()
                    ->toggleable(isToggledHiddenByDefault: true),
            ])
            ->actions([
                EditAction::make(),
                DeleteAction::make(),
            ])
            ->bulkActions([
                BulkActionGroup::make([
                    DeleteBulkAction::make(),
                ]),
            ])
            ->defaultSort('created_at', 'desc');
    }
}

Unlike the Categories table, this table has no filters — Units is a simple flat list with no status field or category to filter by. The Products Using This Unit column uses Filament's counts() method to automatically show how many products reference each unit, which becomes useful later for knowing whether a unit is safe to delete.

Step 6: Create, Edit, and List Pages


File path: app/Filament/Resources/Units/Pages/CreateUnit.php

<?php

namespace App\Filament\Resources\Units\Pages;

use App\Filament\Resources\Units\UnitResource;
use Filament\Resources\Pages\CreateRecord;

class CreateUnit extends CreateRecord
{
    protected static string $resource = UnitResource::class;

    protected function getRedirectUrl(): string
    {
        return $this->getResource()::getUrl('index');
    }

    protected function getCreatedNotificationTitle(): ?string
    {
        return 'Unit created successfully';
    }
}

File path: app/Filament/Resources/Units/Pages/EditUnit.php

<?php

namespace App\Filament\Resources\Units\Pages;

use App\Filament\Resources\Units\UnitResource;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;

class EditUnit extends EditRecord
{
    protected static string $resource = UnitResource::class;

    protected function getHeaderActions(): array
    {
        return [
            DeleteAction::make(),
        ];
    }

    protected function getRedirectUrl(): string
    {
        return $this->getResource()::getUrl('index');
    }

    protected function getSavedNotificationTitle(): ?string
    {
        return 'Unit updated successfully';
    }
}

File path: app/Filament/Resources/Units/Pages/ListUnits.php

<?php

namespace App\Filament\Resources\Units\Pages;

use App\Filament\Resources\Units\UnitResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;

class ListUnits extends ListRecords
{
    protected static string $resource = UnitResource::class;

    protected function getHeaderActions(): array
    {
        return [
            CreateAction::make()
                ->label('New Unit'),
        ];
    }
}

All Files Created in Part 5


app/Models/Unit.php
app/Filament/Resources/Units/UnitResource.php
app/Filament/Resources/Units/Schemas/UnitForm.php
app/Filament/Resources/Units/Tables/UnitsTable.php
app/Filament/Resources/Units/Pages/ListUnits.php
app/Filament/Resources/Units/Pages/CreateUnit.php
app/Filament/Resources/Units/Pages/EditUnit.php



Part 6 — Supplier Management (Filament 5 Resource, CRUD, Filters)

Video Title: Build Supplier Module with Filament 5 Resource | Laravel 13 Inventory System 2026 – Part 6

In part 6, we build the Supplier Management module — the vendors and companies we purchase stock from. This module introduces a few new form fields (email, phone, address) and brings back table filters, which we intentionally skipped in the simpler Unit module.

Step 1: Generate the Filament Resource

php artisan make:filament-resource Supplier --generate

This creates the following files:

app/Filament/Resources/Suppliers/SupplierResource.php
app/Filament/Resources/Suppliers/Schemas/SupplierForm.php
app/Filament/Resources/Suppliers/Tables/SuppliersTable.php
app/Filament/Resources/Suppliers/Pages/ListSuppliers.php
app/Filament/Resources/Suppliers/Pages/CreateSupplier.php
app/Filament/Resources/Suppliers/Pages/EditSupplier.php

Step 2: Update the Supplier Model

File path: app/Models/Supplier.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Supplier extends Model
{
    protected $fillable = [
        'name',
        'email',
        'phone',
        'address',
        'status',
    ];

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

    public function purchases()
    {
        return $this->hasMany(Purchase::class);
    }
}

Step 3: SupplierResource.php

File path: app/Filament/Resources/Suppliers/SupplierResource.php

<?php

namespace App\Filament\Resources\Suppliers;

use App\Filament\Resources\Suppliers\Pages\CreateSupplier;
use App\Filament\Resources\Suppliers\Pages\EditSupplier;
use App\Filament\Resources\Suppliers\Pages\ListSuppliers;
use App\Filament\Resources\Suppliers\Schemas\SupplierForm;
use App\Filament\Resources\Suppliers\Tables\SuppliersTable;
use App\Models\Supplier;
use BackedEnum;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Tables\Table;
use UnitEnum;

class SupplierResource extends Resource
{
    protected static ?string $model = Supplier::class;

    protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-truck';

    protected static string|UnitEnum|null $navigationGroup = 'Master Data';

    protected static ?string $navigationLabel = 'Suppliers';

    protected static ?int $navigationSort = 3;

    protected static ?string $recordTitleAttribute = 'name';

    public static function form(Schema $schema): Schema
    {
        return SupplierForm::configure($schema);
    }

    public static function table(Table $table): Table
    {
        return SuppliersTable::configure($table);
    }

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

    public static function getPages(): array
    {
        return [
            'index'  => ListSuppliers::route('/'),
            'create' => CreateSupplier::route('/create'),
            'edit'   => EditSupplier::route('/{record}/edit'),
        ];
    }
}

Note: $navigationSort = 3 is used here since Categories is sort 1 and Units is sort 2. This keeps Suppliers positioned correctly right below Units in the Master Data group.

Step 4: SupplierForm.php

File path: app/Filament/Resources/Suppliers/Schemas/SupplierForm.php

<?php

namespace App\Filament\Resources\Suppliers\Schemas;

use App\Models\Supplier;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\Toggle;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;

class SupplierForm
{
    public static function configure(Schema $schema): Schema
    {
        return $schema
            ->components([
                Section::make('Supplier Information')
                    ->schema([
                        TextInput::make('name')
                            ->label('Supplier Name')
                            ->placeholder('e.g. ABC Trading Company')
                            ->required()
                            ->maxLength(255)
                            ->columnSpanFull(),

                        TextInput::make('email')
                            ->label('Email Address')
                            ->placeholder('supplier@example.com')
                            ->email()
                            ->maxLength(255)
                            ->unique(Supplier::class, 'email', ignoreRecord: true),

                        TextInput::make('phone')
                            ->label('Phone Number')
                            ->placeholder('e.g. +91 98765 43210')
                            ->tel()
                            ->maxLength(20),

                        Textarea::make('address')
                            ->label('Address')
                            ->placeholder('Enter full address')
                            ->rows(3)
                            ->columnSpanFull(),

                        Toggle::make('status')
                            ->label('Active Status')
                            ->default(true)
                            ->onColor('success')
                            ->offColor('danger')
                            ->columnSpanFull(),
                    ])
                    ->columns(2),
            ]);
    }
}

New in this form: ->email() adds automatic email format validation, ->tel() marks the phone field as a telephone input, and Textarea replaces TextInput for the multi-line address field. ->columnSpanFull() is used on name, address, and status so they stretch across the full form width, while email and phone sit side by side in the 2-column layout.

Step 5: SuppliersTable.php

File path: app/Filament/Resources/Suppliers/Tables/SuppliersTable.php

<?php

namespace App\Filament\Resources\Suppliers\Tables;

use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\ToggleColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;

class SuppliersTable
{
    public static function configure(Table $table): Table
    {
        return $table
            ->columns([
                TextColumn::make('name')
                    ->label('Supplier Name')
                    ->searchable()
                    ->sortable(),

                TextColumn::make('email')
                    ->label('Email')
                    ->searchable()
                    ->default('—')
                    ->icon('heroicon-o-envelope'),

                TextColumn::make('phone')
                    ->label('Phone')
                    ->searchable()
                    ->default('—')
                    ->icon('heroicon-o-phone'),

                TextColumn::make('purchases_count')
                    ->label('Purchases')
                    ->counts('purchases')
                    ->badge()
                    ->color('info'),

                ToggleColumn::make('status')
                    ->label('Status')
                    ->onColor('success')
                    ->offColor('danger'),

                TextColumn::make('created_at')
                    ->label('Created At')
                    ->dateTime('d M Y')
                    ->sortable()
                    ->toggleable(isToggledHiddenByDefault: true),
            ])
            ->filters([
                SelectFilter::make('status')
                    ->label('Status')
                    ->options([
                        '1' => 'Active',
                        '0' => 'Inactive',
                    ]),
            ])
            ->actions([
                EditAction::make(),
                DeleteAction::make(),
            ])
            ->bulkActions([
                BulkActionGroup::make([
                    DeleteBulkAction::make(),
                ]),
            ])
            ->defaultSort('created_at', 'desc');
    }
}

New in this table: ->default('—') shows a clean dash instead of a blank cell when email or phone is empty, ->icon() adds a small envelope/phone icon next to each value, and the Status filter returns — letting users instantly narrow the list to only active or only inactive suppliers.

Step 6: Create, Edit, and List Pages

File path: app/Filament/Resources/Suppliers/Pages/CreateSupplier.php

<?php

namespace App\Filament\Resources\Suppliers\Pages;

use App\Filament\Resources\Suppliers\SupplierResource;
use Filament\Resources\Pages\CreateRecord;

class CreateSupplier extends CreateRecord
{
    protected static string $resource = SupplierResource::class;

    protected function getRedirectUrl(): string
    {
        return $this->getResource()::getUrl('index');
    }

    protected function getCreatedNotificationTitle(): ?string
    {
        return 'Supplier created successfully';
    }
}

File path: app/Filament/Resources/Suppliers/Pages/EditSupplier.php

<?php

namespace App\Filament\Resources\Suppliers\Pages;

use App\Filament\Resources\Suppliers\SupplierResource;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;

class EditSupplier extends EditRecord
{
    protected static string $resource = SupplierResource::class;

    protected function getHeaderActions(): array
    {
        return [
            DeleteAction::make(),
        ];
    }

    protected function getRedirectUrl(): string
    {
        return $this->getResource()::getUrl('index');
    }

    protected function getSavedNotificationTitle(): ?string
    {
        return 'Supplier updated successfully';
    }
}

File path: app/Filament/Resources/Suppliers/Pages/ListSuppliers.php

<?php

namespace App\Filament\Resources\Suppliers\Pages;

use App\Filament\Resources\Suppliers\SupplierResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;

class ListSuppliers extends ListRecords
{
    protected static string $resource = SupplierResource::class;

    protected function getHeaderActions(): array
    {
        return [
            CreateAction::make()
                ->label('New Supplier'),
        ];
    }
}

All Files Created in Part 6

app/Models/Supplier.php
app/Filament/Resources/Suppliers/SupplierResource.php
app/Filament/Resources/Suppliers/Schemas/SupplierForm.php
app/Filament/Resources/Suppliers/Tables/SuppliersTable.php
app/Filament/Resources/Suppliers/Pages/ListSuppliers.php
app/Filament/Resources/Suppliers/Pages/CreateSupplier.php
app/Filament/Resources/Suppliers/Pages/EditSupplier.php



Part 7 — Customer Management (Filament 5 Resource, CRUD, Filters)

Video Title: Build Customer Module with Filament 5 Resource | Laravel 13 Inventory System 2026 – Part 7

In part 7, we build the Customer Management module — the people and businesses we sell our products to. This module uses almost the exact same structure as the Supplier module from part 6, just from the opposite side of the business, which makes it a great part to reinforce the pattern quickly.

Step 1: Generate the Filament Resource

php artisan make:filament-resource Customer --generate

This creates the following files:

app/Filament/Resources/Customers/CustomerResource.php
app/Filament/Resources/Customers/Schemas/CustomerForm.php
app/Filament/Resources/Customers/Tables/CustomersTable.php
app/Filament/Resources/Customers/Pages/ListCustomers.php
app/Filament/Resources/Customers/Pages/CreateCustomer.php
app/Filament/Resources/Customers/Pages/EditCustomer.php

Step 2: Update the Customer Model

File path: app/Models/Customer.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Customer extends Model
{
    protected $fillable = [
        'name',
        'email',
        'phone',
        'address',
        'status',
    ];

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

    public function sales()
    {
        return $this->hasMany(Sale::class);
    }
}

Step 3: CustomerResource.php

File path: app/Filament/Resources/Customers/CustomerResource.php

<?php

namespace App\Filament\Resources\Customers;

use App\Filament\Resources\Customers\Pages\CreateCustomer;
use App\Filament\Resources\Customers\Pages\EditCustomer;
use App\Filament\Resources\Customers\Pages\ListCustomers;
use App\Filament\Resources\Customers\Schemas\CustomerForm;
use App\Filament\Resources\Customers\Tables\CustomersTable;
use App\Models\Customer;
use BackedEnum;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Tables\Table;
use UnitEnum;

class CustomerResource extends Resource
{
    protected static ?string $model = Customer::class;

    protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-user-group';

    protected static string|UnitEnum|null $navigationGroup = 'Master Data';

    protected static ?string $navigationLabel = 'Customers';

    protected static ?int $navigationSort = 4;

    protected static ?string $recordTitleAttribute = 'name';

    public static function form(Schema $schema): Schema
    {
        return CustomerForm::configure($schema);
    }

    public static function table(Table $table): Table
    {
        return CustomersTable::configure($table);
    }

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

    public static function getPages(): array
    {
        return [
            'index'  => ListCustomers::route('/'),
            'create' => CreateCustomer::route('/create'),
            'edit'   => EditCustomer::route('/{record}/edit'),
        ];
    }
}

Note: $navigationSort = 4 keeps Customers positioned correctly right below Suppliers (sort 3), Units (sort 2), and Categories (sort 1) in the Master Data group.

Step 4: CustomerForm.php

File path: app/Filament/Resources/Customers/Schemas/CustomerForm.php

<?php

namespace App\Filament\Resources\Customers\Schemas;

use App\Models\Customer;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\Toggle;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;

class CustomerForm
{
    public static function configure(Schema $schema): Schema
    {
        return $schema
            ->components([
                Section::make('Customer Information')
                    ->schema([
                        TextInput::make('name')
                            ->label('Customer Name')
                            ->placeholder('e.g. John Smith')
                            ->required()
                            ->maxLength(255)
                            ->columnSpanFull(),

                        TextInput::make('email')
                            ->label('Email Address')
                            ->placeholder('customer@example.com')
                            ->email()
                            ->maxLength(255)
                            ->unique(Customer::class, 'email', ignoreRecord: true),

                        TextInput::make('phone')
                            ->label('Phone Number')
                            ->placeholder('e.g. +91 98765 43210')
                            ->tel()
                            ->maxLength(20),

                        Textarea::make('address')
                            ->label('Address')
                            ->placeholder('Enter full address')
                            ->rows(3)
                            ->columnSpanFull(),

                        Toggle::make('status')
                            ->label('Active Status')
                            ->default(true)
                            ->onColor('success')
                            ->offColor('danger')
                            ->columnSpanFull(),
                    ])
                    ->columns(2),
            ]);
    }
}

This form is structurally identical to the Supplier form from part 6 — same fields, same validation, same layout. The only real difference across this whole module is the relationship name (sales() instead of purchases()), which is a good example of how fast new CRUD modules become once the pattern is established.

Step 5: CustomersTable.php

File path: app/Filament/Resources/Customers/Tables/CustomersTable.php

<?php

namespace App\Filament\Resources\Customers\Tables;

use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\ToggleColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;

class CustomersTable
{
    public static function configure(Table $table): Table
    {
        return $table
            ->columns([
                TextColumn::make('name')
                    ->label('Customer Name')
                    ->searchable()
                    ->sortable(),

                TextColumn::make('email')
                    ->label('Email')
                    ->searchable()
                    ->default('—')
                    ->icon('heroicon-o-envelope'),

                TextColumn::make('phone')
                    ->label('Phone')
                    ->searchable()
                    ->default('—')
                    ->icon('heroicon-o-phone'),

                TextColumn::make('sales_count')
                    ->label('Purchases Made')
                    ->counts('sales')
                    ->badge()
                    ->color('success'),

                ToggleColumn::make('status')
                    ->label('Status')
                    ->onColor('success')
                    ->offColor('danger'),

                TextColumn::make('created_at')
                    ->label('Created At')
                    ->dateTime('d M Y')
                    ->sortable()
                    ->toggleable(isToggledHiddenByDefault: true),
            ])
            ->filters([
                SelectFilter::make('status')
                    ->label('Status')
                    ->options([
                        '1' => 'Active',
                        '0' => 'Inactive',
                    ]),
            ])
            ->actions([
                EditAction::make(),
                DeleteAction::make(),
            ])
            ->bulkActions([
                BulkActionGroup::make([
                    DeleteBulkAction::make(),
                ]),
            ])
            ->defaultSort('created_at', 'desc');
    }
}

Note the sales_count badge is colored success (green) here, compared to info (blue) for Supplier's purchases count in part 6 — a small visual cue that helps distinguish "money coming in" (sales) from "money going out" (purchases) at a glance across the admin panel.

Step 6: Create, Edit, and List Pages

File path: app/Filament/Resources/Customers/Pages/CreateCustomer.php

<?php

namespace App\Filament\Resources\Customers\Pages;

use App\Filament\Resources\Customers\CustomerResource;
use Filament\Resources\Pages\CreateRecord;

class CreateCustomer extends CreateRecord
{
    protected static string $resource = CustomerResource::class;

    protected function getRedirectUrl(): string
    {
        return $this->getResource()::getUrl('index');
    }

    protected function getCreatedNotificationTitle(): ?string
    {
        return 'Customer created successfully';
    }
}

File path: app/Filament/Resources/Customers/Pages/EditCustomer.php

<?php

namespace App\Filament\Resources\Customers\Pages;

use App\Filament\Resources\Customers\CustomerResource;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;

class EditCustomer extends EditRecord
{
    protected static string $resource = CustomerResource::class;

    protected function getHeaderActions(): array
    {
        return [
            DeleteAction::make(),
        ];
    }

    protected function getRedirectUrl(): string
    {
        return $this->getResource()::getUrl('index');
    }

    protected function getSavedNotificationTitle(): ?string
    {
        return 'Customer updated successfully';
    }
}

File path: app/Filament/Resources/Customers/Pages/ListCustomers.php

<?php

namespace App\Filament\Resources\Customers\Pages;

use App\Filament\Resources\Customers\CustomerResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;

class ListCustomers extends ListRecords
{
    protected static string $resource = CustomerResource::class;

    protected function getHeaderActions(): array
    {
        return [
            CreateAction::make()
                ->label('New Customer'),
        ];
    }
}

Step 7: Clear Cache and Run

php artisan optimize:clear
php artisan serve

Visit /admin/customers and the Customers link will now appear under Master Data, right below Categories, Units, and Suppliers.

All Files Created in Part 7

app/Models/Customer.php
app/Filament/Resources/Customers/CustomerResource.php
app/Filament/Resources/Customers/Schemas/CustomerForm.php
app/Filament/Resources/Customers/Tables/CustomersTable.php
app/Filament/Resources/Customers/Pages/ListCustomers.php
app/Filament/Resources/Customers/Pages/CreateCustomer.php
app/Filament/Resources/Customers/Pages/EditCustomer.php


Part 8 — Product Management (Filament 5 Resource, CRUD, File Upload, Image Column)

Video Title: Laravel Filament Image Upload | Product Module with Filament 5 File Upload | Inventory System – Part 8

In part 8, we move into a brand new sidebar group called Inventory, and build the most important module in the entire project — Product Management. This is where Laravel Filament file upload comes in: we cover the complete Filament 5 file upload process, from setting up the upload field with a built-in image editor, to displaying the uploaded photo inside the table using an Image Column. This module also introduces Select dropdowns pulling live data from Category and Unit, and price fields formatted with a currency symbol.

Step 1: Generate the Filament Resource

php artisan make:filament-resource Product --generate

This creates the following files:

app/Filament/Resources/Products/ProductResource.php
app/Filament/Resources/Products/Schemas/ProductForm.php
app/Filament/Resources/Products/Tables/ProductsTable.php
app/Filament/Resources/Products/Pages/ListProducts.php
app/Filament/Resources/Products/Pages/CreateProduct.php
app/Filament/Resources/Products/Pages/EditProduct.php

Step 2: Update the Product Model

File path: app/Models/Product.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    protected $fillable = [
        'name',
        'slug',
        'sku',
        'category_id',
        'unit_id',
        'cost_price',
        'selling_price',
        'image',
        'description',
        'status',
    ];

    protected $casts = [
        'status' => 'boolean',
        'cost_price' => 'decimal:2',
        'selling_price' => 'decimal:2',
    ];

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

    public function unit()
    {
        return $this->belongsTo(Unit::class);
    }

    public function stocks()
    {
        return $this->hasMany(Stock::class);
    }

    public function purchaseItems()
    {
        return $this->hasMany(PurchaseItem::class);
    }

    public function saleItems()
    {
        return $this->hasMany(SaleItem::class);
    }

    public function stockAdjustments()
    {
        return $this->hasMany(StockAdjustment::class);
    }
}

Step 3: ProductResource.php

File path: app/Filament/Resources/Products/ProductResource.php

<?php

namespace App\Filament\Resources\Products;

use App\Filament\Resources\Products\Pages\CreateProduct;
use App\Filament\Resources\Products\Pages\EditProduct;
use App\Filament\Resources\Products\Pages\ListProducts;
use App\Filament\Resources\Products\Schemas\ProductForm;
use App\Filament\Resources\Products\Tables\ProductsTable;
use App\Models\Product;
use BackedEnum;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Tables\Table;
use UnitEnum;

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

    protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-cube';

    protected static string|UnitEnum|null $navigationGroup = 'Inventory';

    protected static ?string $navigationLabel = 'Products';

    protected static ?int $navigationSort = 1;

    protected static ?string $recordTitleAttribute = 'name';

    public static function form(Schema $schema): Schema
    {
        return ProductForm::configure($schema);
    }

    public static function table(Table $table): Table
    {
        return ProductsTable::configure($table);
    }

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

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

Note: This is the first resource to use 'Inventory' as the navigation group instead of 'Master Data' — so $navigationSort = 1 restarts the counter, since Products is the first resource in this new group.

Step 4: ProductForm.php (with Filament File Upload)

File path: app/Filament/Resources/Products/Schemas/ProductForm.php

<?php

namespace App\Filament\Resources\Products\Schemas;

use App\Models\Category;
use App\Models\Product;
use App\Models\Unit;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Schemas\Schema;
use Illuminate\Support\Str;

class ProductForm
{
    public static function configure(Schema $schema): Schema
    {
        return $schema
            ->components([
                Section::make('Product Information')
                    ->schema([
                        TextInput::make('name')
                            ->label('Product Name')
                            ->placeholder('e.g. Wireless Mouse')
                            ->required()
                            ->maxLength(255)
                            ->live(onBlur: true)
                            ->afterStateUpdated(function (string $operation, $state, Set $set) {
                                if ($operation === 'create') {
                                    $set('slug', Str::slug($state));
                                }
                            }),

                        TextInput::make('slug')
                            ->label('Slug')
                            ->required()
                            ->maxLength(255)
                            ->unique(Product::class, 'slug', ignoreRecord: true)
                            ->helperText('Auto generated from product name'),

                        TextInput::make('sku')
                            ->label('SKU / Barcode')
                            ->placeholder('e.g. PRD-0001')
                            ->required()
                            ->maxLength(255)
                            ->unique(Product::class, 'sku', ignoreRecord: true),

                        Select::make('category_id')
                            ->label('Category')
                            ->options(Category::pluck('name', 'id'))
                            ->required()
                            ->searchable()
                            ->preload(),

                        Select::make('unit_id')
                            ->label('Unit')
                            ->options(Unit::pluck('name', 'id'))
                            ->required()
                            ->searchable()
                            ->preload(),

                        TextInput::make('cost_price')
                            ->label('Cost Price')
                            ->numeric()
                            ->prefix('₹')
                            ->default(0)
                            ->required(),

                        TextInput::make('selling_price')
                            ->label('Selling Price')
                            ->numeric()
                            ->prefix('₹')
                            ->default(0)
                            ->required(),

                        Toggle::make('status')
                            ->label('Active Status')
                            ->default(true)
                            ->onColor('success')
                            ->offColor('danger'),
                    ])
                    ->columns(2),

                Section::make('Product Image & Description')
                    ->schema([
                        FileUpload::make('image')
                            ->label('Product Image')
                            ->image()
                            ->directory('products')
                            ->imageEditor()
                            ->columnSpanFull(),

                        Textarea::make('description')
                            ->label('Description')
                            ->placeholder('Enter product description')
                            ->rows(4)
                            ->columnSpanFull(),
                    ])
                    ->columns(1),
            ]);
    }
}

How Laravel Filament file upload works here:

  • ->image() restricts the upload to image files only (JPG, PNG, etc.) and shows a live preview once uploaded
  • ->directory('products') tells Filament to save every uploaded file inside a products folder on the storage disk
  • ->imageEditor() opens a built-in cropping and editing tool right inside the form after upload — users can crop, rotate, or resize before saving
  • ->columnSpanFull() stretches the upload box across the full form width

Step 5: ProductsTable.php (with Image Column)

File path: app/Filament/Resources/Products/Tables/ProductsTable.php

<?php

namespace App\Filament\Resources\Products\Tables;

use App\Models\Category;
use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Tables\Columns\ImageColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\ToggleColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;

class ProductsTable
{
    public static function configure(Table $table): Table
    {
        return $table
            ->columns([
                ImageColumn::make('image')
                    ->label('Image')
                    ->circular()
                    ->defaultImageUrl(url('/images/no-image.png')),

                TextColumn::make('name')
                    ->label('Product Name')
                    ->searchable()
                    ->sortable(),

                TextColumn::make('sku')
                    ->label('SKU')
                    ->badge()
                    ->color('gray')
                    ->searchable(),

                TextColumn::make('category.name')
                    ->label('Category')
                    ->searchable()
                    ->sortable(),

                TextColumn::make('unit.short_code')
                    ->label('Unit')
                    ->badge()
                    ->color('info'),

                TextColumn::make('cost_price')
                    ->label('Cost Price')
                    ->money('INR')
                    ->sortable(),

                TextColumn::make('selling_price')
                    ->label('Selling Price')
                    ->money('INR')
                    ->sortable(),

                ToggleColumn::make('status')
                    ->label('Status')
                    ->onColor('success')
                    ->offColor('danger'),

                TextColumn::make('created_at')
                    ->label('Created At')
                    ->dateTime('d M Y')
                    ->sortable()
                    ->toggleable(isToggledHiddenByDefault: true),
            ])
            ->filters([
                SelectFilter::make('category_id')
                    ->label('Category')
                    ->options(Category::pluck('name', 'id'))
                    ->placeholder('All Categories'),

                SelectFilter::make('status')
                    ->label('Status')
                    ->options([
                        '1' => 'Active',
                        '0' => 'Inactive',
                    ]),
            ])
            ->actions([
                EditAction::make(),
                DeleteAction::make(),
            ])
            ->bulkActions([
                BulkActionGroup::make([
                    DeleteBulkAction::make(),
                ]),
            ])
            ->defaultSort('created_at', 'desc');
    }
}

ImageColumn is the key new component here — it renders the actual uploaded picture as a small circular thumbnail directly in the table row, with ->defaultImageUrl() providing a clean placeholder for products without an image yet. ->money('INR') automatically formats price columns with a currency symbol and comma separators (swap 'INR' for your currency code, e.g. 'USD'). Notice category.name and unit.short_code use dot notation to pull fields from related models through Eloquent relationships. Two filters (Category + Status) work together at once.

Step 6: Create, Edit, and List Pages

File path: app/Filament/Resources/Products/Pages/CreateProduct.php

<?php

namespace App\Filament\Resources\Products\Pages;

use App\Filament\Resources\Products\ProductResource;
use Filament\Resources\Pages\CreateRecord;

class CreateProduct extends CreateRecord
{
    protected static string $resource = ProductResource::class;

    protected function getRedirectUrl(): string
    {
        return $this->getResource()::getUrl('index');
    }

    protected function getCreatedNotificationTitle(): ?string
    {
        return 'Product created successfully';
    }
}

File path: app/Filament/Resources/Products/Pages/EditProduct.php

<?php

namespace App\Filament\Resources\Products\Pages;

use App\Filament\Resources\Products\ProductResource;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;

class EditProduct extends EditRecord
{
    protected static string $resource = ProductResource::class;

    protected function getHeaderActions(): array
    {
        return [
            DeleteAction::make(),
        ];
    }

    protected function getRedirectUrl(): string
    {
        return $this->getResource()::getUrl('index');
    }

    protected function getSavedNotificationTitle(): ?string
    {
        return 'Product updated successfully';
    }
}

File path: app/Filament/Resources/Products/Pages/ListProducts.php

<?php

namespace App\Filament\Resources\Products\Pages;

use App\Filament\Resources\Products\ProductResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;

class ListProducts extends ListRecords
{
    protected static string $resource = ProductResource::class;

    protected function getHeaderActions(): array
    {
        return [
            CreateAction::make()
                ->label('New Product'),
        ];
    }
}

Step 7: Enable File Upload Storage

Since we are uploading real product images, run this command so uploaded files are actually visible in the browser:

php artisan storage:link

This creates a symbolic link from public/storage to storage/app/public. Without this link, uploaded images save correctly on the server but won't display in the browser — a common Laravel Filament file upload gotcha worth remembering.

Step 8: Clear Cache and Run

php artisan optimize:clear
php artisan serve

Visit /admin/products and you'll see a brand new Inventory group in the sidebar, with Products as its first entry — complete with image thumbnails, category and unit badges, and formatted prices.

All Files Created in Part 8

app/Models/Product.php
app/Filament/Resources/Products/ProductResource.php
app/Filament/Resources/Products/Schemas/ProductForm.php
app/Filament/Resources/Products/Tables/ProductsTable.php
app/Filament/Resources/Products/Pages/ListProducts.php
app/Filament/Resources/Products/Pages/CreateProduct.php
app/Filament/Resources/Products/Pages/EditProduct.php

Database ER Diagram


Below is the entity relationship diagram showing how all 12 tables in our Inventory Management System connect to each other:

Laravel 13 + Filament 5 Inventory Management System - Complete Tutorial Series (2026)

(Insert the ER diagram image here - exported from the Part 2 video)




Watch the Full Video Tutorial

Part 1: Installation & Setup Guide - https://youtu.be/796FXMF6joY

Part 2: Database Design - https://youtu.be/5dPfGUTR9kE

Part 3: Filament Panel branding and setup (custom logo, colors, navigation groups) - https://www.youtube.com/watch?v=o7jgfRGvi9s

Part 4: Category Management (Filament Resource, CRUD, Parent/Child Subcategory) - https://www.youtube.com/watch?v=UsLIgCeDshk

Part 5: Unit Management with Filament 5 Resource – https://youtu.be/m8EkbuEMG-I

Part 6: Supplier Management with Filament 5 Resource – https://www.youtube.com/watch?v=q7FE9loMmtM

Part 7: Customer Management with Filament 5 Resource – https://youtu.be/Iy5M0yht8Fc

Part 8: Product Management with Filament File Upload – [Add Part 8 YouTube link here]




What's Coming Next in This Series


  • Part 9: Warehouse Management (final Master Data module)
  • Part 10 onward: Purchase, Sale, and Stock modules
  • Reports, dashboards, and final deployment




This blog post will be updated with new source code and explanation after every new part is published. Bookmark this page and check back regularly.







Frequently Asked Questions


Q1. Is this Inventory Management System free to download?
Yes, the complete source code for this project is free and will be shared part by part as the series progresses.

Q2. What is the minimum requirement to follow this tutorial?
You need PHP 8.3 or above, Composer, MySQL, and basic knowledge of Laravel.

Q3. Does this project support multiple warehouses?
Yes, the database is designed from the start to support multiple warehouses, with stock tracked separately for each warehouse.

Q4. Can I use this project for a real business?
Yes, once the series is complete, this project can be used as a starting point for a real inventory management application, with some customization based on your business needs.



If this tutorial helped you, don't forget to like the video, subscribe to Webslesson, and share this blog post with other developers who are learning Laravel and Filament.



Sunday, 19 July 2026

Build a Complete Doctor Appointment Booking System with Laravel 13 & Filament

In modern web development, creating efficient, scalable, and user-friendly medical applications is highly sought after. If you are looking for a comprehensive doctor appointment booking system in laravel or searching for high-quality final year php projects, this guide is exactly what you need. Building an online doctor appointment system project in laravel using the powerful Filament admin panel allows you to deliver a premium, multi-role web application in record time.

Whether you need a robust doctor appointment system in laravel with source code for your portfolio, a commercial application, or your university final year projects, this system features a complete architecture. Built using php, mysql, laravel (fully compatible up to latest versions like laravel 13), and stylized beautifully using modern UI components alongside bootstrap 5 principles, this application streamlines clinical workflows seamlessly.



Build a Complete Doctor Appointment Booking System with Laravel & Filament

Why Choose Laravel and Filament for an Online Doctor Appointment System?


Developing an online doctor appointment system project requires handling strict user roles, secure data, dynamic schedules, and financial statistics. Traditional php development can take weeks just to build the backend scaffolding.





By leveraging filament—a powerful content management and administration framework for Laravel—we bypass repetitive UI building. Combined with the reliability of a mysql database, this stack ensures your online booking doctor appointment system runs smoothly, handles automated calculations, and remains highly extendable. This makes it one of the best free projects to learn modern development practices.


Core Architecture & User Panels


Build a Complete Doctor Appointment Booking System with Laravel & Filament

This project on doctor appointment system is divided into three functional modules, ensuring a seamless user experience for everyone involved:

  • Admin Panel: The control center where administrators manage master data (departments, medicines), track system-wide revenue, analyze medical charts, approve withdrawals, and oversee accounts.
  • Doctor Panel: A dedicated dashboard where healthcare providers manage their availability schedules, conduct dynamic patient consultations, view clinical vitals, and track earnings.
  • Patient Panel: An intuitive interface for patients to search for doctors, choose dynamic time slots, securely track booking histories, and download prescriptions.



How to Download and Install the Source Code


If you are ready to set up this doctor appointment system in laravel on your local machine, follow these simple installation steps:

Step 1: Get the Source Code


Open your web browser and navigate to the webslesson.info website. Search for the "Doctor Appointment Booking System" post. Inside the article, you will find a secure Google Drive link to download the source code zip file.

Note on Password Protection: The source code zip file is password protected to ensure secure delivery. To receive your password automatically, simply drop your email address in the comment box under that specific post on webslesson.info, and the password will be sent directly to your inbox.

Step 2: Database and Environment Configuration


Once downloaded, extract the project files. Open your database management tool (such as phpMyAdmin) and create a fresh mysql database. Next, locate the .env file in the root directory of the project and update the following connection fields:

DB_DATABASE=your_database_name
DB_USERNAME=your_mysql_username
DB_PASSWORD=your_mysql_password
APP_URL=http://localhost:8000

Step 3: Database Migrations and Seeders


Open your terminal, navigate to the project directory, and run the migration command to construct your database schema and populate it with sample testing data:

php artisan migrate --seed

Once finished, launch your local server using php artisan serve and open the application URL in your web browser.



Exploring the Admin Dashboard & Analytics


Logging into the admin dashboard presents a beautiful analytical layout built using Filament's widget components. Admins instantly gain access to financial insights, platform performance metrics, and data visualizations:

  • Stat Cards: Tracks metrics like Total Revenue, Commission Earned, Admin Net Balance, Total Departments, Registered Doctors, Patients, and Pending Doctor Withdrawals.
  • Data Charts: Includes an interactive 30-day Appointment Volume line chart, Monthly Revenue graphs, and a Department Distribution pie chart for seamless administrative oversight.

Managing Core Medical Modules


  • Departments Module: Admins can easily Create, Read, Update, and Delete (CRUD) hospital departments by filling in the Name, Slug, Description, Contact Details, and physical location. This categorized data is highly crucial for matching patients with corresponding specialists.
  • Doctors Management: When adding a new doctor, the admin creates their secure login credentials, inputting personal qualifications, bios, consultation fees, profile images, and digital signature files.
  • Medicines Master Data: A dedicated repository where admins pre-populate medicine names and types. This serves as a vital master list used by doctors during patient consultations.



The Doctor Panel: Managing Schedules & Consultations


When doctors log into their authenticated portal, they can manage their medical practices autonomously.

Setting Dynamic Availability Schedules


Patients cannot book appointments blindly. Doctors use the Schedules Module to define specific operational slots. When creating a new schedule, the doctor inputs:

  • Available Date
  • Shift Start Time & End Time
  • Maximum Patient Capacity for that slot

The Patient Consultation Workflow


When a patient arrives for their appointment, the doctor clicks the Consult button to open a digital health record worksheet. Within this single Filament screen, the doctor records:

  • Patient Vitals: Real-time tracking for Blood Pressure, Pulse, and Body Temperature.
  • Clinical Notes: Spaces for Diagnosis details and explicit medical Advice.
  • Prescription Engine: A dynamic dropdown connected directly to the master medicine list where doctors select prescriptions with custom dosages.

Earning and Financial Withdrawals


Doctors earn income on a per-consultation basis. From the Finance tab, they can track net income after platform commission and request a payout by inputting their bank account credentials via the New Withdrawal system.



The Patient Journey: Booking an Online Appointment


The public website features a stunning, fast landing page highlighting featured medical departments, available specialist cards, and a newsletter subscription footer block built to follow bootstrap 5 UI benchmarks.

Quick Account Setup


New users can seamlessly register by clicking the Register option, providing their Name, Email, Phone Number, and Password. Secure recovery links are built-in via the Forgot Password function if a user loses account access.

Step-by-Step Booking Process


Once logged into the patient portal, scheduling an online doctor appointment takes less than a minute:

  1. Select the desired medical department from a dropdown list.
  2. Choose an available doctor assigned to that specific field.
  3. Pick from the live dates and time slots configured by the doctor.
  4. Review the consultation fees. The system is designed to support "Pay at Clinic" checkouts upon arrival.
  5. Confirm the appointment booking.

Viewing Digital Prescriptions


From the "My Appointments" panel, patients monitor past booking statuses. Once a session transitions to completed, a Prescription button unlocks, giving patients an optimized window to view vitals, read diagnosis advice, and print their medical orders directly from their web browsers.



Conclusion


Building an online doctor appointment system doesn't have to be overwhelming. Utilizing laravel alongside filament allows you to build safe, high-performing enterprise management platforms efficiently. This project perfectly covers database configuration using mysql, interactive web views, secure authorization logic, and custom administrative interfaces, making it one of the absolute best final year php projects for students and professional engineers alike.





Head over to webslesson.info to grab your source code bundle, deploy it locally, and launch your own functional medical platform today! If you found this step-by-step developer guide helpful, make sure to share it with fellow developers and leave your feedback in the comment section below.