refactor UI

This commit is contained in:
IlhamIslamy 2026-04-15 00:10:22 +07:00
parent cd50b5b10d
commit 859dbe13a1
11 changed files with 122 additions and 559 deletions

View File

@ -10,11 +10,9 @@
class PayrollController extends Controller
{
/**
* Tampilkan daftar semua payroll dengan Eager Loading (hindari N+1).
*/
public function index()
{
// Eager loading untuk mencegah N+1 query
$payrolls = Payroll::with('employee')
->latest()
->get()
@ -32,25 +30,21 @@ public function index()
]);
}
/**
* Tampilkan form Generate Payroll.
* Kirim daftar employees (beserta relasi position.basic_salary) ke frontend.
*/
public function create()
{
$employees = Employee::with(['position', 'department'])
->orderBy('name')
->get()
->map(fn($e) => [
'id' => $e->id,
'name' => $e->name,
'nip' => $e->nip,
'position' => [
'id' => $e->id,
'name' => $e->name,
'nip' => $e->nip,
'position' => [
'id' => $e->position?->id,
'name' => $e->position?->name,
'basic_salary' => $e->position?->basic_salary ?? 0,
],
'department' => [
'department' => [
'name' => $e->department?->name,
],
]);
@ -60,14 +54,6 @@ public function create()
]);
}
/**
* Simpan data payroll & hitung net_salary secara otomatis.
*
* Logika kalkulasi:
* net_salary = basic_salary
* + SUM(detail tipe "bonus")
* - SUM(detail tipe "deduction")
*/
public function store(Request $request)
{
$validated = $request->validate([
@ -80,20 +66,20 @@ public function store(Request $request)
'details.*.amount' => 'required|integer|min:0',
]);
// net_salary = basic_salary + Σbonus Σpotongan, minimal 0
$net = $validated['basic_salary'];
foreach ($validated['details'] ?? [] as $item) {
$net += $item['type'] === 'bonus'
? $item['amount']
: -$item['amount'];
}
$net = max(0, $net);
Payroll::create([
'employee_id' => $validated['employee_id'],
'period' => $validated['period'],
'basic_salary' => $validated['basic_salary'],
'details' => $validated['details'] ?? [],
'net_salary' => $net,
'net_salary' => max(0, $net),
'status' => 'pending',
]);
@ -101,9 +87,6 @@ public function store(Request $request)
->with('success', 'Payroll berhasil di-generate.');
}
/**
* Tampilkan detail slip gaji 1 payroll.
*/
public function show(Payroll $payroll)
{
$payroll->load('employee.position', 'employee.department');

View File

@ -21,12 +21,9 @@ public function index()
]);
}
/**
* Kirim hanya karyawan yang BELUM memiliki akun user ke form create.
* Query whereDoesntHave('user') mencegah 1 employee punya 2 akun.
*/
public function create()
{
// whereDoesntHave mencegah 1 employee memiliki lebih dari 1 akun
$employees = Employee::whereDoesntHave('user')
->orderBy('name')
->get()
@ -42,19 +39,13 @@ public function create()
]);
}
/**
* Buat akun user baru yang terhubung ke Employee yang dipilih.
* - Nama diambil otomatis dari Employee.
* - Password di-set default 'password'.
* - employee_id disimpan di kolom users.employee_id.
*/
public function store(Request $request)
{
$validated = $request->validate([
'employee_id' => [
'required',
'exists:employees,id',
'unique:users,employee_id', // pastikan belum punya user
'unique:users,employee_id',
],
'email' => 'required|email|max:255|unique:users,email',
'role' => ['required', Rule::in(['admin', 'employee'])],
@ -69,13 +60,12 @@ public function store(Request $request)
'role.in' => 'Role hanya boleh admin atau employee.',
]);
// Ambil nama dari profil Employee yang dipilih
$employee = Employee::findOrFail($validated['employee_id']);
User::create([
'name' => $employee->name,
'email' => $validated['email'],
'password' => Hash::make('password'), // default password
'password' => Hash::make('password'),
'role' => $validated['role'],
'employee_id' => $employee->id,
]);

View File

@ -24,9 +24,6 @@ class Payroll extends Model
'net_salary' => 'integer',
];
/**
* Relasi ke Employee
*/
public function employee()
{
return $this->belongsTo(Employee::class);

View File

@ -2,7 +2,6 @@
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
@ -10,14 +9,8 @@
class User extends Authenticatable
{
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory, Notifiable, TwoFactorAuthenticatable;
/**
* The attributes that are mass assignable.
*
* @var list<string>
*/
protected $fillable = [
'name',
'email',
@ -26,11 +19,6 @@ class User extends Authenticatable
'employee_id',
];
/**
* The attributes that should be hidden for serialization.
*
* @var list<string>
*/
protected $hidden = [
'password',
'two_factor_secret',
@ -43,7 +31,7 @@ public function employee()
return $this->hasOne(Employee::class);
}
/** Akses langsung ke profil Employee milik user ini */
// employee_id di kolom users untuk relasi One-to-One langsung
public function employeeProfile()
{
return $this->belongsTo(Employee::class, 'employee_id');
@ -54,17 +42,12 @@ public function hasRole($role)
return $this->role === $role;
}
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
'two_factor_confirmed_at' => 'datetime',
'email_verified_at' => 'datetime',
'password' => 'hashed',
'two_factor_confirmed_at'=> 'datetime',
];
}
}

View File

@ -14,9 +14,9 @@ public function up(): void
Schema::create('payrolls', function (Blueprint $table) {
$table->id();
$table->foreignId('employee_id')->constrained()->onDelete('cascade');
$table->string('period'); // Format: "2026-04" (YYYY-MM)
$table->string('period');
$table->unsignedBigInteger('basic_salary');
$table->json('details')->nullable(); // [{name, type, amount}]
$table->json('details')->nullable();
$table->unsignedBigInteger('net_salary');
$table->enum('status', ['pending', 'paid'])->default('pending');
$table->timestamps();

View File

@ -6,18 +6,14 @@
return new class extends Migration
{
/**
* Tambah kolom employee_id (nullable) pada tabel users.
* Nullable karena akun admin tidak harus punya profil Employee.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->foreignId('employee_id')
->nullable()
->unique() // One-to-One: 1 user = 1 employee
->unique()
->constrained('employees')
->nullOnDelete() // Jika employee dihapus, kolom ini jadi NULL
->nullOnDelete()
->after('id');
});
}

View File

@ -9,10 +9,10 @@ class DatabaseSeeder extends Seeder
public function run(): void
{
$this->call([
UserSeeder::class, // Admin account (jangan dihapus, dibutuhkan login)
MasterDataSeeder::class, // 1. Departments + Positions (dengan basic_salary)
EmployeeSeeder::class, // 2. Users (employee) + profil Employee (FK ke dept & position)
PayrollSeeder::class, // 3. Slip gaji (FK ke employee)
UserSeeder::class,
MasterDataSeeder::class,
EmployeeSeeder::class,
PayrollSeeder::class,
]);
}
}

View File

@ -9,266 +9,56 @@
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
/**
* EmployeeSeeder
*
* Membuat akun User (role: employee) beserta profil Employee yang
* terhubung ke Department dan Position yang sudah dibuat oleh MasterDataSeeder.
*
* Variasi kasus yang di-cover:
* - Karyawan tetap (PKWTT) di berbagai departemen
* - Karyawan kontrak (PKWT)
* - Karyawan magang
* - Berbagai jabatan dengan gaji pokok berbeda
*/
class EmployeeSeeder extends Seeder
{
public function run(): void
{
// ── Resolve Master Data by Name (aman terhadap perubahan ID) ──
$depts = Department::pluck('id', 'name');
$depts = Department::pluck('id', 'name');
$positions = Position::pluck('id', 'name');
// ──────────────────────────────────────────────────────────────
// DATA KARYAWAN
// Format: [user_data, employee_data]
// ──────────────────────────────────────────────────────────────
$employees = [
// ── IT DEPARTMENT ────────────────────────────────────────
[
'user' => [
'name' => 'Jono Joni',
'email' => 'jono12@gmail.com',
'password' => Hash::make('password'),
'role' => 'employee',
],
'employee' => [
'nip' => '2023001',
'name' => 'Jono Joni',
'gender' => 'L',
'place_of_birth' => 'Jakarta',
'birth_date' => '1992-05-14',
'address' => 'Jl. Merdeka No.12, Jakarta Pusat',
'phone_number' => '081234567890',
'department_id' => $depts['Information Technology'],
'position_id' => $positions['Senior Developer'],
'status' => 'PKWTT',
'join_date' => '2023-01-15',
],
'user' => ['name' => 'Jono Joni', 'email' => 'jono12@gmail.com', 'password' => Hash::make('password'), 'role' => 'employee'],
'employee' => ['nip' => '2023001', 'name' => 'Jono Joni', 'gender' => 'L', 'place_of_birth' => 'Jakarta', 'birth_date' => '1992-05-14', 'address' => 'Jl. Merdeka No.12, Jakarta Pusat', 'phone_number' => '081234567890', 'department_id' => $depts['Information Technology'], 'position_id' => $positions['Senior Developer'], 'status' => 'PKWTT', 'join_date' => '2023-01-15'],
],
[
'user' => [
'name' => 'Budi Santoso',
'email' => 'budi.santoso12@hris.com',
'password' => Hash::make('password'),
'role' => 'employee',
],
'employee' => [
'nip' => '2023002',
'name' => 'Budi Santoso',
'gender' => 'L',
'place_of_birth' => 'Bandung',
'birth_date' => '1995-08-22',
'address' => 'Jl. Sukajadi No.45, Bandung',
'phone_number' => '082345678901',
'department_id' => $depts['Information Technology'],
'position_id' => $positions['Junior Developer'],
'status' => 'PKWT',
'join_date' => '2023-03-01',
],
'user' => ['name' => 'Budi Santoso', 'email' => 'budi.santoso12@hris.com', 'password' => Hash::make('password'), 'role' => 'employee'],
'employee' => ['nip' => '2023002', 'name' => 'Budi Santoso', 'gender' => 'L', 'place_of_birth' => 'Bandung', 'birth_date' => '1995-08-22', 'address' => 'Jl. Sukajadi No.45, Bandung', 'phone_number' => '082345678901', 'department_id' => $depts['Information Technology'], 'position_id' => $positions['Junior Developer'], 'status' => 'PKWT', 'join_date' => '2023-03-01'],
],
[
'user' => [
'name' => 'Rizky Firmansyah',
'email' => 'rizky.firmansyah12@hris.com',
'password' => Hash::make('password'),
'role' => 'employee',
],
'employee' => [
'nip' => '2024001',
'name' => 'Rizky Firmansyah',
'gender' => 'L',
'place_of_birth' => 'Surabaya',
'birth_date' => '1998-11-30',
'address' => 'Jl. Pemuda No.7, Surabaya',
'phone_number' => '083456789012',
'department_id' => $depts['Information Technology'],
'position_id' => $positions['Junior Developer'],
'status' => 'Magang',
'join_date' => '2024-02-01',
],
'user' => ['name' => 'Rizky Firmansyah', 'email' => 'rizky.firmansyah12@hris.com', 'password' => Hash::make('password'), 'role' => 'employee'],
'employee' => ['nip' => '2024001', 'name' => 'Rizky Firmansyah', 'gender' => 'L', 'place_of_birth' => 'Surabaya', 'birth_date' => '1998-11-30', 'address' => 'Jl. Pemuda No.7, Surabaya', 'phone_number' => '083456789012', 'department_id' => $depts['Information Technology'], 'position_id' => $positions['Junior Developer'], 'status' => 'Magang','join_date' => '2024-02-01'],
],
// ── HR DEPARTMENT ────────────────────────────────────────
[
'user' => [
'name' => 'Dewi Rahayu',
'email' => 'dewi.rahayu12@hris.com',
'password' => Hash::make('password'),
'role' => 'employee',
],
'employee' => [
'nip' => '2022001',
'name' => 'Dewi Rahayu',
'gender' => 'P',
'place_of_birth' => 'Yogyakarta',
'birth_date' => '1990-03-10',
'address' => 'Jl. Malioboro No.88, Yogyakarta',
'phone_number' => '084567890123',
'department_id' => $depts['Human Resources'],
'position_id' => $positions['Manager'],
'status' => 'PKWTT',
'join_date' => '2022-06-01',
],
'user' => ['name' => 'Dewi Rahayu', 'email' => 'dewi.rahayu12@hris.com', 'password' => Hash::make('password'), 'role' => 'employee'],
'employee' => ['nip' => '2022001', 'name' => 'Dewi Rahayu', 'gender' => 'P', 'place_of_birth' => 'Yogyakarta', 'birth_date' => '1990-03-10', 'address' => 'Jl. Malioboro No.88, Yogyakarta', 'phone_number' => '084567890123', 'department_id' => $depts['Human Resources'], 'position_id' => $positions['Manager'], 'status' => 'PKWTT', 'join_date' => '2022-06-01'],
],
[
'user' => [
'name' => 'Anisa Putri',
'email' => 'anisa.putri12@hris.com',
'password' => Hash::make('password'),
'role' => 'employee',
],
'employee' => [
'nip' => '2023003',
'name' => 'Anisa Putri',
'gender' => 'P',
'place_of_birth' => 'Semarang',
'birth_date' => '1997-07-19',
'address' => 'Jl. Pahlawan No.3, Semarang',
'phone_number' => '085678901234',
'department_id' => $depts['Human Resources'],
'position_id' => $positions['HR Specialist'],
'status' => 'PKWTT',
'join_date' => '2023-07-01',
],
'user' => ['name' => 'Anisa Putri', 'email' => 'anisa.putri12@hris.com', 'password' => Hash::make('password'), 'role' => 'employee'],
'employee' => ['nip' => '2023003', 'name' => 'Anisa Putri', 'gender' => 'P', 'place_of_birth' => 'Semarang', 'birth_date' => '1997-07-19', 'address' => 'Jl. Pahlawan No.3, Semarang', 'phone_number' => '085678901234', 'department_id' => $depts['Human Resources'], 'position_id' => $positions['HR Specialist'], 'status' => 'PKWTT', 'join_date' => '2023-07-01'],
],
// ── FINANCE DEPARTMENT ───────────────────────────────────
[
'user' => [
'name' => 'Hendra Kurniawan',
'email' => 'hendra.kurniawan12@hris.com',
'password' => Hash::make('password'),
'role' => 'employee',
],
'employee' => [
'nip' => '2021001',
'name' => 'Hendra Kurniawan',
'gender' => 'L',
'place_of_birth' => 'Medan',
'birth_date' => '1988-12-05',
'address' => 'Jl. Sudirman No.22, Medan',
'phone_number' => '086789012345',
'department_id' => $depts['Finance'],
'position_id' => $positions['Finance Analyst'],
'status' => 'PKWTT',
'join_date' => '2021-04-01',
],
'user' => ['name' => 'Hendra Kurniawan', 'email' => 'hendra.kurniawan12@hris.com', 'password' => Hash::make('password'), 'role' => 'employee'],
'employee' => ['nip' => '2021001', 'name' => 'Hendra Kurniawan', 'gender' => 'L', 'place_of_birth' => 'Medan', 'birth_date' => '1988-12-05', 'address' => 'Jl. Sudirman No.22, Medan', 'phone_number' => '086789012345', 'department_id' => $depts['Finance'], 'position_id' => $positions['Finance Analyst'], 'status' => 'PKWTT', 'join_date' => '2021-04-01'],
],
[
'user' => [
'name' => 'Sari Wulandari',
'email' => 'sari.wulandari12@hris.com',
'password' => Hash::make('password'),
'role' => 'employee',
],
'employee' => [
'nip' => '2023004',
'name' => 'Sari Wulandari',
'gender' => 'P',
'place_of_birth' => 'Solo',
'birth_date' => '1996-02-28',
'address' => 'Jl. Brigjen Katamso No.5, Solo',
'phone_number' => '087890123456',
'department_id' => $depts['Finance'],
'position_id' => $positions['Staff Admin'],
'status' => 'PKWT',
'join_date' => '2023-09-01',
],
'user' => ['name' => 'Sari Wulandari', 'email' => 'sari.wulandari12@hris.com', 'password' => Hash::make('password'), 'role' => 'employee'],
'employee' => ['nip' => '2023004', 'name' => 'Sari Wulandari', 'gender' => 'P', 'place_of_birth' => 'Solo', 'birth_date' => '1996-02-28', 'address' => 'Jl. Brigjen Katamso No.5, Solo', 'phone_number' => '087890123456', 'department_id' => $depts['Finance'], 'position_id' => $positions['Staff Admin'], 'status' => 'PKWT', 'join_date' => '2023-09-01'],
],
// ── MARKETING DEPARTMENT ─────────────────────────────────
[
'user' => [
'name' => 'Fajar Nugroho',
'email' => 'fajar.nugroho12@hris.com',
'password' => Hash::make('password'),
'role' => 'employee',
],
'employee' => [
'nip' => '2022002',
'name' => 'Fajar Nugroho',
'gender' => 'L',
'place_of_birth' => 'Makassar',
'birth_date' => '1993-09-15',
'address' => 'Jl. Sultan Hasanuddin No.10, Makassar',
'phone_number' => '088901234567',
'department_id' => $depts['Marketing'],
'position_id' => $positions['Marketing Staff'],
'status' => 'PKWTT',
'join_date' => '2022-11-01',
],
'user' => ['name' => 'Fajar Nugroho', 'email' => 'fajar.nugroho12@hris.com', 'password' => Hash::make('password'), 'role' => 'employee'],
'employee' => ['nip' => '2022002', 'name' => 'Fajar Nugroho', 'gender' => 'L', 'place_of_birth' => 'Makassar', 'birth_date' => '1993-09-15', 'address' => 'Jl. Sultan Hasanuddin No.10, Makassar', 'phone_number' => '088901234567', 'department_id' => $depts['Marketing'], 'position_id' => $positions['Marketing Staff'], 'status' => 'PKWTT', 'join_date' => '2022-11-01'],
],
// ── OPERATIONS DEPARTMENT ────────────────────────────────
[
'user' => [
'name' => 'Linda Permatasari',
'email' => 'linda.permata12@hris.com',
'password' => Hash::make('password'),
'role' => 'employee',
],
'employee' => [
'nip' => '2024002',
'name' => 'Linda Permatasari',
'gender' => 'P',
'place_of_birth' => 'Palembang',
'birth_date' => '1999-04-20',
'address' => 'Jl. Demang Lebar Daun No.8, Palembang',
'phone_number' => '089012345678',
'department_id' => $depts['Operations'],
'position_id' => $positions['Operations Staff'],
'status' => 'PKWT',
'join_date' => '2024-01-10',
],
'user' => ['name' => 'Linda Permatasari','email' => 'linda.permata12@hris.com', 'password' => Hash::make('password'), 'role' => 'employee'],
'employee' => ['nip' => '2024002', 'name' => 'Linda Permatasari','gender' => 'P', 'place_of_birth' => 'Palembang', 'birth_date' => '1999-04-20', 'address' => 'Jl. Demang Lebar Daun No.8, Palembang', 'phone_number' => '089012345678', 'department_id' => $depts['Operations'], 'position_id' => $positions['Operations Staff'], 'status' => 'PKWT', 'join_date' => '2024-01-10'],
],
[
'user' => [
'name' => 'Agus Prasetyo',
'email' => 'agus.prasetyo12@hris.com',
'password' => Hash::make('password'),
'role' => 'employee',
],
'employee' => [
'nip' => '2020001',
'name' => 'Agus Prasetyo',
'gender' => 'L',
'place_of_birth' => 'Malang',
'birth_date' => '1985-06-10',
'address' => 'Jl. Ijen No.33, Malang',
'phone_number' => '081112233445',
'department_id' => $depts['Operations'],
'position_id' => $positions['Manager'],
'status' => 'PKWTT',
'join_date' => '2020-08-01',
],
'user' => ['name' => 'Agus Prasetyo', 'email' => 'agus.prasetyo12@hris.com', 'password' => Hash::make('password'), 'role' => 'employee'],
'employee' => ['nip' => '2020001', 'name' => 'Agus Prasetyo', 'gender' => 'L', 'place_of_birth' => 'Malang', 'birth_date' => '1985-06-10', 'address' => 'Jl. Ijen No.33, Malang', 'phone_number' => '081112233445', 'department_id' => $depts['Operations'], 'position_id' => $positions['Manager'], 'status' => 'PKWTT', 'join_date' => '2020-08-01'],
],
];
// ──────────────────────────────────────────────────────────────
// Buat User → lalu buat Employee yang terhubung ke user tersebut
// ──────────────────────────────────────────────────────────────
foreach ($employees as $data) {
$user = User::create($data['user']);
Employee::create(array_merge($data['employee'], ['user_id' => $user->id]));

View File

@ -6,12 +6,6 @@
use App\Models\Position;
use Illuminate\Database\Seeder;
/**
* MasterDataSeeder
*
* Seed departemen dan jabatan beserta gaji pokok masing-masing.
* Seeder ini HARUS dijalankan sebelum EmployeeSeeder dan PayrollSeeder.
*/
class MasterDataSeeder extends Seeder
{
public function run(): void

View File

@ -6,24 +6,11 @@
use App\Models\Payroll;
use Illuminate\Database\Seeder;
/**
* PayrollSeeder
*
* Membuat data payroll dummy yang realistis dan terhubung ke employee.
* Seeder ini HARUS dijalankan setelah EmployeeSeeder.
*
* Variasi kasus yang di-cover:
* - Status 'paid' (bulan lalu) dan 'pending' (bulan ini)
* - Karyawan dengan bonus lembur, tunjangan jabatan, tunjangan transport
* - Karyawan dengan potongan (BPJS, absen, pinjaman, keterlambatan)
* - Karyawan magang (gaji pokok rendah, komponen minimal)
* - Karyawan senior dengan tunjangan lengkap
*/
class PayrollSeeder extends Seeder
{
public function run(): void
{
// Helper: hitung net_salary otomatis (sama dengan logika Controller)
// Kalkulasi net_salary konsisten dengan logika di PayrollController
$net = function (int $basic, array $details): int {
$total = $basic;
foreach ($details as $item) {
@ -32,177 +19,98 @@ public function run(): void
return max(0, $total);
};
// Resolve semua employee sekaligus (1 query, bukan N query)
// Eager pluck: 1 query untuk semua employee
$emp = Employee::pluck('id', 'name');
// Periode referensi
$bulanLalu = now()->subMonth()->format('Y-m');
$bulanIni = now()->format('Y-m');
// ──────────────────────────────────────────────────────────────
// DATASET PAYROLL
// ──────────────────────────────────────────────────────────────
$records = [];
// ── 1. JONO JONI — Senior Developer (IT) ─────────────────────
// Kasus: Karyawan senior, ada bonus proyek + tunjangan lengkap.
// Bulan lalu: sudah dibayar. Bulan ini: masih pending.
if (isset($emp['Jono Joni'])) {
$basicJono = 9_000_000;
$basic = 9_000_000;
$detailLalu = [
['name' => 'Tunjangan Transport', 'type' => 'bonus', 'amount' => 600_000],
['name' => 'Tunjangan Makan', 'type' => 'bonus', 'amount' => 450_000],
['name' => 'Potongan BPJS Kesehatan','type' => 'deduction', 'amount' => 180_000],
['name' => 'Potongan BPJS TK', 'type' => 'deduction', 'amount' => 90_000],
];
$records[] = [
'employee_id' => $emp['Jono Joni'],
'period' => $bulanLalu,
'basic_salary' => $basicJono,
'details' => $detailLalu,
'net_salary' => $net($basicJono, $detailLalu),
'status' => 'paid',
['name' => 'Tunjangan Transport', 'type' => 'bonus', 'amount' => 600_000],
['name' => 'Tunjangan Makan', 'type' => 'bonus', 'amount' => 450_000],
['name' => 'Potongan BPJS Kesehatan', 'type' => 'deduction', 'amount' => 180_000],
['name' => 'Potongan BPJS TK', 'type' => 'deduction', 'amount' => 90_000],
];
$records[] = ['employee_id' => $emp['Jono Joni'], 'period' => $bulanLalu, 'basic_salary' => $basic, 'details' => $detailLalu, 'net_salary' => $net($basic, $detailLalu), 'status' => 'paid'];
$detailIni = [
['name' => 'Tunjangan Transport', 'type' => 'bonus', 'amount' => 600_000],
['name' => 'Tunjangan Makan', 'type' => 'bonus', 'amount' => 450_000],
['name' => 'Bonus Proyek Klien', 'type' => 'bonus', 'amount' => 1_500_000],
['name' => 'Potongan BPJS Kesehatan','type' => 'deduction', 'amount' => 180_000],
['name' => 'Potongan BPJS TK', 'type' => 'deduction', 'amount' => 90_000],
];
$records[] = [
'employee_id' => $emp['Jono Joni'],
'period' => $bulanIni,
'basic_salary' => $basicJono,
'details' => $detailIni,
'net_salary' => $net($basicJono, $detailIni),
'status' => 'pending',
['name' => 'Tunjangan Transport', 'type' => 'bonus', 'amount' => 600_000],
['name' => 'Tunjangan Makan', 'type' => 'bonus', 'amount' => 450_000],
['name' => 'Bonus Proyek Klien', 'type' => 'bonus', 'amount' => 1_500_000],
['name' => 'Potongan BPJS Kesehatan', 'type' => 'deduction', 'amount' => 180_000],
['name' => 'Potongan BPJS TK', 'type' => 'deduction', 'amount' => 90_000],
];
$records[] = ['employee_id' => $emp['Jono Joni'], 'period' => $bulanIni, 'basic_salary' => $basic, 'details' => $detailIni, 'net_salary' => $net($basic, $detailIni), 'status' => 'pending'];
}
// ── 2. BUDI SANTOSO — Junior Developer (IT) ──────────────────
// Kasus: Karyawan kontrak, ada potongan keterlambatan.
if (isset($emp['Budi Santoso'])) {
$basicBudi = 5_500_000;
$basic = 5_500_000;
$detailLalu = [
['name' => 'Tunjangan Transport', 'type' => 'bonus', 'amount' => 400_000],
['name' => 'Tunjangan Makan', 'type' => 'bonus', 'amount' => 350_000],
['name' => 'Potongan BPJS TK', 'type' => 'deduction', 'amount' => 55_000],
['name' => 'Potongan Keterlambatan', 'type' => 'deduction', 'amount' => 100_000],
];
$records[] = [
'employee_id' => $emp['Budi Santoso'],
'period' => $bulanLalu,
'basic_salary' => $basicBudi,
'details' => $detailLalu,
'net_salary' => $net($basicBudi, $detailLalu),
'status' => 'paid',
['name' => 'Tunjangan Transport', 'type' => 'bonus', 'amount' => 400_000],
['name' => 'Tunjangan Makan', 'type' => 'bonus', 'amount' => 350_000],
['name' => 'Potongan BPJS TK', 'type' => 'deduction', 'amount' => 55_000],
['name' => 'Potongan Keterlambatan', 'type' => 'deduction', 'amount' => 100_000],
];
$records[] = ['employee_id' => $emp['Budi Santoso'], 'period' => $bulanLalu, 'basic_salary' => $basic, 'details' => $detailLalu, 'net_salary' => $net($basic, $detailLalu), 'status' => 'paid'];
$detailIni = [
['name' => 'Tunjangan Transport', 'type' => 'bonus', 'amount' => 400_000],
['name' => 'Tunjangan Makan', 'type' => 'bonus', 'amount' => 350_000],
['name' => 'Potongan BPJS TK', 'type' => 'deduction', 'amount' => 55_000],
];
$records[] = [
'employee_id' => $emp['Budi Santoso'],
'period' => $bulanIni,
'basic_salary' => $basicBudi,
'details' => $detailIni,
'net_salary' => $net($basicBudi, $detailIni),
'status' => 'pending',
['name' => 'Tunjangan Transport', 'type' => 'bonus', 'amount' => 400_000],
['name' => 'Tunjangan Makan', 'type' => 'bonus', 'amount' => 350_000],
['name' => 'Potongan BPJS TK', 'type' => 'deduction', 'amount' => 55_000],
];
$records[] = ['employee_id' => $emp['Budi Santoso'], 'period' => $bulanIni, 'basic_salary' => $basic, 'details' => $detailIni, 'net_salary' => $net($basic, $detailIni), 'status' => 'pending'];
}
// ── 3. RIZKY FIRMANSYAH — Junior Developer Magang (IT) ───────
// Kasus: Karyawan magang, komponen sangat minimal, tidak ada BPJS penuh.
if (isset($emp['Rizky Firmansyah'])) {
$basicRizky = 5_500_000; // gaji pokok jabatan "Junior Developer"
$basic = 5_500_000;
$detailIni = [
['name' => 'Uang Transport Magang', 'type' => 'bonus', 'amount' => 200_000],
['name' => 'Potongan Tidak Hadir', 'type' => 'deduction', 'amount' => 250_000],
];
$records[] = [
'employee_id' => $emp['Rizky Firmansyah'],
'period' => $bulanIni,
'basic_salary' => $basicRizky,
'details' => $detailIni,
'net_salary' => $net($basicRizky, $detailIni),
'status' => 'pending',
];
$records[] = ['employee_id' => $emp['Rizky Firmansyah'], 'period' => $bulanIni, 'basic_salary' => $basic, 'details' => $detailIni, 'net_salary' => $net($basic, $detailIni), 'status' => 'pending'];
}
// ── 4. DEWI RAHAYU — Manager HR ──────────────────────────────
// Kasus: Manajer dengan tunjangan jabatan besar + BPJS penuh.
if (isset($emp['Dewi Rahayu'])) {
$basicDewi = 12_000_000;
$basic = 12_000_000;
$detailLalu = [
['name' => 'Tunjangan Jabatan', 'type' => 'bonus', 'amount' => 2_000_000],
['name' => 'Tunjangan Transport', 'type' => 'bonus', 'amount' => 700_000],
['name' => 'Tunjangan Makan', 'type' => 'bonus', 'amount' => 500_000],
['name' => 'Potongan BPJS Kesehatan', 'type' => 'deduction', 'amount' => 240_000],
['name' => 'Potongan BPJS TK', 'type' => 'deduction', 'amount' => 120_000],
['name' => 'Potongan PPh 21', 'type' => 'deduction', 'amount' => 500_000],
];
$records[] = [
'employee_id' => $emp['Dewi Rahayu'],
'period' => $bulanLalu,
'basic_salary' => $basicDewi,
'details' => $detailLalu,
'net_salary' => $net($basicDewi, $detailLalu),
'status' => 'paid',
['name' => 'Tunjangan Jabatan', 'type' => 'bonus', 'amount' => 2_000_000],
['name' => 'Tunjangan Transport', 'type' => 'bonus', 'amount' => 700_000],
['name' => 'Tunjangan Makan', 'type' => 'bonus', 'amount' => 500_000],
['name' => 'Potongan BPJS Kesehatan', 'type' => 'deduction', 'amount' => 240_000],
['name' => 'Potongan BPJS TK', 'type' => 'deduction', 'amount' => 120_000],
['name' => 'Potongan PPh 21', 'type' => 'deduction', 'amount' => 500_000],
];
$records[] = ['employee_id' => $emp['Dewi Rahayu'], 'period' => $bulanLalu, 'basic_salary' => $basic, 'details' => $detailLalu, 'net_salary' => $net($basic, $detailLalu), 'status' => 'paid'];
$detailIni = [
['name' => 'Tunjangan Jabatan', 'type' => 'bonus', 'amount' => 2_000_000],
['name' => 'Tunjangan Transport', 'type' => 'bonus', 'amount' => 700_000],
['name' => 'Tunjangan Makan', 'type' => 'bonus', 'amount' => 500_000],
['name' => 'Bonus Kinerja Triwulan', 'type' => 'bonus', 'amount' => 1_000_000],
['name' => 'Potongan BPJS Kesehatan', 'type' => 'deduction', 'amount' => 240_000],
['name' => 'Potongan BPJS TK', 'type' => 'deduction', 'amount' => 120_000],
['name' => 'Potongan PPh 21', 'type' => 'deduction', 'amount' => 600_000],
];
$records[] = [
'employee_id' => $emp['Dewi Rahayu'],
'period' => $bulanIni,
'basic_salary' => $basicDewi,
'details' => $detailIni,
'net_salary' => $net($basicDewi, $detailIni),
'status' => 'pending',
['name' => 'Tunjangan Jabatan', 'type' => 'bonus', 'amount' => 2_000_000],
['name' => 'Tunjangan Transport', 'type' => 'bonus', 'amount' => 700_000],
['name' => 'Tunjangan Makan', 'type' => 'bonus', 'amount' => 500_000],
['name' => 'Bonus Kinerja Triwulan', 'type' => 'bonus', 'amount' => 1_000_000],
['name' => 'Potongan BPJS Kesehatan', 'type' => 'deduction', 'amount' => 240_000],
['name' => 'Potongan BPJS TK', 'type' => 'deduction', 'amount' => 120_000],
['name' => 'Potongan PPh 21', 'type' => 'deduction', 'amount' => 600_000],
];
$records[] = ['employee_id' => $emp['Dewi Rahayu'], 'period' => $bulanIni, 'basic_salary' => $basic, 'details' => $detailIni, 'net_salary' => $net($basic, $detailIni), 'status' => 'pending'];
}
// ── 5. ANISA PUTRI — HR Specialist ───────────────────────────
// Kasus: Karyawan tetap biasa, komponen standard.
if (isset($emp['Anisa Putri'])) {
$basicAnisa = 5_800_000;
$basic = 5_800_000;
$detailLalu = [
['name' => 'Tunjangan Transport', 'type' => 'bonus', 'amount' => 400_000],
['name' => 'Tunjangan Makan', 'type' => 'bonus', 'amount' => 350_000],
['name' => 'Potongan BPJS Kesehatan', 'type' => 'deduction', 'amount' => 116_000],
['name' => 'Potongan BPJS TK', 'type' => 'deduction', 'amount' => 58_000],
];
$records[] = [
'employee_id' => $emp['Anisa Putri'],
'period' => $bulanLalu,
'basic_salary' => $basicAnisa,
'details' => $detailLalu,
'net_salary' => $net($basicAnisa, $detailLalu),
'status' => 'paid',
];
$records[] = ['employee_id' => $emp['Anisa Putri'], 'period' => $bulanLalu, 'basic_salary' => $basic, 'details' => $detailLalu, 'net_salary' => $net($basic, $detailLalu), 'status' => 'paid'];
}
// ── 6. HENDRA KURNIAWAN — Finance Analyst ────────────────────
// Kasus: Karyawan lama, ada potongan cicilan pinjaman perusahaan.
if (isset($emp['Hendra Kurniawan'])) {
$basicHendra = 7_000_000;
$basic = 7_000_000;
$detailLalu = [
['name' => 'Tunjangan Transport', 'type' => 'bonus', 'amount' => 500_000],
['name' => 'Tunjangan Makan', 'type' => 'bonus', 'amount' => 450_000],
@ -210,111 +118,51 @@ public function run(): void
['name' => 'Potongan BPJS TK', 'type' => 'deduction', 'amount' => 70_000],
['name' => 'Cicilan Pinjaman', 'type' => 'deduction', 'amount' => 500_000],
];
$records[] = [
'employee_id' => $emp['Hendra Kurniawan'],
'period' => $bulanLalu,
'basic_salary' => $basicHendra,
'details' => $detailLalu,
'net_salary' => $net($basicHendra, $detailLalu),
'status' => 'paid',
];
$records[] = ['employee_id' => $emp['Hendra Kurniawan'], 'period' => $bulanLalu, 'basic_salary' => $basic, 'details' => $detailLalu, 'net_salary' => $net($basic, $detailLalu), 'status' => 'paid'];
$detailIni = [
['name' => 'Tunjangan Transport', 'type' => 'bonus', 'amount' => 500_000],
['name' => 'Tunjangan Makan', 'type' => 'bonus', 'amount' => 450_000],
['name' => 'Potongan BPJS Kesehatan', 'type' => 'deduction', 'amount' => 140_000],
['name' => 'Potongan BPJS TK', 'type' => 'deduction', 'amount' => 70_000],
['name' => 'Cicilan Pinjaman', 'type' => 'deduction', 'amount' => 500_000],
];
$records[] = [
'employee_id' => $emp['Hendra Kurniawan'],
'period' => $bulanIni,
'basic_salary' => $basicHendra,
'details' => $detailIni,
'net_salary' => $net($basicHendra, $detailIni),
'status' => 'pending',
];
$detailIni = $detailLalu;
$records[] = ['employee_id' => $emp['Hendra Kurniawan'], 'period' => $bulanIni, 'basic_salary' => $basic, 'details' => $detailIni, 'net_salary' => $net($basic, $detailIni), 'status' => 'pending'];
}
// ── 7. SARI WULANDARI — Staff Admin Finance (PKWT) ───────────
// Kasus: Kontrak, ada potongan absen, tidak ada bonus.
if (isset($emp['Sari Wulandari'])) {
$basicSari = 4_000_000;
$basic = 4_000_000;
$detailIni = [
['name' => 'Tunjangan Transport', 'type' => 'bonus', 'amount' => 300_000],
['name' => 'Potongan Absen', 'type' => 'deduction', 'amount' => 200_000],
['name' => 'Potongan BPJS TK', 'type' => 'deduction', 'amount' => 40_000],
];
$records[] = [
'employee_id' => $emp['Sari Wulandari'],
'period' => $bulanIni,
'basic_salary' => $basicSari,
'details' => $detailIni,
'net_salary' => $net($basicSari, $detailIni),
'status' => 'pending',
['name' => 'Tunjangan Transport', 'type' => 'bonus', 'amount' => 300_000],
['name' => 'Potongan Absen', 'type' => 'deduction', 'amount' => 200_000],
['name' => 'Potongan BPJS TK', 'type' => 'deduction', 'amount' => 40_000],
];
$records[] = ['employee_id' => $emp['Sari Wulandari'], 'period' => $bulanIni, 'basic_salary' => $basic, 'details' => $detailIni, 'net_salary' => $net($basic, $detailIni), 'status' => 'pending'];
}
// ── 8. FAJAR NUGROHO — Marketing Staff ───────────────────────
// Kasus: Marketing, ada bonus komisi penjualan bulan lalu.
if (isset($emp['Fajar Nugroho'])) {
$basicFajar = 4_500_000;
$basic = 4_500_000;
$detailLalu = [
['name' => 'Tunjangan Transport', 'type' => 'bonus', 'amount' => 350_000],
['name' => 'Komisi Penjualan', 'type' => 'bonus', 'amount' => 2_000_000],
['name' => 'Potongan BPJS TK', 'type' => 'deduction', 'amount' => 45_000],
];
$records[] = [
'employee_id' => $emp['Fajar Nugroho'],
'period' => $bulanLalu,
'basic_salary' => $basicFajar,
'details' => $detailLalu,
'net_salary' => $net($basicFajar, $detailLalu),
'status' => 'paid',
['name' => 'Tunjangan Transport', 'type' => 'bonus', 'amount' => 350_000],
['name' => 'Komisi Penjualan', 'type' => 'bonus', 'amount' => 2_000_000],
['name' => 'Potongan BPJS TK', 'type' => 'deduction', 'amount' => 45_000],
];
$records[] = ['employee_id' => $emp['Fajar Nugroho'], 'period' => $bulanLalu, 'basic_salary' => $basic, 'details' => $detailLalu, 'net_salary' => $net($basic, $detailLalu), 'status' => 'paid'];
$detailIni = [
['name' => 'Tunjangan Transport', 'type' => 'bonus', 'amount' => 350_000],
['name' => 'Potongan BPJS TK', 'type' => 'deduction', 'amount' => 45_000],
];
$records[] = [
'employee_id' => $emp['Fajar Nugroho'],
'period' => $bulanIni,
'basic_salary' => $basicFajar,
'details' => $detailIni,
'net_salary' => $net($basicFajar, $detailIni),
'status' => 'pending',
['name' => 'Tunjangan Transport', 'type' => 'bonus', 'amount' => 350_000],
['name' => 'Potongan BPJS TK', 'type' => 'deduction', 'amount' => 45_000],
];
$records[] = ['employee_id' => $emp['Fajar Nugroho'], 'period' => $bulanIni, 'basic_salary' => $basic, 'details' => $detailIni, 'net_salary' => $net($basic, $detailIni), 'status' => 'pending'];
}
// ── 9. AGUS PRASETYO — Manager Operations ────────────────────
// Kasus: Manager senior dengan PPh 21 dan semua tunjangan.
if (isset($emp['Agus Prasetyo'])) {
$basicAgus = 12_000_000;
$basic = 12_000_000;
$detailLalu = [
['name' => 'Tunjangan Jabatan', 'type' => 'bonus', 'amount' => 2_500_000],
['name' => 'Tunjangan Transport', 'type' => 'bonus', 'amount' => 700_000],
['name' => 'Tunjangan Makan', 'type' => 'bonus', 'amount' => 500_000],
['name' => 'Potongan BPJS Kesehatan', 'type' => 'deduction', 'amount' => 240_000],
['name' => 'Potongan BPJS TK', 'type' => 'deduction', 'amount' => 120_000],
['name' => 'Potongan PPh 21', 'type' => 'deduction', 'amount' => 650_000],
];
$records[] = [
'employee_id' => $emp['Agus Prasetyo'],
'period' => $bulanLalu,
'basic_salary' => $basicAgus,
'details' => $detailLalu,
'net_salary' => $net($basicAgus, $detailLalu),
'status' => 'paid',
['name' => 'Tunjangan Jabatan', 'type' => 'bonus', 'amount' => 2_500_000],
['name' => 'Tunjangan Transport', 'type' => 'bonus', 'amount' => 700_000],
['name' => 'Tunjangan Makan', 'type' => 'bonus', 'amount' => 500_000],
['name' => 'Potongan BPJS Kesehatan', 'type' => 'deduction', 'amount' => 240_000],
['name' => 'Potongan BPJS TK', 'type' => 'deduction', 'amount' => 120_000],
['name' => 'Potongan PPh 21', 'type' => 'deduction', 'amount' => 650_000],
];
$records[] = ['employee_id' => $emp['Agus Prasetyo'], 'period' => $bulanLalu, 'basic_salary' => $basic, 'details' => $detailLalu, 'net_salary' => $net($basic, $detailLalu), 'status' => 'paid'];
}
// ──────────────────────────────────────────────────────────────
// Insert semua record
// ──────────────────────────────────────────────────────────────
foreach ($records as $data) {
Payroll::create($data);
}

View File

@ -13,8 +13,6 @@ import {
import AppLayout from '@/layouts/app-layout';
import { PlusCircle, FileText } from 'lucide-react';
// ─── Types ───────────────────────────────────────────────────────────────────
interface Payroll {
id: number;
employee_name: string;
@ -28,8 +26,6 @@ interface PageProps {
payrolls: Payroll[];
}
// ─── Helpers ─────────────────────────────────────────────────────────────────
function formatRupiah(value: number): string {
return new Intl.NumberFormat('id-ID', {
style: 'currency',
@ -38,14 +34,19 @@ function formatRupiah(value: number): string {
}).format(value);
}
/** Format "2026-04" → "April 2026" */
function formatPeriod(period: string): string {
const [year, month] = period.split('-');
const date = new Date(Number(year), Number(month) - 1, 1);
return date.toLocaleDateString('id-ID', { month: 'long', year: 'numeric' });
return new Date(Number(year), Number(month) - 1, 1)
.toLocaleDateString('id-ID', { month: 'long', year: 'numeric' });
}
// ─── Component ───────────────────────────────────────────────────────────────
function StatusBadge({ status }: { status: 'pending' | 'paid' }) {
return status === 'paid' ? (
<Badge className="bg-emerald-100 text-emerald-700 hover:bg-emerald-100">Paid</Badge>
) : (
<Badge className="bg-amber-100 text-amber-700 hover:bg-amber-100">Pending</Badge>
);
}
export default function Index({ payrolls }: PageProps) {
return (
@ -53,8 +54,6 @@ export default function Index({ payrolls }: PageProps) {
<Head title="Daftar Payroll" />
<div className="p-4 md:p-8 w-full space-y-4">
{/* Page Header */}
<div className="space-y-1">
<h2 className="text-2xl font-bold tracking-tight">Manajemen Payroll</h2>
<p className="text-muted-foreground">Daftar seluruh payroll yang telah di-generate.</p>
@ -131,20 +130,3 @@ export default function Index({ payrolls }: PageProps) {
</AppLayout>
);
}
// ─── Sub-component: Status Badge ─────────────────────────────────────────────
function StatusBadge({ status }: { status: 'pending' | 'paid' }) {
if (status === 'paid') {
return (
<Badge className="bg-emerald-100 text-emerald-700 hover:bg-emerald-100">
Paid
</Badge>
);
}
return (
<Badge className="bg-amber-100 text-amber-700 hover:bg-amber-100">
Pending
</Badge>
);
}