Creating Sales Management System in Laravel with Filament

In this tutorial, we will build a Sales Management System in Laravel, which includes Categories, Products, Suppliers, Customers, and Sales modules. We will use Filament for the admin interface. Follow this step-by-step guide to implement the features.
Step 1: Setting Up Category and Product Seeders
Create Category Seeder
Run the command:
php artisan make:seeder CategorySeeder
Edit the CategorySeeder
file:
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
class CategorySeeder extends Seeder
{
public function run(): void
{
DB::table('categories')->insert([
['code' => '001', 'name' => 'Elektronik'],
['code' => '002', 'name' => 'Fashion'],
['code' => '003', 'name' => 'Makanan'],
['code' => '004', 'name' => 'Kesehatan'],
['code' => '005', 'name' => 'Otomotif'],
['code' => '006', 'name' => 'Perabot'],
['code' => '007', 'name' => 'Perawatan Pribadi'],
['code' => '008', 'name' => 'Mainan'],
]);
}
}
Create Product Seeder
Run the command:
php artisan make:seeder ProductSeeder
Edit the ProductSeeder
file:
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
class ProductSeeder extends Seeder
{
public function run(): void
{
DB::table('products')->insert([
[
'code' => 'P001',
'name' => 'Smartphone',
'quantity' => 100,
'quantity_alert' => 10,
'unit' => 'pcs',
'cost' => 3000000,
'price' => 4000000,
'tax' => 11,
'tax_type' => 1,
'note' => 'Smartphone terbaru',
'category_id' => 1, // Elektronik
],
[
'code' => 'P002',
'name' => 'Kaos Polos',
'quantity' => 200,
'quantity_alert' => 20,
'unit' => 'pcs',
'cost' => 50000,
'price' => 100000,
'tax' => 11,
'tax_type' => 1,
'note' => 'Kaos polos untuk pria',
'category_id' => 2, // Fashion
],
[
'code' => 'P003',
'name' => 'Snack Keripik',
'quantity' => 500,
'quantity_alert' => 50,
'unit' => 'pack',
'cost' => 2000,
'price' => 5000,
'tax' => 11,
'tax_type' => null,
'note' => 'Keripik rasa pedas',
'category_id' => 3, // Makanan
],
[
'code' => 'P004',
'name' => 'Vitamin C',
'quantity' => 150,
'quantity_alert' => 15,
'unit' => 'box',
'cost' => 25000,
'price' => 40000,
'tax' => 111,
'tax_type' => 1,
'note' => 'Vitamin C untuk daya tahan tubuh',
'category_id' => 4, // Kesehatan
],
[
'code' => 'P005',
'name' => 'Oli Mesin',
'quantity' => 80,
'quantity_alert' => 5,
'unit' => 'liter',
'cost' => 50000,
'price' => 75000,
'tax' => 111,
'tax_type' => 1,
'note' => 'Oli mesin berkualitas',
'category_id' => 5, // Otomotif
],
[
'code' => 'P006',
'name' => 'Set Meja Makan',
'quantity' => 30,
'quantity_alert' => 5,
'unit' => 'set',
'cost' => 1500000,
'price' => 2000000,
'tax' => 111,
'tax_type' => 1,
'note' => 'Meja makan dari kayu jati',
'category_id' => 6, // Perabot
],
[
'code' => 'P007',
'name' => 'Shampoo',
'quantity' => 100,
'quantity_alert' => 10,
'unit' => 'botol',
'cost' => 30000,
'price' => 50000,
'tax' => 11,
'tax_type' => 1,
'note' => 'Shampoo untuk rambut sehat',
'category_id' => 7, // Perawatan Pribadi
],
[
'code' => 'P008',
'name' => 'Mainan Anak',
'quantity' => 200,
'quantity_alert' => 20,
'unit' => 'pcs',
'cost' => 20000,
'price' => 40000,
'tax' => 11,
'tax_type' => null,
'note' => 'Mainan edukasi untuk anak-anak',
'category_id' => 8, // Mainan
],
]);
}
}
Step 2: Creating Customer and Supplier Factories
Create Customer Factory
Run the command:
php artisan make:factory CustomerFactory
Edit the CustomerFactory
file:
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
class CustomerFactory extends Factory
{
public function definition(): array
{
return [
'name' => fake()->name,
'email' => fake()->unique()->safeEmail,
'phone' => fake()->phoneNumber,
'address' => fake()->address,
'province_id' => 35,
'regency_id' => 3578,
'district_id' => 3578080,
'village_id' => fake()->numberBetween(3578080001, 3578080007),
];
}
}
Create Supplier Factory
Run the command:
php artisan make:factory SupplierFactory
Edit the SupplierFactory
file:
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
class SupplierFactory extends Factory
{
public function definition(): array
{
return [
'name' => fake()->company,
'email' => fake()->unique()->safeEmail,
'phone' => fake()->phoneNumber,
'address' => fake()->address,
'province_id' => 35,
'regency_id' => 3578,
'district_id' => 3578080,
'village_id' => fake()->numberBetween(3578080001, 3578080007),
];
}
}
Step 3: Adjusting DatabaseSeeder
Edit DatabaseSeeder
to include seeders and factories:
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use App\Models\Customer;
use App\Models\Supplier;
use App\Models\User;
class DatabaseSeeder extends Seeder
{
public function run(): void
{
User::factory()->create([
'name' => 'Test User',
'email' => 'admin@admin.com',
]);
$this->call(ProvinceSeeder::class);
$this->call(RegencySeeder::class);
$this->call(DistrictSeeder::class);
$this->call(VillageSeeder::class);
$this->call(CategorySeeder::class);
$this->call(ProductSeeder::class);
Customer::factory(50)->create();
Supplier::factory(50)->create();
}
}
Step 4: Creating Sales and SaleDetails Models
Run the commands:
php artisan make:model Sale -m
php artisan make:model SaleDetail -m
Sale Migration
Edit create_sales_table
:
public function up(): void
{
Schema::create('sales', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete();
$table->foreignId('customer_id')->nullable()->constrained('customers')->nullOnDelete();
$table->string('customer_name');
$table->date('date');
$table->string('reference')->unique();
$table->timestamps();
});
}
SaleDetails Migration
Edit create_sale_details_table
:
public function up(): void
{
Schema::create('sale_details', function (Blueprint $table) {
$table->id();
$table->foreignId('sale_id')->constrained('sales')->cascadeOnDelete();
$table->foreignId('product_id')->nullable()->constrained('products')->nullOnDelete();
$table->string('product_name');
$table->string('product_code');
$table->unsignedBigInteger('unit_price');
$table->unsignedBigInteger('quantity');
$table->unsignedBigInteger('total_product_price');
$table->timestamps();
});
}
Sale Model
Edit Sale
:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Sale extends Model
{
protected $guarded = [];
public function customer(): BelongsTo
{
return $this->belongsTo(Customer::class);
}
public function saleDetails(): HasMany
{
return $this->hasMany(SaleDetail::class);
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
SaleDetail Model
Edit SaleDetail
:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class SaleDetail extends Model
{
protected $guarded = [];
public function product(): BelongsTo
{
return $this->belongsTo(Product::class);
}
}
Step 5: Running Migrations and Seeders
Run:
php artisan migrate:fresh --seed
Step 6: Installing Laravel Shield
Run:
php artisan shield:install
Step 7: Creating Filament Resource
Run:
php artisan make:filament-resource Sale --generate --view
Customizing SaleResource
Edit SaleResource.php
to define the form schema:
public static function form(Form $form): Form
{
$date = now()->format('Ymd');
return $form
->schema([
Forms\Components\Section::make()
->schema([
Forms\Components\Select::make('customer_id')
->relationship('customer', 'name'),
Forms\Components\DatePicker::make('date')
->label(__('Date'))
->native(false)
->default(now())
->live()
->afterStateUpdated(function (Set $set, $state) {
$formattedDate = \Carbon\Carbon::parse($state)->format('Ymd');
$date = \Carbon\Carbon::parse($state);
$count = Sale::whereDate('date', $date->toDateString())->count() + 1;
$set('reference', "SL_$formattedDate" . str_pad($count, 3, "0", STR_PAD_LEFT));
})
->required(),
Forms\Components\TextInput::make('reference')
->default(fn() => "SL_$date" . str_pad(Sale::whereDate('date', $date)->count() + 1, 3, "0", STR_PAD_LEFT))
->unique(ignoreRecord: true)
->required()
->maxLength(255),
Repeater::make('saleDetails')
->label(__('Products'))
->relationship()
->schema([
Select::make('product_id')
->relationship('product', 'name')
->live()
->afterStateUpdated(function (Set $set, $state, Get $get) {
$product = Product::query()->find($state);
if ($product) {
$set('product_name', $product?->name);
$set('product_code', $product?->code);
$formattedPrice = number_format($product?->price, 0); // Format price as "Rp 1.000"
$set('unit_price', $formattedPrice);
$quantity = (int) str_replace(',', '', $get('quantity'));
$total_price = $product?->price * $quantity;
$formattedPrice = number_format($total_price, 0);
$set('total_product_price', $formattedPrice);
} else {
$set('unit_price', 0);
$set('quantity', 0);
$set('total_product_price', 0);
$set('product_name', '');
$set('product_code', '');
}
})
->preload()
->searchable()
->columnSpan(3),
TextInput::make('product_name')
->required()
->columnSpan(2),
TextInput::make('unit_price')
->label(__('Unit Price'))
->prefix('Rp')
->readOnly()
->mask(RawJs::make('$money($input)'))
->live()
->dehydrateStateUsing(fn(string $state): string => (int) str_replace(',', '', $state))
->required()
->columnSpan(3),
TextInput::make('quantity')
->numeric()
->default(0)
->live(debounce: 300)
->afterStateUpdated(function (Set $set, $state, Get $get) {
$unitPrice = (int) str_replace(',', '', $get('unit_price'));
$total_price = $state * $unitPrice;
$formattedPrice = number_format($total_price, 0);
$set('total_product_price', $formattedPrice);
})
->required()
->columnSpan(1),
TextInput::make('total_product_price')
->label(__('Total Price'))
->live()
->prefix('Rp')
->readOnly()
->mask(RawJs::make('$money($input)'))
->dehydrateStateUsing(fn(string $state): string => (int) str_replace(',', '', $state))
->required()
->columnSpan(3),
Hidden::make('product_code'),
])
->columns(12)
->columnSpanFull()
])
->columns(3)
]);
}
Adding mutateFormDataBeforeCreate
Edit CreateSale.php
:
protected function mutateFormDataBeforeCreate(array $data): array
{
$data['user_id'] = Auth::id();
return $data;
}
Step 8: Testing the Application
Visit your application and test creating sales using the Filament interface. You should be able to manage all related resources effectively.
Subscribe to my newsletter
Read articles from Programmer Telo directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
