Product Inventory Dashboard in PHP
Build a FilamentPHP admin panel with CRUD, stats widgets, and charts.
Introduction
30 Second Summary
Every business that sells products needs to know what's in stock, what's running low, and where the money is tied up. Spreadsheets get the job done until your catalog outgrows them.
In this project, you will build a product inventory dashboard using FilamentPHP and Laravel. You will create an admin panel where you can manage products, track low-stock items, and visualize inventory data through real-time stats and charts.
Why Are We Using Filament?
Laravel is a full stack web devleopment framework that works primarily off of the PHP programming language.
That means that it comes bootstrapped with tools for handling web applications that work with databases (e.g., YouTube, Facebook, etc.). Unlike a static site, which is pure frontend, full stack web applications include both a frontend (what you see on a website) and backend (what's managed behind the scenes, e.g., databases).
If you're familiar with full-stack web development and Laravel specifically, you know that you can build the same dashboard using just Laravel (or just PHP, if you're feeling adventurous).
However, in real-life web development situations, you are often met with tight deadlines and demanding clients, so if we're looking to build a dashboard for a client who wants something for internal use and has PHP in his or her tech stack, think of Filament as a convenient plugin to speed things up for that specific job.
(Sidenote: Filament is declarative, which means that a lot of the frontend components are built out for you and you just need to call them from your backend. This generally leaves you enough mental energy to focus on the backend and core business logic. Though, depending on the requirements, you may need to pop in occassionally to tweak some frontend components.)
What You'll Build
Picture signing into your admin panel and instantly seeing how many products are running low, what your total inventory is worth, and how your catalog splits across categories.
Your admin panel runs off your main site, so while you have an ecommerce shop at buymystuff.com, you access buymystuff.com/admin to manage all the stuff your customers see (and don't see).
By the end of this project, you'll have:
- A product catalog with full CRUD: create products with validated fields like name, SKU, category, and price, then browse them in a sortable, searchable table with currency-formatted prices.
- A stats overview widget on your dashboard that pulls live data to display total product count, low-stock alerts, and the dollar value of your entire inventory.
- A bar chart widget that groups products by category so you can spot inventory imbalances at a glance.
- An optional Secret Mission to push your skills further.
Are there any prerequisites?
This project assumes you are comfortable with Laravel and PHP. You should have PHP 8.2 or higher, Composer, Node.js/npm, and VS Code installed before you start.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters.
Set Up Your Laravel Project and Install Filament
You have a clear picture of what you are building. Now you need a foundation to build it on.
Every Filament admin panel runs inside a Laravel application. In this step, you will create a fresh Laravel project, install Filament's Panel Builder, and create your first admin user so you can sign in and see the default dashboard.
In this step, get ready to:
- Create a new Laravel project with SQLite as the default database.
- Install the Filament v5 Panel Builder.
- Create an admin user and sign into the Filament dashboard.
Create a new Laravel project
Laravel uses SQLite as its default database. That means you get a working database with zero configuration.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal and press Enter to open it.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create a new Laravel project called inventory-dashboard by running this command:
composer create-project laravel/laravel inventory-dashboard
What does this command do?
Composer downloads the latest Laravel framework and all its dependencies into a new inventory-dashboard folder. It also generates a .env configuration file, creates a database/database.sqlite file, and runs the initial database migrations automatically.
No database server needed. SQLite stores everything in a single file inside your project.
- Move into your new project folder by running:
cd inventory-dashboard
- Start the Laravel development server by running:
php artisan serve
What does php artisan serve do?
This command starts a local web server on port 8000. It keeps the terminal busy while the server runs, so you will need to open a new terminal tab for future commands.
- Open your browser and visit http://localhost:8000.
You should see the default Laravel welcome page. This confirms your project is running.
Page not loading?
- Make sure the php artisan serve command is still running in your terminal. If you closed it, run it again from inside the inventory-dashboard folder.
- Check that nothing else is using port 8000. If another app is using it, stop that app first.
- help me debug why localhost:8000 is not loading my Laravel project
Install the Filament Panel Builder
Your Laravel app is running, but it has no admin panel yet. Filament's Panel Builder adds a complete admin interface that you will use to manage products and view dashboard widgets.
- Open a new terminal tab or window. Your dev server is still running in the first terminal. You need a fresh terminal for the next commands.
- Navigate to your project folder by running:
cd ~/Desktop/inventory-dashboard
- Install the Filament package by running:
composer require filament/filament:"^5.0"
Using Windows PowerShell?
PowerShell ignores the ^ character in version constraints. Use this command instead:
composer require filament/filament:"~5.0"
Composer will download Filament and all its dependencies. This may take a minute.
- Run the Filament panel installation command:
php artisan filament:install --panels
If prompted with these questions, answer with the following:
- What is the panel's ID?
- All done! Would you like to show some love starring the Filament repo on GitHub?
What does this command do?
This command creates a new file at app/Providers/Filament/AdminPanelProvider.php. This file is the configuration hub for your admin panel. It controls the panel's URL path, authentication, and which resources and widgets are available.
It also registers the provider automatically so Laravel knows about the panel.
Create an admin user and sign in
Filament protects the admin panel behind a login screen. You need a user account to access it.
- Create your admin user by running:
php artisan make:filament-user
What will this command ask?
The command will prompt you for three things, one at a time:
- Name - enter any name (e.g. Admin).
- Email - enter any email address (e.g. admin@example.com).
- Password - choose a password you will remember. This is a local dev environment, so it does not need to be complex (e.g., password).
Remember these credentials. You will use them to sign in next.
- Answer each prompt in your terminal to create the user.
You should see a success message confirming the user was created.
- Open your browser and visit http://localhost:8000/admin.
Before you sign in, what do you think you will see on the dashboard? Filament ships with some default widgets out of the box.
- Enter the email and password you just created to sign in.
You should see the default Filament dashboard with a welcome widget. This is your admin panel, ready to be customized with product resources and inventory widgets.
Seeing a login error?
- Double-check the email and password match exactly what you entered during php artisan make:filament-user. If you forgot them, run the command again to create a new user.
- If you see a 404 error at /admin, make sure php artisan filament:install --panels completed without errors. Check that the file app/Providers/Filament/AdminPanelProvider.php exists in your project.
- help me fix my Filament admin login
Your Laravel project is up and running with Filament's admin panel ready to go. Next up, you will create a Product model and fill the database with sample data so Filament has something to display.
Create the Product Model with Sample Data
Your Filament admin panel is live and the default dashboard loads at /admin. The panel is ready to manage data, but there is no data to manage yet.
In this step, you will create a Product model backed by a database table, then seed it with 50 realistic sample products. By the end, Filament will have a populated database to work with when you build the product resource in the next step.
In this step, get ready to:
- Generate a Product model, migration, and factory with a single Artisan command.
- Define the database schema and populate the factory with realistic product data.
- Seed the database with 50 products and verify the data using Tinker.
Generate the model, migration, and factory
Laravel's Artisan CLI can scaffold multiple files in one command. The -mf flags tell it to create a migration and a factory alongside the model.
- Your dev server is running in an existing terminal, so open a new terminal tab or window. You can do this with Cmd+T (macOS) or Ctrl+Shift+T (Windows) in most terminals.
- Navigate to your project folder by running:
cd ~/Desktop/inventory-dashboard
- Generate the Product model, migration, and factory by running:
php artisan make:model Product -mf
Artisan confirms three files were created. You should see output listing app/Models/Product.php, a migration file, and database/factories/ProductFactory.php.
What does -mf do?
The -m flag generates a database migration file that defines the table structure. The -f flag generates a factory file for creating test data. Without these flags, you would need to run separate commands for each file.
🙋♀️ Command not found?
Make sure you are inside your Laravel project folder. The php artisan command only works from the root of a Laravel project where the artisan file exists.
Help me debug this issue
Define the database schema and factory
The generated files contain placeholder content. You need to define what columns the products table has, tell the model which fields are fillable, and teach the factory how to generate realistic product data.
- In VS Code, open the database/migrations folder. Find the file ending in _create_products_table.php. The timestamp at the start of the filename will be different from what you see below. That is normal.
- Replace the entire file contents with the following migration schema:
<?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) {
// Auto-incrementing primary key
$table->id();
$table->string('name');
// SKU must be unique across all products
$table->string('sku')->unique();
$table->string('category');
// Price stored as decimal with 2 decimal places
$table->decimal('price', 10, 2);
$table->integer('quantity')->default(0);
$table->string('status')->default('in_stock');
$table->text('description')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('products');
}
};
What does this schema define?
Each column maps to a product attribute. The sku column has a unique() constraint so no two products can share the same SKU. The price column uses decimal(10, 2) to store precise dollar amounts.
The status column defaults to in_stock and description is nullable() because not every product needs a description.
- Save the migration file.
- Now open app/Models/Product.php and replace the entire file with the following:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
use HasFactory;
// Fields that can be mass-assigned
protected $fillable = [
'name',
'sku',
'category',
'price',
'quantity',
'status',
'description',
];
// Cast price to a decimal with 2 places
protected $casts = [
'price' => 'decimal:2',
];
}
What do fillable and casts do?
The $fillable array lists which fields can be set when creating or updating a product through Eloquent. Without it, Laravel blocks mass assignment as a security measure.
The $casts array tells Eloquent to automatically format price as a decimal with two places whenever you read it from the database.
- Save Product.php.
You have two files edited. Before writing any more code, let's run the migration to confirm the table gets created.
- Run the migration by running this command:
php artisan migrate
You should see output confirming the products table was created. Look for a line that includes create_products_table.
Seeing a syntax error?
Double check that your migration file has no missing commas or semicolons. The most common issue is a typo in the column definition calls. Compare your file against the code block above line by line.
Help me debug my migration error
Now let's define how the factory generates random product data.
- Open database/factories/ProductFactory.php and replace the entire file with the following:
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
class ProductFactory extends Factory
{
public function definition(): array
{
// Five product categories for variety
$categories = ['Electronics', 'Clothing', 'Food & Beverage', 'Office Supplies', 'Furniture'];
$statuses = ['in_stock', 'low_stock', 'out_of_stock'];
return [
'name' => fake()->words(3, true),
// Unique SKU like "ABC-1234"
'sku' => strtoupper(fake()->unique()->bothify('???-####')),
'category' => fake()->randomElement($categories),
'price' => fake()->randomFloat(2, 5, 500),
'quantity' => fake()->numberBetween(0, 200),
'status' => fake()->randomElement($statuses),
'description' => fake()->sentence(),
];
}
}
What does the factory generate?
Each call to fake() generates random but realistic data. The bothify('???-####') method creates SKUs like ABC-1234 by replacing ? with letters and # with digits.
Products are randomly assigned one of five categories and one of three statuses. Prices range from $5 to $500 and quantities from 0 to 200.
- Save ProductFactory.php.
- Verify the factory works by opening Tinker:
php artisan tinker
- Inside the Tinker prompt, create a sample product without saving it to the database:
App\Models\Product::factory()->make();
You should see a Product object with randomly generated values for name, SKU, category, price, quantity, status, and description.
- Exit Tinker by typing exit and pressing Enter.
Seed the database and verify
The factory knows how to generate one product. The seeder tells Laravel to use it 50 times and save every product to the database.
- Open database/seeders/DatabaseSeeder.php and replace the entire file with the following:
<?php
namespace Database\Seeders;
use App\Models\Product;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
public function run(): void
{
// Create 50 products using the factory
Product::factory(50)->create();
}
}
What does factory(50)->create() do?
The factory(50) method tells Laravel to run the ProductFactory 50 times. The create() method saves each generated product to the database. If you used make() instead, it would build objects in memory without saving them.
- Save DatabaseSeeder.php.
- Seed the database by running:
php artisan db:seed
You should see a confirmation message indicating the seeder ran successfully.
Before you check the count, what number do you think Product::count() will return?
- Verify the data by opening Tinker and checking the product count:
php artisan tinker
App\Models\Product::count();
You should see 50 returned. That confirms all 50 products were seeded successfully.
- Exit Tinker by typing exit and pressing Enter.
Seeing 0 products?
If Product::count() returns 0, the seeder did not run. Make sure you saved DatabaseSeeder.php before running php artisan db:seed. Check that your seeder imports App\Models\Product at the top of the file.
Help me figure out why no products were created
✔️ Awesome, I've got everything!
Great. Double check you have saved all four files before moving on.
ⓧ I'd like to double check the full code
Here is the complete code for each file you edited in this step. Compare yours against these.
<?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->string('name');
$table->string('sku')->unique();
$table->string('category');
$table->decimal('price', 10, 2);
$table->integer('quantity')->default(0);
$table->string('status')->default('in_stock');
$table->text('description')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('products');
}
};
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
use HasFactory;
protected $fillable = [
'name',
'sku',
'category',
'price',
'quantity',
'status',
'description',
];
protected $casts = [
'price' => 'decimal:2',
];
}
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
class ProductFactory extends Factory
{
public function definition(): array
{
$categories = ['Electronics', 'Clothing', 'Food & Beverage', 'Office Supplies', 'Furniture'];
$statuses = ['in_stock', 'low_stock', 'out_of_stock'];
return [
'name' => fake()->words(3, true),
'sku' => strtoupper(fake()->unique()->bothify('???-####')),
'category' => fake()->randomElement($categories),
'price' => fake()->randomFloat(2, 5, 500),
'quantity' => fake()->numberBetween(0, 200),
'status' => fake()->randomElement($statuses),
'description' => fake()->sentence(),
];
}
}
<?php
namespace Database\Seeders;
use App\Models\Product;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
public function run(): void
{
Product::factory(50)->create();
}
}
Your database now has 50 products with varied categories, prices, and stock levels. Next up, you will build a Filament resource that turns this data into a fully interactive admin interface with a form and a formatted table.
Build the Product Resource with Form and Table
You have 50 products sitting in your database, but there is no way to see or manage them from the admin panel. If you wanted to add a new product or update an existing one, you would have to write raw SQL or use tinker every time.
Filament solves this with resources. A resource is a complete CRUD interface for an Eloquent model. It gives you a form for creating and editing records, a table for browsing them, and page routing to tie it all together.
In this step, get ready to:
- Generate the Product resource scaffolding with Artisan.
- Build a form with validated fields for creating and editing products.
- Set up a formatted, searchable table to browse your inventory.
Generate the Product resource
Filament provides an Artisan command that scaffolds everything a resource needs: a resource class, page classes for listing, creating, and editing records, a form schema file, and a table file.
- Generate the Product resource by running this command in your terminal:
php artisan make:filament-resource Product
If it asks the following questions:
- What is the title attribute for this model?
- Would you like to generate a read-only view page for the resource?
- Should the configuration be generated from the current database columns?
What did this command create?
This created a Products directory inside app/Filament/Resources/ with six files:
- ProductResource.php registers the resource with Filament.
- Pages/ListProducts.php, Pages/CreateProduct.php, and Pages/EditProduct.php handle the three CRUD views.
- Schemas/ProductForm.php defines the form fields you will customize next.
- Tables/ProductsTable.php defines the table columns you will customize after that.
- Return to your browser and refresh the admin panel at http://localhost:8000/admin.
- Look at the sidebar on the left. You should see a new Products navigation item.
- Click Products. You land on the products list page with your 50 seeded records shown in a default table.
Don't see Products in the sidebar?
- Make sure the dev server is still running. If it stopped, start it again with php artisan serve.
- Double-check that the command created the app/Filament/Resources/Products/ directory. If it is missing, re-run the command.
- help me debug why my Product resource isn't showing in the Filament sidebar
Customize the product form
The generated form file has placeholder content. You need to replace it with fields that match your Product model: name, SKU, category, price, quantity, status, and description.
- Open app/Filament/Resources/Products/Schemas/ProductForm.php in VS Code.
- Replace the entire file contents with this code to add the name and SKU fields:
<?php
namespace App\Filament\Resources\Products\Schemas;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Schema;
class ProductForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->components([
// Product name - required with a max length to match the DB column
TextInput::make('name')
->required()
->maxLength(255),
// SKU must be unique so two products never share the same code
TextInput::make('sku')
->label('SKU')
->required()
->unique(ignoreRecord: true)
->maxLength(255),
]);
}
}
What does this code do?
- The configure() method receives a Schema object and returns it with your form components attached.
- TextInput::make('name') creates a text field bound to the name column. ->required() makes it mandatory.
- unique(ignoreRecord: true) on the SKU field prevents duplicate SKUs but allows the current record to keep its own value when editing.
- Save ProductForm.php.
- In the admin panel, click New product in the top right of the products page.
- You should see a form with two fields: Name and SKU.
Now add the remaining five fields. These cover category, price, quantity, status, and a description textarea.
- In ProductForm.php, find the closing ]); on the line after the SKU field's ->maxLength(255), and replace it with ]); so you can add more components before it. Copy this code and paste it between the last SKU line and the ]);:
// Dropdown limited to the five categories from the factory
Select::make('category')
->required()
->options([
'Electronics' => 'Electronics',
'Clothing' => 'Clothing',
'Food & Beverage' => 'Food & Beverage',
'Office Supplies' => 'Office Supplies',
'Furniture' => 'Furniture',
]),
// Dollar prefix displays "$" inside the input field
TextInput::make('price')
->required()
->numeric()
->prefix('$'),
TextInput::make('quantity')
->required()
->numeric()
->default(0),
// Defaults to "in_stock" for new products
Select::make('status')
->required()
->options([
'in_stock' => 'In Stock',
'low_stock' => 'Low Stock',
'out_of_stock' => 'Out of Stock',
])
->default('in_stock'),
// Full-width textarea for longer descriptions
Textarea::make('description')
->columnSpanFull(),
What do these fields do?
- Select::make('category') creates a dropdown with the same five categories your factory uses.
- ->numeric() on the price field restricts input to numbers. ->prefix('$') adds a dollar sign inside the input so users know the unit.
- ->default('in_stock') pre-selects a sensible status when creating a new product.
- ->columnSpanFull() makes the description textarea stretch across the full width of the form.
- Save ProductForm.php.
- Refresh the New product page in your browser.
- You should now see all seven fields: Name, SKU, Category, Price, Quantity, Status, and Description.
Seeing an error or missing fields?
- Make sure the new code is inside the ->components([...]) array, not outside it. Every component should be separated by a comma.
- Check that all three use statements at the top of the file are present: Select, Textarea, and TextInput.
- help me fix my ProductForm.php
✔️ Awesome, I've got everything!
Great. Double-check you have saved ProductForm.php before moving on.
ⓧ I'd like to double check the full code
Scroll up and confirm your ProductForm.php contains both chunks in order:
- Chunk 1: The file skeleton with the name and sku TextInputs.
- Chunk 2: The category Select through to the description Textarea, all inside the ->components([...]) array.
The file should have seven components total inside the components() call. Make sure the closing ]); comes after the description Textarea.
Customize the products table
The form handles creating and editing products. Now you need a table that displays them in a browsable, searchable format with properly formatted values.
- Open app/Filament/Resources/Products/Tables/ProductsTable.php in VS Code.
- Replace the entire file contents with this code to define the table columns:
<?php
namespace App\Filament\Resources\Products\Tables;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class ProductsTable
{
public static function configure(Table $table): Table
{
return $table
->columns([
TextColumn::make('name')
->searchable(),
TextColumn::make('sku')
->label('SKU') // Displays "SKU" instead of "Sku"
->searchable(),
TextColumn::make('category'),
TextColumn::make('price')
->money('USD') // Formats as $XX.XX
->sortable(),
]);
What do these columns do?
- ->searchable() on name and sku adds a search box to the top of the table that filters across both fields.
- ->money('USD') formats the raw decimal price as $123.45 automatically.
- ->sortable() lets users click a column header to sort the table by that value.
The file is not valid PHP yet. You still need to add the remaining columns and the row actions.
- Find the closing ]); at the bottom of the file. Replace everything from that line to the end of the file with this code:
TextColumn::make('quantity')
->numeric()
->sortable(),
TextColumn::make('status'),
])
// Edit button on each table row
->recordActions([
EditAction::make(),
])
// Bulk delete checkbox action in the toolbar
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
]);
}
}
What do the actions do?
- ->recordActions() adds an edit button on each row so users can click through to the edit form.
- ->toolbarActions() adds a bulk action toolbar. DeleteBulkAction lets users select multiple rows and delete them at once.
- Save ProductsTable.php.
- In the admin panel, click Products in the sidebar to return to the product list.
You should see all 50 products in a formatted table. The Price column displays values like $123.45 instead of raw decimals. Each row has an edit pencil icon on the right.
Table not rendering or showing errors?
- Check the use statements at the top. Actions like EditAction and DeleteBulkAction come from Filament\Actions, not Filament\Tables\Actions. This is a Filament v5 change.
- Make sure the ->columns() array closes with ]) before ->recordActions() starts. A missing bracket breaks the chain.
- help me fix my ProductsTable.php
✔️ Awesome, I've got everything!
Great. Double-check you have saved ProductsTable.php before moving on.
ⓧ I'd like to double check the full code
Scroll up and confirm your ProductsTable.php contains both chunks in order:
- Chunk 1: The file skeleton with columns for name, sku, category, and price.
- Chunk 2: The quantity and status columns, plus ->recordActions() and ->toolbarActions() with the closing braces.
You should have six columns total inside ->columns(), followed by ->recordActions() and ->toolbarActions() chained after.
Time for the final test. Before you try this, what do you think will happen when you fill out the create form and hit save?
- Click New product in the top right of the products page.
- Fill in each field with test data. For example: Name Test Widget, SKU TST-0001, Category Electronics, Price 29.99, Quantity 100.
- Leave Status as In Stock (the default) and add a short Description.
- Click Create.
Filament redirects you to the edit page for the new record. Click Products in the sidebar to return to the list. Your new product appears in the table with its price formatted as currency.
Your product resource is fully operational. Next up, you will build dashboard widgets that surface aggregate metrics from this data so you can monitor inventory health at a glance.
Build Stats Overview Widgets for the Dashboard
Your Product resource gives you full CRUD over individual products. But the table does not surface aggregate metrics like total stock value or how many items are running dangerously low.
In this step, you will build a stats overview widget that queries your database with Eloquent and displays total products, low-stock alerts, and total inventory value directly on the Filament dashboard.
In this step, get ready to:
- Generate a stats overview widget using Filament's artisan command.
- Wire the widget to live Eloquent queries that display product count, low stock count, and total inventory value.
Generate the stats overview widget
Filament provides an artisan command that scaffolds a stats overview widget with placeholder stat cards. You will generate the widget file and then replace its contents with real data queries.
- Generate the widget by running this command in your terminal:
php artisan make:filament-widget StatsOverview --stats-overview
For the questions, select the defaults:
- Would you like to create this widget in a panel? Yes
- Would you like to create this widget in a resource? No
What does this command do?
The --stats-overview flag tells Filament to generate a widget that extends StatsOverviewWidget. This base class renders stat cards in a horizontal row on your dashboard.
Filament auto-discovers any widget placed in the app/Filament/Widgets directory. No manual registration is needed.
- Confirm the file was created by running:
ls app/Filament/Widgets/StatsOverview.php
You should see app/Filament/Widgets/StatsOverview.php in the output.
Don't see the file?
Make sure you ran the command from the root of your Laravel project (the folder containing artisan).
help me figure out why the make:filament-widget command did not create my StatsOverview file
Add Eloquent-powered stats to the widget
The generated file has placeholder stats. You will replace the entire file with three real stats that query your Product model for live inventory data.
- Open app/Filament/Widgets/StatsOverview.php in VS Code.
- Select all the existing code and replace it with the following:
<?php
namespace App\Filament\Widgets;
use App\Models\Product;
use Filament\Widgets\StatsOverviewWidget as BaseWidget;
use Filament\Widgets\StatsOverviewWidget\Stat;
class StatsOverview extends BaseWidget
{
protected function getStats(): array
{
// Calculate total value: price x quantity for each product
$totalValue = Product::all()->sum(fn ($product) => $product->price * $product->quantity);
return [
// Count every product in the database
Stat::make('Total Products', Product::count())
->description('All products in inventory')
->descriptionIcon('heroicon-m-cube')
->color('primary'),
// Count products with fewer than 10 units remaining
Stat::make('Low Stock Items', Product::where('quantity', '<', 10)->count())
->description('Below 10 units')
->descriptionIcon('heroicon-m-arrow-trending-down')
->color('danger'),
// Format the summed inventory value as a dollar string
Stat::make('Total Inventory Value', '$' . number_format($totalValue, 2))
->description('Sum of price x quantity')
->descriptionIcon('heroicon-m-currency-dollar')
->color('success'),
];
}
}
What does this code do?
- $totalValue loads every product, multiplies each product's price by its quantity, and sums the results. This gives you the dollar value of your entire inventory.
- The first Stat calls Product::count() to display how many products exist in the database.
- The second Stat uses a where clause to count only products with fewer than 10 units. This is your low-stock alert.
- The third Stat formats $totalValue as a dollar string with number_format() so the dashboard shows a readable currency figure.
- Each stat gets a description, descriptionIcon, and color to make the cards visually distinct on the dashboard.
✔️ Awesome, I've got everything!
Great. Double check you have saved StatsOverview.php before moving on.
ⓧ I'd like to double check the full code
Here is the complete StatsOverview.php file for comparison. Make sure your version matches, then save.
<?php
namespace App\Filament\Widgets;
use App\Models\Product;
use Filament\Widgets\StatsOverviewWidget as BaseWidget;
use Filament\Widgets\StatsOverviewWidget\Stat;
class StatsOverview extends BaseWidget
{
protected function getStats(): array
{
$totalValue = Product::all()->sum(fn ($product) => $product->price * $product->quantity);
return [
Stat::make('Total Products', Product::count())
->description('All products in inventory')
->descriptionIcon('heroicon-m-cube')
->color('primary'),
Stat::make('Low Stock Items', Product::where('quantity', '<', 10)->count())
->description('Below 10 units')
->descriptionIcon('heroicon-m-arrow-trending-down')
->color('danger'),
Stat::make('Total Inventory Value', '$' . number_format($totalValue, 2))
->description('Sum of price x quantity')
->descriptionIcon('heroicon-m-currency-dollar')
->color('success'),
];
}
}
Before you check the dashboard, what do you expect to see? Will the new stats cards appear alongside the default Filament widgets, or will they replace them?
- Save StatsOverview.php.
- Return to your browser and visit http://localhost:8000/admin.
The dashboard now shows three stat cards alongside the default Filament welcome widgets. You should see Total Products showing your product count, Low Stock Items counting products with fewer than 10 units, and Total Inventory Value displaying a dollar figure.
Why do my numbers look different from someone else's?
Your factory generates random data, so the exact counts and dollar values will differ every time you seed the database. The important thing is that all three cards appear and display non-zero values.
🙋♀️ Don't see the stats cards?
- Make sure you saved StatsOverview.php. Unsaved changes will not be picked up by the server.
- Check that the file is located at app/Filament/Widgets/StatsOverview.php. Filament only auto-discovers widgets in this exact directory.
- Verify the namespace in your file reads App\Filament\Widgets. A mismatched namespace prevents Filament from loading the class.
Help me figure out why my StatsOverview widget is not appearing on the Filament dashboard
Your dashboard now surfaces key inventory metrics at a glance. Next up, you will add a bar chart widget that visualizes how your products are distributed across categories.
Build a Chart Widget for Product Categories
In the previous step, you built a stats overview widget that surfaces total product count, low stock alerts, and total inventory value. You can now see the big picture at a glance.
Totals tell you how much inventory you have, but not how it is distributed across your five categories. Are most of your products in Electronics, or is Furniture dominating? A chart widget visualizes this distribution so you can spot imbalances instantly.
In this step, get ready to:
- Create a chart widget using Filament's artisan command.
- Write an Eloquent query that groups products by category and returns Chart.js-formatted data as a bar chart.
Generate the chart widget
Filament provides a dedicated artisan command that scaffolds a chart widget class with the ChartWidget base class already wired up.
- Create the chart widget by running this command in your terminal:
php artisan make:filament-widget ProductsByCategoryChart --chart
For the following questions, answer the following:
- Would you like to create this widget in a panel? Yes
- Would you like to create this widget in a resource? No
- Which type of chart would you like to create? Bar chart
- Confirm the file was created by running:
ls app/Filament/Widgets/
You should see ProductsByCategoryChart.php alongside StatsOverview.php.
Don't see the file?
Make sure you ran the command from your project's root directory (the folder containing artisan). Check that you typed --chart at the end of the command.
Help me troubleshoot why my Filament widget file was not created.
Set up the chart widget class
The scaffold includes placeholder methods. You will replace the entire file with a class that sets the chart heading, chart type, and an empty data method for now.
- Open app/Filament/Widgets/ProductsByCategoryChart.php in VS Code.
- Replace the entire file contents with this code:
<?php
namespace App\Filament\Widgets;
use App\Models\Product;
use Filament\Widgets\ChartWidget;
class ProductsByCategoryChart extends ChartWidget
{
// Heading displayed above the chart
protected ?string $heading = 'Products by Category';
protected function getData(): array
{
return [];
}
// Set the chart type to a bar chart
protected function getType(): string
{
return 'bar';
}
}
What does this code do?
- The class extends ChartWidget, which is Filament's base class for rendering charts powered by Chart.js.
- The $heading property sets the title displayed above the chart on the dashboard.
- The getType() method tells Filament to render a bar chart. Other options include line, pie, and doughnut.
- The getData() method returns an empty array for now. Filament shows an empty state when there is no data. You will fill this in next.
- Save ProductsByCategoryChart.php.
- Refresh http://localhost:8000/admin in your browser.
Below the stats cards, you should see a new "Products by Category" widget showing an empty state. The widget is registered and rendering, but it has no data yet.
Query products by category for the chart data
The empty state proves the widget is wired up. Now you need an Eloquent query that counts how many products belong to each category and returns the result in Chart.js format.
- In ProductsByCategoryChart.php, find the getData() method that currently returns an empty array:
protected function getData(): array
{
return [];
}
- Replace that entire method with this version:
protected function getData(): array
{
// Group products by category and count each
$data = Product::query()
->selectRaw('category, count(*) as count')
->groupBy('category')
->pluck('count', 'category');
// Return Chart.js-formatted data with custom bar colors
return [
'datasets' => [
[
'label' => 'Products',
'data' => $data->values()->toArray(),
'backgroundColor' => [
'#6366f1',
'#ec4899',
'#f59e0b',
'#10b981',
'#3b82f6',
],
],
],
'labels' => $data->keys()->toArray(),
];
}
What does this query do?
The selectRaw('category, count(*) as count') groups all 50 products by their category column and counts how many fall into each group. The pluck('count', 'category') returns a collection keyed by category name with the count as the value.
The return array follows the Chart.js data structure. The datasets array holds one dataset with the product counts and five hex color codes for the bars. The labels array holds the category names pulled from the collection's keys.
- Save ProductsByCategoryChart.php.
Before you refresh, what do you think the chart will look like? Will all five categories have the same number of products, or will some be taller than others?
- Refresh http://localhost:8000/admin in your browser.
Below the stats cards, a bar chart now displays your five product categories (Electronics, Clothing, Food & Beverage, Office Supplies, Furniture) with their respective product counts. The bars will be different heights because the seeder assigned categories randomly.
Chart still showing the empty state?
Make sure you replaced the entire getData() method, not just the return []; line. The method should now contain the $data query and the full return array.
Check that you saved the file. If the chart heading appears but there are no bars, confirm your database has products by running php artisan tinker and then App\Models\Product::count().
Help me debug why my Filament chart widget is not displaying data.
✔️ Awesome, I've got everything!
Great. Double check you have saved ProductsByCategoryChart.php.
ⓧ I'd like to double check the full code
Your ProductsByCategoryChart.php file was built in two chunks. Scroll up and confirm both are present, in this order:
- The class shell chunk: the <?php opening, namespace, imports for Product and ChartWidget, the class declaration, $heading property, and getType() returning 'bar'.
- The data query chunk: getData() with the selectRaw and groupBy query, the Chart.js return array with five backgroundColor hex codes, and labels from the collection's keys.
Secret mission
Add Table Filters and Color-Coded Status Badges
Your products table works, but every status looks identical and there is no way to narrow down the list. Can you add color-coded badges, low-stock highlighting, and dropdown filters to turn your table into a real inventory triage tool?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally with SQLite, so there are no ongoing cloud costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- The Laravel project folder on your Desktop (contains all application code, the SQLite database, and installed Composer packages).
- The PHP development server (the php artisan serve process running in your terminal).
Keep everything running
No action needed. Choose this if you are still actively building or want to keep experimenting with your Filament panel.
- Your project folder stays on your Desktop with all code, migrations, and the SQLite database intact.
- If you stopped the dev server, restart it by opening your terminal, navigating to your project folder, and running php artisan serve.
Pause - I'll come back to this later
Shut down the running server to free up your terminal. Your project files remain on your Desktop so you can pick up where you left off.
- Switch to the terminal where php artisan serve is running.
- Press Ctrl+C to stop the server.
- When you are ready to resume, open your terminal, navigate back to your project folder, and run php artisan serve to start the server again.
Delete - I don't want to use this again
Remove all project files and start fresh. This deletes your code, the SQLite database, and all seeded product data.
- Switch to the terminal where php artisan serve is running.
- Press Ctrl+C to stop the server.
- Delete your Laravel project folder from the Desktop. On macOS or Linux, run the following command (replace the folder name with yours):
cd ~/Desktop
rm -rf [[PROJECT_FOLDER="your Laravel project folder name"]]
On Windows, open File Explorer, navigate to your Desktop, right-click your project folder, and select Delete.
- Confirm the folder is gone by running:
ls ~/Desktop
Your project folder should no longer appear in the output.
Nice Work!
Nice Work!
You just built a production-ready inventory dashboard powered by FilamentPHP and Laravel. From an empty project to a fully interactive admin panel with live stats, charts, and full CRUD, you now have a real tool for managing product inventory.
You've learned how to:
- Built a full CRUD admin interface for products with validated forms, searchable columns, and currency-formatted prices using Filament v5's Panel Builder and Resource system.
- Created stats overview widgets powered by Eloquent queries that display total product count, low stock alerts, and total inventory value in real time.
- Built a bar chart widget that groups products by category and renders the distribution using Chart.js data, giving you a visual snapshot of your inventory.
- Secret Mission: Added SelectFilter dropdowns for status and category, color-coded status badges, and red quantity highlighting for low-stock items.
Ready to quiz yourself?