Easy and Quick POS System Using Laravel Filament


Step 1: Create the Laravel Project
To create a new Laravel project named laravel-pos
, run:
laravel new laravel-pos
Alternatively, you can use Composer:
composer create-project laravel/laravel=11.3.2 laravel-pos
Step 2: Open the Project in Your Code Editor
Navigate into the project directory and open it in your code editor:
cd laravel-pos
code .
npm install && npm run build
composer run dev
Step 3: Configure the Database
Open the .env
file in your project root and update the database settings. Since Laravel's installer already configured the database and ran migrations during setup, this step is complete.
Step 3: Install Filament
Install Filament: Run the following command to install Filament version 3.2:
composer require filament/filament:"^3.2" -W
Install Admin Panel: Install the Filament admin panel using the command:
php artisan filament:install --panels
Step 4: Create Admin User
To create an admin user for your Filament admin panel, run the following command:
php artisan make:filament-user
Step 5: Create Location Management
To set up models, migrations, and seeders for Provinces, Regencies, Districts, and Villages, follow these steps:
1. Generate Models and Migrations
Run the following commands to create the models with their corresponding migrations and seeders:
php artisan make:model Province -ms
php artisan make:model Regency -ms
php artisan make:model District -ms
php artisan make:model Village -ms
2. Define Migrations
Replace the content of the generated migration files as follows:
Provinces Migration:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('provinces', function (Blueprint $table) {
$table->unsignedTinyInteger('id')->autoIncrement()->index();
$table->string('name', 50)->nullable(false);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('provinces');
}
};
Regencies Migration:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('regencies', function (Blueprint $table) {
$table->unsignedSmallInteger('id')->autoIncrement()->index();
$table->unsignedTinyInteger('province_id');
$table->string('name', 50)->nullable(false);
$table->foreign('province_id')->references('id')->on('provinces')->restrictOnDelete()->cascadeOnUpdate();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('regencies');
}
};
Districts Migration:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('districts', function (Blueprint $table) {
$table->unsignedMediumInteger('id')->autoIncrement()->index();
$table->unsignedSmallInteger('regency_id');
$table->string('name', 50)->nullable(false);
$table->foreign('regency_id')->references('id')->on('regencies')->cascadeOnUpdate()->restrictOnDelete();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('districts');
}
};
Villages Migration:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('villages', function (Blueprint $table) {
$table->unsignedBigInteger('id')->autoIncrement()->index();
$table->unsignedMediumInteger('district_id');
$table->string('name', 50)->nullable(false);
$table->foreign('district_id')->references('id')->on('districts')->cascadeOnUpdate()->restrictOnDelete();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('villages');
}
};
3. Prepare Seeder Data
Create a directory for seeder data:
mkdir database/seeders/data
Download seeder data using the following commands:
curl -o "database/seeders/data/laravel_pos_districts.sql" "https://raw.githubusercontent.com/CahBantul/laravel-pos/develop/database/seeders/data/laravel_pos_districts.sql"
curl -o "database/seeders/data/laravel_pos_provinces.sql" "https://raw.githubusercontent.com/CahBantul/laravel-pos/develop/database/seeders/data/laravel_pos_provinces.sql"
curl -o "database/seeders/data/laravel_pos_regencies.sql" "https://raw.githubusercontent.com/CahBantul/laravel-pos/develop/database/seeders/data/laravel_pos_regencies.sql"
curl -o "database/seeders/data/laravel_pos_villages.sql" "https://raw.githubusercontent.com/CahBantul/laravel-pos/develop/database/seeders/data/laravel_pos_villages.sql"
4. Define Seeders
Edit the seeders for each table as follows:
ProvinceSeeder:
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
class ProvinceSeeder extends Seeder
{
public function run(): void
{
$sqlPath = database_path('seeders/data/laravel_pos_provinces.sql');
if (File::exists($sqlPath)) {
DB::unprepared(File::get($sqlPath));
$this->command->info('Provinces data seeded successfully!');
} else {
$this->command->error('Provinces SQL file not found.');
}
}
}
Similarly, edit RegencySeeder, DistrictSeeder, and VillageSeeder, updating the $sqlPath
and success/error messages accordingly.
5. Add Seeders to DatabaseSeeder
Edit the DatabaseSeeder
file to include the new seeders:
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
public function run(): void
{
$this->call(ProvinceSeeder::class);
$this->call(RegencySeeder::class);
$this->call(DistrictSeeder::class);
$this->call(VillageSeeder::class);
}
}
6. Run Migrations and Seeders
Run the following commands to apply migrations and seed the database:
php artisan migrate
php artisan db:seed
Step 6: Create Filament Resource
to create filament resource use this command
php artisan make:filament-resource Province --generate
php artisan make:filament-resource Regency --generate
php artisan make:filament-resource District --generate
php artisan make:filament-resource Village --generate
Don’t forget to define relationships in the respective models. With these commands, you’ll instantly have CRUD forms generated for each resource. 🎉
Step 7: Validate Unique for Province, Regency, District, and Village
To validate uniqueness for the Province
, Regency
, District
, and Village
resources, you will add the unique
validation rule and ensure that the proper relationships are defined in both the models and resources.
1. Province Resource
In the ProvinceResource.php
, you can add the unique
validation rule for the name
field and ensure the model's fillable
property includes the name
attribute.
ProvinceResource.php:
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('name')
->unique(ignoreRecord: true) // Ensures unique validation while ignoring the current record
->required()
->maxLength(50),
]);
}
Province Model:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Province extends Model
{
protected $fillable = ['name']; // Make sure 'name' is fillable for mass assignment
}
2. Regency Resource
In the RegencyResource.php
, you'll validate the uniqueness of the name
within a specific province_id
by adding the custom rule
validation.
RegencyResource.php:
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\Select::make('province_id')
->relationship('province', 'name') // Establish relationship with Province
->required(),
Forms\Components\TextInput::make('name')
->rule(function (Get $get, $record) {
$recordId = $record?->id;
$provinceId = $get('province_id');
return "unique:regencies,name,{$recordId},id,province_id,{$provinceId}"; // Custom unique rule to check uniqueness in the context of the selected province
})
->required()
->maxLength(50),
]);
}
3. District Resource
Similarly, for the District
resource, you will validate uniqueness of the name
within a specific regency_id
.
DistrictResource.php:
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\Select::make('regency_id')
->relationship('regency', 'name') // Relationship with Regency
->required(),
Forms\Components\TextInput::make('name')
->rule(function (Get $get, $record) {
$recordId = $record?->id;
$regencyId = $get('regency_id');
return "unique:districts,name,{$recordId},id,regency_id,{$regencyId}"; // Ensure the district name is unique within the specific regency
})
->required()
->maxLength(50),
]);
}
4. Village Resource
Lastly, for the Village
resource, validate the uniqueness of the name
within a specific district_id
.
VillageResource.php:
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\Select::make('district_id')
->relationship('district', 'name') // Relationship with District
->required(),
Forms\Components\TextInput::make('name')
->rule(function (Get $get, $record) {
$recordId = $record?->id;
$districtId = $get('district_id');
return "unique:villages,name,{$recordId},id,district_id,{$districtId}"; // Ensure the village name is unique within the specific district
})
->required()
->maxLength(50),
]);
}
Important Notes:
Make sure that the relationships (
province
,regency
,district
) are correctly defined in the models.The
ignoreRecord: true
option is used in theProvince
resource to ensure that the current record is ignored when checking for uniqueness during updates (i.e., it doesn't consider the record itself as a duplicate).The custom
rule
method in theRegency
,District
, andVillage
resources ensures that the uniqueness check is limited to the parent resource (e.g., province, regency, or district).
Step 8: Create Customer and Supplier Models, Migrations, and Resources
1. Create Models and Migrations
First, create the Customer
and Supplier
models along with their migrations:
php artisan make:model Customer -m
php artisan make:model Supplier -m
2. Edit Customer Migration
Modify the up
method in the Customer
migration file as follows:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('customers', function (Blueprint $table) {
$table->id();
$table->string('name', 100);
$table->string('email', 100);
$table->string('phone', 20);
$table->string('address', 200);
$table->unsignedTinyInteger('province_id');
$table->unsignedSmallInteger('regency_id');
$table->unsignedMediumInteger('district_id');
$table->unsignedBigInteger('village_id');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('customers');
}
};
Run the migration:
php artisan migrate
You can replicate the same structure for the Supplier
table, adjusting as needed for specific requirements.
3. Define Relationships in Models
Update the Customer
model to include relationships with related location entities (Province
, Regency
, District
, and Village
):
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Customer extends Model
{
protected $fillable = ['name', 'email', 'phone', 'address', 'province_id', 'regency_id', 'district_id', 'village_id'];
public function province(): BelongsTo
{
return $this->belongsTo(Province::class);
}
public function regency(): BelongsTo
{
return $this->belongsTo(Regency::class);
}
public function district(): BelongsTo
{
return $this->belongsTo(District::class);
}
public function village(): BelongsTo
{
return $this->belongsTo(Village::class);
}
}
Repeat this process for the Supplier
model.
4. Generate Filament Resources
Generate resources for Customer
and Supplier
:
php artisan make:filament-resource Customer --generate --view
php artisan make:filament-resource Supplier --generate --view
5. Edit the CustomerResource
Customize the CustomerResource
class to define the form schema and table columns. Here’s the complete code:
<?php
namespace App\Filament\Resources;
use App\Filament\Resources\CustomerResource\Pages;
use App\Models\Customer;
use App\Models\District;
use App\Models\Regency;
use App\Models\Village;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Forms\Get;
use Filament\Forms\Set;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
class CustomerResource extends Resource
{
protected static ?string $model = Customer::class;
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('name')
->required()
->maxLength(100),
Forms\Components\TextInput::make('email')
->email()
->required()
->maxLength(100),
Forms\Components\TextInput::make('phone')
->tel()
->required()
->maxLength(20),
Forms\Components\TextInput::make('address')
->required()
->maxLength(100),
Forms\Components\Select::make('province_id')
->relationship('province', 'name')
->searchable()
->preload()
->live()
->afterStateUpdated(function (Set $set) {
$set('regency_id', null);
$set('district_id', null);
$set('village_id', null);
})
->required(),
Forms\Components\Select::make('regency_id')
->options(fn(Get $get) => Regency::query()->where('province_id', $get('province_id'))->pluck('name', 'id'))
->searchable()
->preload()
->live()
->afterStateUpdated(function (Set $set) {
$set('district_id', null);
$set('village_id', null);
})
->required(),
Forms\Components\Select::make('district_id')
->options(fn(Get $get) => District::query()->where('regency_id', $get('regency_id'))->pluck('name', 'id'))
->searchable()
->preload()
->live()
->afterStateUpdated(function (Set $set) {
$set('village_id', null);
})
->required(),
Forms\Components\Select::make('village_id')
->options(fn(Get $get) => Village::query()->where('district_id', $get('district_id'))->pluck('name', 'id'))
->searchable()
->preload()
->required(),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('name')->searchable(),
Tables\Columns\TextColumn::make('email')->searchable(),
Tables\Columns\TextColumn::make('phone')->searchable(),
Tables\Columns\TextColumn::make('address')->searchable(),
Tables\Columns\TextColumn::make('province.name')->sortable(),
Tables\Columns\TextColumn::make('regency.name')->sortable(),
Tables\Columns\TextColumn::make('district.name')->sortable(),
Tables\Columns\TextColumn::make('village.name')->sortable(),
Tables\Columns\TextColumn::make('created_at')->dateTime()->sortable(),
Tables\Columns\TextColumn::make('updated_at')->dateTime()->sortable(),
])
->filters([])
->actions([
Tables\Actions\ViewAction::make(),
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public static function getRelations(): array
{
return [];
}
public static function getPages(): array
{
return [
'index' => Pages\ListCustomers::route('/'),
'create' => Pages\CreateCustomer::route('/create'),
'view' => Pages\ViewCustomer::route('/{record}'),
'edit' => Pages\EditCustomer::route('/{record}/edit'),
];
}
}
6. Repeat for SupplierResource
Follow similar steps to customize the SupplierResource
class.
7. Organize Resources into a Submenu
To group the Customer
and Supplier
resources under a Parties
submenu, adjust the navigation configuration in both resources:
protected static ?string $navigationGroup = 'Parties';
With these steps, your Customer
and Supplier
management systems are ready, including location-based dynamic dropdowns and seamless integration with Filament.
Subscribe to my newsletter
Read articles from Programmer Telo directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
