diff --git a/app/Http/Controllers/Admin/PayrollController.php b/app/Http/Controllers/Admin/PayrollController.php index 2ebd628..647fe8b 100644 --- a/app/Http/Controllers/Admin/PayrollController.php +++ b/app/Http/Controllers/Admin/PayrollController.php @@ -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'); diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php index 928ba10..097176e 100644 --- a/app/Http/Controllers/Admin/UserController.php +++ b/app/Http/Controllers/Admin/UserController.php @@ -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, ]); diff --git a/app/Models/Payroll.php b/app/Models/Payroll.php index dca344b..4c7f885 100644 --- a/app/Models/Payroll.php +++ b/app/Models/Payroll.php @@ -24,9 +24,6 @@ class Payroll extends Model 'net_salary' => 'integer', ]; - /** - * Relasi ke Employee - */ public function employee() { return $this->belongsTo(Employee::class); diff --git a/app/Models/User.php b/app/Models/User.php index 376be0e..7c0a8eb 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -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 - */ protected $fillable = [ 'name', 'email', @@ -26,11 +19,6 @@ class User extends Authenticatable 'employee_id', ]; - /** - * The attributes that should be hidden for serialization. - * - * @var list - */ 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 - */ 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', ]; } } diff --git a/database/migrations/2026_04_14_000002_create_payrolls_table.php b/database/migrations/2026_04_14_000002_create_payrolls_table.php index af24910..5f5ec8d 100644 --- a/database/migrations/2026_04_14_000002_create_payrolls_table.php +++ b/database/migrations/2026_04_14_000002_create_payrolls_table.php @@ -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(); diff --git a/database/migrations/2026_04_14_000003_add_employee_id_to_users_table.php b/database/migrations/2026_04_14_000003_add_employee_id_to_users_table.php index 2dc2440..3c01236 100644 --- a/database/migrations/2026_04_14_000003_add_employee_id_to_users_table.php +++ b/database/migrations/2026_04_14_000003_add_employee_id_to_users_table.php @@ -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'); }); } diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index d064134..9210d47 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -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, ]); } } \ No newline at end of file diff --git a/database/seeders/EmployeeSeeder.php b/database/seeders/EmployeeSeeder.php index 2b5dd38..566f543 100644 --- a/database/seeders/EmployeeSeeder.php +++ b/database/seeders/EmployeeSeeder.php @@ -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])); diff --git a/database/seeders/MasterDataSeeder.php b/database/seeders/MasterDataSeeder.php index 6624451..a0e8eb1 100644 --- a/database/seeders/MasterDataSeeder.php +++ b/database/seeders/MasterDataSeeder.php @@ -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 diff --git a/database/seeders/PayrollSeeder.php b/database/seeders/PayrollSeeder.php index be918ff..5c8e213 100644 --- a/database/seeders/PayrollSeeder.php +++ b/database/seeders/PayrollSeeder.php @@ -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); } diff --git a/resources/js/pages/admin/payrolls/index.tsx b/resources/js/pages/admin/payrolls/index.tsx index dac78c0..5882678 100644 --- a/resources/js/pages/admin/payrolls/index.tsx +++ b/resources/js/pages/admin/payrolls/index.tsx @@ -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' ? ( + Paid + ) : ( + Pending + ); +} export default function Index({ payrolls }: PageProps) { return ( @@ -53,8 +54,6 @@ export default function Index({ payrolls }: PageProps) {
- - {/* Page Header */}

Manajemen Payroll

Daftar seluruh payroll yang telah di-generate.

@@ -131,20 +130,3 @@ export default function Index({ payrolls }: PageProps) { ); } - -// ─── Sub-component: Status Badge ───────────────────────────────────────────── - -function StatusBadge({ status }: { status: 'pending' | 'paid' }) { - if (status === 'paid') { - return ( - - Paid - - ); - } - return ( - - Pending - - ); -}