Feat: Dashboard, Manajemen Positions, and Manajemen Departments
This commit is contained in:
parent
0ef81fb590
commit
ae9e0a2eb0
|
|
@ -0,0 +1,68 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Department;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
|
||||||
|
class DepartmentController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
return Inertia::render('admin/departments/index', [
|
||||||
|
'departments' => Department::withCount('employees')->latest()->get(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
return Inertia::render('admin/departments/create');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'name' => 'required|string|max:255|unique:departments,name',
|
||||||
|
'description' => 'nullable|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
Department::create($validated);
|
||||||
|
|
||||||
|
return redirect()->to('/admin/departments')
|
||||||
|
->with('success', 'Departemen berhasil ditambahkan.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function edit(Department $department)
|
||||||
|
{
|
||||||
|
return Inertia::render('admin/departments/edit', [
|
||||||
|
'department' => $department
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, Department $department)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'name' => 'required|string|max:255|unique:departments,name,' . $department->id,
|
||||||
|
'description' => 'nullable|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$department->update($validated);
|
||||||
|
|
||||||
|
return redirect()->to('/admin/departments')
|
||||||
|
->with('success', 'Departemen berhasil diperbarui.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(Department $department)
|
||||||
|
{
|
||||||
|
// Cek jika departemen masih punya karyawan
|
||||||
|
if ($department->employees()->count() > 0) {
|
||||||
|
return back()->with('error', 'Gagal hapus! Departemen ini masih memiliki karyawan.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$department->delete();
|
||||||
|
|
||||||
|
return back()->with('success', 'Departemen berhasil dihapus.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,85 +3,123 @@
|
||||||
namespace App\Http\Controllers\Admin;
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Department;
|
||||||
use App\Models\Employee;
|
use App\Models\Employee;
|
||||||
|
use App\Models\Position;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Http\Requests\StoreEmployeeRequest;
|
use Illuminate\Http\Request;
|
||||||
use App\Http\Requests\UpdateEmployeeRequest;
|
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
|
|
||||||
class EmployeeController extends Controller
|
class EmployeeController extends Controller
|
||||||
{
|
{
|
||||||
public function index()
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
$employees = Employee::with('user')
|
$query = Employee::with(['user', 'department', 'position']);
|
||||||
->latest()
|
|
||||||
->paginate(10);
|
if ($request->search) {
|
||||||
|
$query->where('nip', 'like', '%' . $request->search . '%')
|
||||||
|
->orWhereHas('user', function ($q) use ($request) {
|
||||||
|
$q->where('name', 'like', '%' . $request->search . '%');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->department_id) {
|
||||||
|
$query->where('department_id', $request->department_id);
|
||||||
|
}
|
||||||
|
|
||||||
return Inertia::render('admin/employees/index', [
|
return Inertia::render('admin/employees/index', [
|
||||||
'employees' => $employees
|
'employees' => $query->latest()->paginate(10)->withQueryString(),
|
||||||
|
'departments' => Department::all(),
|
||||||
|
'filters' => $request->only(['search', 'department_id']),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function create()
|
public function create()
|
||||||
{
|
{
|
||||||
return Inertia::render('admin/employees/create');
|
return Inertia::render('admin/employees/create', [
|
||||||
|
'departments' => Department::all(),
|
||||||
|
'positions' => Position::all(),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function store(StoreEmployeeRequest $request)
|
public function store(Request $request)
|
||||||
{
|
{
|
||||||
DB::transaction(function () use ($request) {
|
$validated = $request->validate([
|
||||||
$user = User::create([
|
'name' => 'required|string|max:255',
|
||||||
'name' => $request->name,
|
'email' => 'required|email|unique:users,email',
|
||||||
'email' => $request->email,
|
'nip' => 'required|string|unique:employees,nip',
|
||||||
'password' => Hash::make($request->password),
|
'gender' => 'required|in:L,P',
|
||||||
'role' => 'employee',
|
'place_of_birth' => 'nullable|string|max:255',
|
||||||
]);
|
'birth_date' => 'required|date',
|
||||||
|
'address' => 'nullable|string',
|
||||||
|
'phone_number' => 'nullable|string',
|
||||||
|
'department_id' => 'required|exists:departments,id',
|
||||||
|
'position_id' => 'required|exists:positions,id',
|
||||||
|
'status' => 'required|in:PKWT,PKWTT,Magang',
|
||||||
|
'join_date' => 'required|date',
|
||||||
|
]);
|
||||||
|
|
||||||
$user->employee()->create([
|
$user = User::create([
|
||||||
'nip' => $request->nip,
|
'name' => $validated['name'],
|
||||||
'position' => $request->position,
|
'email' => $validated['email'],
|
||||||
'status' => $request->status,
|
'password' => Hash::make('password123'),
|
||||||
'join_date' => $request->join_date,
|
]);
|
||||||
]);
|
|
||||||
});
|
Employee::create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'nip' => $validated['nip'],
|
||||||
|
'name' => $validated['name'],
|
||||||
|
'gender' => $validated['gender'],
|
||||||
|
'place_of_birth' => $validated['place_of_birth'],
|
||||||
|
'birth_date' => $validated['birth_date'],
|
||||||
|
'address' => $validated['address'],
|
||||||
|
'phone_number' => $validated['phone_number'],
|
||||||
|
'department_id' => $validated['department_id'],
|
||||||
|
'position_id' => $validated['position_id'],
|
||||||
|
'status' => $validated['status'],
|
||||||
|
'join_date' => $validated['join_date'],
|
||||||
|
]);
|
||||||
|
|
||||||
return redirect()->route('admin.employees.index')
|
return redirect()->route('admin.employees.index')
|
||||||
->with('success', 'Karyawan berhasil ditambahkan.');
|
->with('success', 'Data karyawan berhasil ditambahkan.');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function edit(Employee $employee)
|
public function edit(Employee $employee)
|
||||||
{
|
{
|
||||||
$employee->load('user');
|
$employee->load(['user', 'department', 'position']);
|
||||||
|
|
||||||
return Inertia::render('admin/employees/edit', [
|
return Inertia::render('admin/employees/edit', [
|
||||||
'employee' => $employee
|
'employee' => $employee,
|
||||||
|
'departments' => Department::all(),
|
||||||
|
'positions' => Position::all(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update(UpdateEmployeeRequest $request, Employee $employee)
|
public function update(Request $request, Employee $employee)
|
||||||
{
|
{
|
||||||
DB::transaction(function () use ($request, $employee) {
|
$validated = $request->validate([
|
||||||
// Update User Data
|
'name' => 'required|string|max:255',
|
||||||
$userData = [
|
'email' => ['required', 'email', Rule::unique('users')->ignore($employee->user_id)],
|
||||||
'name' => $request->name,
|
'nip' => ['required', 'string', Rule::unique('employees')->ignore($employee->id)],
|
||||||
'email' => $request->email,
|
'gender' => 'required|in:L,P',
|
||||||
];
|
'place_of_birth' => 'nullable|string|max:255',
|
||||||
|
'birth_date' => 'required|date',
|
||||||
|
'address' => 'nullable|string',
|
||||||
|
'phone_number' => 'nullable|string',
|
||||||
|
'department_id' => 'required|exists:departments,id',
|
||||||
|
'position_id' => 'required|exists:positions,id',
|
||||||
|
'status' => 'required|in:PKWT,PKWTT,Magang',
|
||||||
|
'join_date' => 'required|date',
|
||||||
|
]);
|
||||||
|
|
||||||
if ($request->filled('password')) {
|
$employee->user->update([
|
||||||
$userData['password'] = Hash::make($request->password);
|
'name' => $validated['name'],
|
||||||
}
|
'email' => $validated['email'],
|
||||||
|
]);
|
||||||
|
|
||||||
$employee->user->update($userData);
|
$employee->update($validated);
|
||||||
|
|
||||||
// Update Employee Data
|
|
||||||
$employee->update([
|
|
||||||
'nip' => $request->nip,
|
|
||||||
'position' => $request->position,
|
|
||||||
'status' => $request->status,
|
|
||||||
'join_date' => $request->join_date,
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
return redirect()->route('admin.employees.index')
|
return redirect()->route('admin.employees.index')
|
||||||
->with('success', 'Data karyawan diperbarui.');
|
->with('success', 'Data karyawan diperbarui.');
|
||||||
|
|
@ -89,7 +127,7 @@ public function update(UpdateEmployeeRequest $request, Employee $employee)
|
||||||
|
|
||||||
public function destroy(Employee $employee)
|
public function destroy(Employee $employee)
|
||||||
{
|
{
|
||||||
$employee->user->delete();
|
$employee->user->delete();
|
||||||
return back()->with('success', 'Karyawan dihapus.');
|
return back()->with('success', 'Karyawan dihapus.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Position;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
|
||||||
|
class PositionController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
return Inertia::render('admin/positions/index', [
|
||||||
|
'positions' => Position::withCount('employees')->latest()->get(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
return Inertia::render('admin/positions/create');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'name' => 'required|string|max:255|unique:positions,name',
|
||||||
|
]);
|
||||||
|
|
||||||
|
Position::create($validated);
|
||||||
|
|
||||||
|
return redirect()->to('/admin/positions')
|
||||||
|
->with('success', 'Jabatan berhasil ditambahkan.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function edit(Position $position)
|
||||||
|
{
|
||||||
|
return Inertia::render('admin/positions/edit', [
|
||||||
|
'position' => $position
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, Position $position)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'name' => 'required|string|max:255|unique:positions,name,' . $position->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$position->update($validated);
|
||||||
|
|
||||||
|
return redirect()->to('/admin/positions')
|
||||||
|
->with('success', 'Jabatan berhasil diperbarui.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(Position $position)
|
||||||
|
{
|
||||||
|
if ($position->employees()->count() > 0) {
|
||||||
|
return back()->with('error', 'Gagal hapus! Masih ada karyawan dengan jabatan ini.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$position->delete();
|
||||||
|
|
||||||
|
return back()->with('success', 'Jabatan berhasil dihapus.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Department;
|
||||||
|
use App\Models\Employee;
|
||||||
|
use App\Models\Position;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
|
||||||
|
class DashboardController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
// Ringkasan Data
|
||||||
|
$stats = [
|
||||||
|
'total_employees' => Employee::count(),
|
||||||
|
'total_departments' => Department::count(),
|
||||||
|
'total_positions' => Position::count(),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Data Grafik Gender (Pie Chart)
|
||||||
|
$genderData = Employee::selectRaw('gender, count(*) as total')
|
||||||
|
->groupBy('gender')
|
||||||
|
->get()
|
||||||
|
->map(fn($item) => [
|
||||||
|
'name' => $item->gender == 'L' ? 'Laki-laki' : 'Perempuan',
|
||||||
|
'value' => $item->total,
|
||||||
|
'fill' => $item->gender == 'L' ? '#3b82f6' : '#ec4899',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Data Grafik Departemen (Bar Chart)
|
||||||
|
$deptData = Department::withCount('employees')
|
||||||
|
->having('employees_count', '>', 0)
|
||||||
|
->get()
|
||||||
|
->map(fn($item) => [
|
||||||
|
'name' => $item->name,
|
||||||
|
'employees' => $item->employees_count,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Karyawan Terbaru (Table)
|
||||||
|
$latestEmployees = Employee::with(['department', 'position', 'user'])
|
||||||
|
->latest('join_date')
|
||||||
|
->take(5)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return Inertia::render('dashboard', [
|
||||||
|
'stats' => $stats,
|
||||||
|
'genderData' => $genderData,
|
||||||
|
'deptData' => $deptData,
|
||||||
|
'latestEmployees' => $latestEmployees,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Department extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = ['name', 'description'];
|
||||||
|
|
||||||
|
public function employees()
|
||||||
|
{
|
||||||
|
return $this->hasMany(Employee::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,23 +2,43 @@
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
||||||
|
|
||||||
class Employee extends Model
|
class Employee extends Model
|
||||||
{
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'user_id',
|
'user_id',
|
||||||
'nip',
|
'nip',
|
||||||
'position',
|
'name',
|
||||||
'status',
|
'gender',
|
||||||
'phone',
|
'place_of_birth',
|
||||||
|
'birth_date',
|
||||||
'address',
|
'address',
|
||||||
|
'phone_number',
|
||||||
|
'department_id',
|
||||||
|
'position_id',
|
||||||
|
'status',
|
||||||
'join_date'
|
'join_date'
|
||||||
];
|
];
|
||||||
|
|
||||||
public function user(): BelongsTo
|
// Relasi ke User
|
||||||
|
public function user()
|
||||||
{
|
{
|
||||||
return $this->belongsTo(User::class);
|
return $this->belongsTo(User::class);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// Relasi ke Departemen
|
||||||
|
public function department()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Department::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Relasi ke Jabatan
|
||||||
|
public function position()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Position::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Position extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = ['name'];
|
||||||
|
|
||||||
|
public function employees()
|
||||||
|
{
|
||||||
|
return $this->hasMany(Employee::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,17 +9,29 @@
|
||||||
/**
|
/**
|
||||||
* Run the migrations.
|
* Run the migrations.
|
||||||
*/
|
*/
|
||||||
public function up(): void
|
public function up()
|
||||||
{
|
{
|
||||||
Schema::create('employees', function (Blueprint $table) {
|
Schema::create('employees', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||||
$table->string('nip')->unique();
|
|
||||||
$table->string('position');
|
// Data Pribadi
|
||||||
$table->enum('status', ['PKWT', 'PKWTT']);
|
$table->string('nip')->unique();
|
||||||
$table->string('phone')->nullable();
|
$table->string('name');
|
||||||
|
$table->enum('gender', ['L', 'P']);
|
||||||
|
$table->string('place_of_birth')->nullable();
|
||||||
|
$table->date('birth_date');
|
||||||
$table->text('address')->nullable();
|
$table->text('address')->nullable();
|
||||||
$table->date('join_date');
|
$table->string('phone_number')->nullable();
|
||||||
|
|
||||||
|
// Data Pekerjaan
|
||||||
|
$table->foreignId('department_id')->constrained()->onDelete('restrict');
|
||||||
|
$table->foreignId('position_id')->constrained()->onDelete('restrict');
|
||||||
|
|
||||||
|
// Status & Tanggal
|
||||||
|
$table->enum('status', ['PKWT', 'PKWTT', 'Magang']);
|
||||||
|
$table->date('join_date');
|
||||||
|
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?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()
|
||||||
|
{
|
||||||
|
Schema::create('departments', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('name');
|
||||||
|
$table->text('description')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('departments');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
<?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()
|
||||||
|
{
|
||||||
|
Schema::create('positions', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('name');
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('positions');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Models\Department;
|
||||||
|
use App\Models\Position;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
|
@ -20,5 +22,17 @@ public function run(): void
|
||||||
'role' => 'admin',
|
'role' => 'admin',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Seed departments
|
||||||
|
Department::create(['name' => 'Information Technology', 'description' => 'Bagian IT & Development']);
|
||||||
|
Department::create(['name' => 'Human Resources', 'description' => 'Bagian Kepegawaian']);
|
||||||
|
Department::create(['name' => 'Finance', 'description' => 'Bagian Keuangan']);
|
||||||
|
Department::create(['name' => 'Marketing', 'description' => 'Bagian Pemasaran']);
|
||||||
|
|
||||||
|
// Seed positions
|
||||||
|
Position::create(['name' => 'General Manager']);
|
||||||
|
Position::create(['name' => 'Manager']);
|
||||||
|
Position::create(['name' => 'Supervisor']);
|
||||||
|
Position::create(['name' => 'Staff']);
|
||||||
|
Position::create(['name' => 'Intern']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -53,12 +53,15 @@
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"laravel-vite-plugin": "^2.0",
|
"laravel-vite-plugin": "^2.0",
|
||||||
"lucide-react": "^0.475.0",
|
"lucide-react": "^0.475.0",
|
||||||
|
"radix-ui": "^1.4.3",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
|
"recharts": "^3.7.0",
|
||||||
"tailwind-merge": "^3.0.1",
|
"tailwind-merge": "^3.0.1",
|
||||||
"tailwindcss": "^4.0.0",
|
"tailwindcss": "^4.0.0",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "^5.7.2",
|
"typescript": "^5.7.2",
|
||||||
|
"use-debounce": "^10.1.0",
|
||||||
"vite": "^7.0.4"
|
"vite": "^7.0.4"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Link } from '@inertiajs/react';
|
import { Link } from '@inertiajs/react';
|
||||||
import { LayoutGrid, Users } from 'lucide-react';
|
import { LayoutGrid, Users, Briefcase, Building2} from 'lucide-react';
|
||||||
import { NavMain } from '@/components/nav-main';
|
import { NavMain } from '@/components/nav-main';
|
||||||
import { NavUser } from '@/components/nav-user';
|
import { NavUser } from '@/components/nav-user';
|
||||||
import {
|
import {
|
||||||
|
|
@ -18,13 +18,23 @@ import AppLogo from './app-logo';
|
||||||
const mainNavItems: NavItem[] = [
|
const mainNavItems: NavItem[] = [
|
||||||
{
|
{
|
||||||
title: 'Dashboard',
|
title: 'Dashboard',
|
||||||
href: dashboard(),
|
href: '/dashboard',
|
||||||
icon: LayoutGrid,
|
icon: LayoutGrid,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Manajemen Karyawan',
|
title: 'Manajemen Karyawan',
|
||||||
href: '/admin/employees',
|
href: '/admin/employees',
|
||||||
icon: Users,
|
icon: Users,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Manajemen Departemen',
|
||||||
|
href: '/admin/departments',
|
||||||
|
icon: Building2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Manajemen Jabatan',
|
||||||
|
href: '/admin/positions',
|
||||||
|
icon: Briefcase,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
import { Head, useForm, Link } from '@inertiajs/react';
|
||||||
|
import React from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
|
import AppLayout from '@/layouts/app-layout';
|
||||||
|
|
||||||
|
export default function Create() {
|
||||||
|
const { data, setData, post, processing, errors } = useForm({
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const submit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
post('/admin/departments');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppLayout>
|
||||||
|
<Head title="Tambah Departemen" />
|
||||||
|
|
||||||
|
<div className="p-4 md:p-8 w-full">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-bold tracking-tight">Tambah Departemen</h2>
|
||||||
|
<p className="text-muted-foreground">Buat divisi baru dalam perusahaan.</p>
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" asChild>
|
||||||
|
<Link href="/admin/departments">Kembali</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Form Departemen</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<Separator />
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<form onSubmit={submit} className="space-y-6">
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Nama Departemen <span className="text-red-500">*</span></Label>
|
||||||
|
<Input
|
||||||
|
placeholder="Contoh: Information Technology"
|
||||||
|
value={data.name}
|
||||||
|
onChange={e => setData('name', e.target.value)}
|
||||||
|
/>
|
||||||
|
{errors.name && <div className="text-red-500 text-xs">{errors.name}</div>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Deskripsi Singkat</Label>
|
||||||
|
<Input
|
||||||
|
placeholder="Keterangan fungsi departemen..."
|
||||||
|
value={data.description}
|
||||||
|
onChange={e => setData('description', e.target.value)}
|
||||||
|
/>
|
||||||
|
{errors.description && <div className="text-red-500 text-xs">{errors.description}</div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-4 pt-4">
|
||||||
|
<Button type="button" variant="ghost" asChild>
|
||||||
|
<Link href="/admin/departments">Batal</Link>
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={processing}>
|
||||||
|
Simpan Departemen
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</AppLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
import { Head, useForm, Link } from '@inertiajs/react';
|
||||||
|
import React from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
|
import AppLayout from '@/layouts/app-layout';
|
||||||
|
|
||||||
|
interface Department {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PageProps {
|
||||||
|
department: Department;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Edit({ department }: PageProps) {
|
||||||
|
const { data, setData, put, processing, errors } = useForm({
|
||||||
|
name: department.name,
|
||||||
|
description: department.description || '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const submit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
put(`/admin/departments/${department.id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppLayout>
|
||||||
|
<Head title="Edit Departemen" />
|
||||||
|
|
||||||
|
<div className="p-4 md:p-8 w-full">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-bold tracking-tight">Edit Departemen</h2>
|
||||||
|
<p className="text-muted-foreground">Perbarui informasi divisi.</p>
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" asChild>
|
||||||
|
<Link href="/admin/departments">Kembali</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Form Edit Departemen</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<Separator />
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<form onSubmit={submit} className="space-y-6">
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Nama Departemen</Label>
|
||||||
|
<Input
|
||||||
|
value={data.name}
|
||||||
|
onChange={e => setData('name', e.target.value)}
|
||||||
|
/>
|
||||||
|
{errors.name && <div className="text-red-500 text-xs">{errors.name}</div>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Deskripsi Singkat</Label>
|
||||||
|
<Input
|
||||||
|
value={data.description}
|
||||||
|
onChange={e => setData('description', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-4 pt-4">
|
||||||
|
<Button type="button" variant="ghost" asChild>
|
||||||
|
<Link href="/admin/departments">Batal</Link>
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={processing}>
|
||||||
|
Simpan Perubahan
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</AppLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,152 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import { Head, Link, usePage } from '@inertiajs/react';
|
||||||
|
import React from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip"; // Import Tooltip
|
||||||
|
import AppLayout from '@/layouts/app-layout';
|
||||||
|
|
||||||
|
interface Department {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
employees_count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PageProps {
|
||||||
|
departments: Department[];
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SharedData {
|
||||||
|
flash: {
|
||||||
|
success?: string;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Index({ departments }: PageProps) {
|
||||||
|
const { flash } = usePage<any>().props as SharedData;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppLayout>
|
||||||
|
<Head title="Manajemen Departemen" />
|
||||||
|
|
||||||
|
<div className="p-4 md:p-8 w-full space-y-4">
|
||||||
|
|
||||||
|
{flash?.success && (
|
||||||
|
<div className="p-4 bg-green-50 text-green-700 border border-green-200 rounded-md text-sm">
|
||||||
|
{flash.success}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{flash?.error && (
|
||||||
|
<div className="p-4 bg-red-50 text-red-700 border border-red-200 rounded-md text-sm">
|
||||||
|
{flash.error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-bold tracking-tight">Data Departemen</h2>
|
||||||
|
<p className="text-muted-foreground">Kelola struktur organisasi dan unit kerja.</p>
|
||||||
|
</div>
|
||||||
|
<Button asChild>
|
||||||
|
<Link href="/admin/departments/create">+ Tambah Departemen</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-4">
|
||||||
|
<CardTitle>Daftar Departemen</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<Separator />
|
||||||
|
<CardContent className="pt-0">
|
||||||
|
<div className="relative w-full overflow-auto">
|
||||||
|
<table className="w-full text-sm text-left">
|
||||||
|
<thead className="text-muted-foreground bg-zinc-50/50">
|
||||||
|
<tr className="border-b">
|
||||||
|
<th className="h-12 px-4 font-medium">Nama Departemen</th>
|
||||||
|
<th className="h-12 px-4 font-medium">Deskripsi</th>
|
||||||
|
<th className="h-12 px-4 font-medium text-center">Total Karyawan</th>
|
||||||
|
<th className="h-12 px-4 font-medium text-right">Aksi</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{departments.length > 0 ? (
|
||||||
|
departments.map((dept) => (
|
||||||
|
<tr key={dept.id} className="border-b hover:bg-zinc-50">
|
||||||
|
<td className="p-4 font-bold text-gray-900">
|
||||||
|
{dept.name}
|
||||||
|
</td>
|
||||||
|
<td className="p-4 text-muted-foreground">
|
||||||
|
{dept.description || '-'}
|
||||||
|
</td>
|
||||||
|
<td className="p-4 text-center">
|
||||||
|
<span className={`inline-flex items-center justify-center px-2.5 py-0.5 rounded-full text-xs font-medium ${dept.employees_count > 0 ? 'bg-blue-100 text-blue-800' : 'bg-gray-100 text-gray-600'}`}>
|
||||||
|
{dept.employees_count}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="p-4 text-right space-x-2">
|
||||||
|
<Button variant="ghost" size="sm" asChild>
|
||||||
|
<Link href={`/admin/departments/${dept.id}/edit`}>Edit</Link>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{dept.employees_count > 0 ? (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<span tabIndex={0} className="cursor-not-allowed inline-block">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
disabled
|
||||||
|
className="text-red-300 hover:text-red-300 hover:bg-transparent opacity-50"
|
||||||
|
>
|
||||||
|
Hapus
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="bg-red-50 text-red-700 border-red-200">
|
||||||
|
<p className="font-semibold">Akses Ditolak</p>
|
||||||
|
<p className="text-xs">Masih ada {dept.employees_count} karyawan aktif di sini.</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
) : (
|
||||||
|
<Link
|
||||||
|
href={`/admin/departments/${dept.id}`}
|
||||||
|
method="delete"
|
||||||
|
as="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
if (!confirm('Hapus departemen ini?')) e.preventDefault();
|
||||||
|
}}
|
||||||
|
className="inline-flex items-center justify-center rounded-md text-sm font-medium h-9 px-3 text-red-600 hover:bg-red-50 transition-colors"
|
||||||
|
>
|
||||||
|
Hapus
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={4} className="p-8 text-center text-muted-foreground">
|
||||||
|
Belum ada data departemen.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</AppLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,96 +1,232 @@
|
||||||
import { Head, useForm, Link } from '@inertiajs/react';
|
import { Head, useForm, Link } from '@inertiajs/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type { FormEvent } from 'react';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
import AppLayout from '@/layouts/app-layout';
|
import AppLayout from '@/layouts/app-layout';
|
||||||
|
|
||||||
export default function Create() {
|
interface PageProps {
|
||||||
|
departments: { id: number; name: string }[];
|
||||||
|
positions: { id: number; name: string }[];
|
||||||
|
errors: Partial<Record<string, string>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Create({ departments, positions }: PageProps) {
|
||||||
const { data, setData, post, processing, errors } = useForm({
|
const { data, setData, post, processing, errors } = useForm({
|
||||||
name: '',
|
name: '',
|
||||||
email: '',
|
email: '',
|
||||||
password: '',
|
nip: '',
|
||||||
nip: '',
|
gender: '',
|
||||||
position: '',
|
birth_date: '',
|
||||||
status: 'PKWT',
|
place_of_birth: '',
|
||||||
|
address: '',
|
||||||
|
phone_number: '',
|
||||||
|
department_id: '',
|
||||||
|
position_id: '',
|
||||||
|
status: '',
|
||||||
join_date: '',
|
join_date: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const submit = (e: FormEvent) => {
|
const submit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
post('/admin/employees');
|
post('/admin/employees');
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppLayout>
|
<AppLayout>
|
||||||
<Head title="Tambah Karyawan" />
|
<Head title="Tambah Karyawan Baru" />
|
||||||
<div className="p-8 max-w-2xl mx-auto">
|
|
||||||
|
<div className="p-4 md:p-8 max-w-5xl mx-auto">
|
||||||
|
<Button variant="outline" asChild className="mb-6">
|
||||||
|
<Link href="/admin/employees">← Kembali</Link>
|
||||||
|
</Button>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Tambah Karyawan</CardTitle>
|
<CardTitle>Form Data Karyawan</CardTitle>
|
||||||
<CardDescription>Masukkan data detail karyawan baru.</CardDescription>
|
<p className="text-sm text-muted-foreground">Lengkapi form di bawah ini untuk menambahkan karyawan baru.</p>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<Separator />
|
||||||
<form onSubmit={submit} className="space-y-4">
|
<CardContent className="pt-6">
|
||||||
<div className="space-y-2">
|
<form onSubmit={submit} className="space-y-8">
|
||||||
<Label>Nama Lengkap</Label>
|
|
||||||
<Input value={data.name} onChange={e => setData('name', e.target.value)} />
|
{/* INFORMASI PRIBADI */}
|
||||||
{errors.name && <p className="text-xs text-red-500">{errors.name}</p>}
|
<div className="space-y-4">
|
||||||
</div>
|
<h3 className="text-lg font-medium flex items-center gap-2">
|
||||||
|
<span className="bg-primary/10 text-primary w-6 h-6 rounded-full flex items-center justify-center text-xs">1</span>
|
||||||
|
Informasi Pribadi
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
{/* Kolom Kiri */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Nama Lengkap</Label>
|
||||||
|
<Input
|
||||||
|
placeholder="Nama sesuai KTP"
|
||||||
|
value={data.name}
|
||||||
|
onChange={e => setData('name', e.target.value)}
|
||||||
|
/>
|
||||||
|
{errors.name && <div className="text-red-500 text-xs">{errors.name}</div>}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Email</Label>
|
<Label>Email</Label>
|
||||||
<Input type="email" value={data.email} onChange={e => setData('email', e.target.value)} />
|
<Input
|
||||||
{errors.email && <p className="text-xs text-red-500">{errors.email}</p>}
|
type="email"
|
||||||
</div>
|
value={data.email}
|
||||||
|
onChange={e => setData('email', e.target.value)}
|
||||||
|
/>
|
||||||
|
{errors.email && <div className="text-red-500 text-xs">{errors.email}</div>}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Password Default</Label>
|
<Label>No. Telepon</Label>
|
||||||
<Input type="password" value={data.password} onChange={e => setData('password', e.target.value)} />
|
<Input
|
||||||
</div>
|
value={data.phone_number}
|
||||||
|
onChange={e => setData('phone_number', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
{/* Kolom Kanan */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-4">
|
||||||
<Label>NIP</Label>
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<Input value={data.nip} onChange={e => setData('nip', e.target.value)} />
|
<div className="space-y-2">
|
||||||
</div>
|
<Label>Tempat Lahir</Label>
|
||||||
<div className="space-y-2">
|
<Input
|
||||||
<Label>Tanggal Masuk</Label>
|
value={data.place_of_birth}
|
||||||
<Input type="date" value={data.join_date} onChange={e => setData('join_date', e.target.value)} />
|
onChange={e => setData('place_of_birth', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Tanggal Lahir</Label>
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
value={data.birth_date}
|
||||||
|
onChange={e => setData('birth_date', e.target.value)}
|
||||||
|
/>
|
||||||
|
{errors.birth_date && <div className="text-red-500 text-xs">{errors.birth_date}</div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Jenis Kelamin</Label>
|
||||||
|
<Select onValueChange={(val) => setData('gender', val)}>
|
||||||
|
<SelectTrigger><SelectValue placeholder="Pilih Gender" /></SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="L">Laki-laki</SelectItem>
|
||||||
|
<SelectItem value="P">Perempuan</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
{errors.gender && <div className="text-red-500 text-xs">{errors.gender}</div>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Alamat Lengkap</Label>
|
||||||
|
<Input
|
||||||
|
value={data.address}
|
||||||
|
onChange={e => setData('address', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<Separator />
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Jabatan</Label>
|
{/* DATA KEPEGAWAIAN */}
|
||||||
<Select onValueChange={(val) => setData('position', val)}>
|
<div className="space-y-4">
|
||||||
<SelectTrigger><SelectValue placeholder="Pilih" /></SelectTrigger>
|
<h3 className="text-lg font-medium flex items-center gap-2">
|
||||||
<SelectContent>
|
<span className="bg-primary/10 text-primary w-6 h-6 rounded-full flex items-center justify-center text-xs">2</span>
|
||||||
<SelectItem value="Staff">Staff</SelectItem>
|
Data Kepegawaian
|
||||||
<SelectItem value="Supervisor">Supervisor</SelectItem>
|
</h3>
|
||||||
<SelectItem value="Manager">Manager</SelectItem>
|
|
||||||
</SelectContent>
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
</Select>
|
{/* Kolom Kiri */}
|
||||||
</div>
|
<div className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Status</Label>
|
<Label>Nomor Induk (NIP)</Label>
|
||||||
<Select onValueChange={(val) => setData('status', val)}>
|
<Input
|
||||||
<SelectTrigger><SelectValue placeholder="Pilih" /></SelectTrigger>
|
placeholder="Contoh: 2024001"
|
||||||
<SelectContent>
|
value={data.nip}
|
||||||
<SelectItem value="PKWT">PKWT</SelectItem>
|
onChange={e => setData('nip', e.target.value)}
|
||||||
<SelectItem value="PKWTT">PKWTT</SelectItem>
|
/>
|
||||||
</SelectContent>
|
{errors.nip && <div className="text-red-500 text-xs">{errors.nip}</div>}
|
||||||
</Select>
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Departemen</Label>
|
||||||
|
<Select onValueChange={(val) => setData('department_id', val)}>
|
||||||
|
<SelectTrigger><SelectValue placeholder="Pilih Departemen" /></SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{departments.map((dept) => (
|
||||||
|
<SelectItem key={dept.id} value={String(dept.id)}>{dept.name}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
{errors.department_id && <div className="text-red-500 text-xs">Wajib diisi</div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Kolom Kanan */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Jabatan</Label>
|
||||||
|
<Select onValueChange={(val) => setData('position_id', val)}>
|
||||||
|
<SelectTrigger><SelectValue placeholder="Pilih Jabatan" /></SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{positions.map((pos) => (
|
||||||
|
<SelectItem key={pos.id} value={String(pos.id)}>{pos.name}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
{errors.position_id && <div className="text-red-500 text-xs">Wajib diisi</div>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Status</Label>
|
||||||
|
<Select onValueChange={(val) => setData('status', val)}>
|
||||||
|
<SelectTrigger><SelectValue placeholder="Pilih Status" /></SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="PKWT">PKWT (Kontrak)</SelectItem>
|
||||||
|
<SelectItem value="PKWTT">PKWTT (Tetap)</SelectItem>
|
||||||
|
<SelectItem value="Magang">Magang</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
{errors.status && <div className="text-red-500 text-xs">{errors.status}</div>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Tanggal Masuk</Label>
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
value={data.join_date}
|
||||||
|
onChange={e => setData('join_date', e.target.value)}
|
||||||
|
/>
|
||||||
|
{errors.join_date && <div className="text-red-500 text-xs">{errors.join_date}</div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end gap-2 pt-4">
|
<Separator />
|
||||||
<Button variant="outline" asChild><Link href="/admin/employees">Batal</Link></Button>
|
|
||||||
<Button type="submit" disabled={processing}>Simpan</Button>
|
{/* TOMBOL AKSI */}
|
||||||
|
<div className="flex justify-end gap-4 pt-2">
|
||||||
|
<Button type="button" variant="ghost" asChild>
|
||||||
|
<Link href="/admin/employees">Batal</Link>
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={processing} className="px-8">
|
||||||
|
Simpan Data
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
||||||
|
|
@ -1,159 +1,247 @@
|
||||||
import { Head, useForm, Link } from '@inertiajs/react';
|
import { Head, useForm, Link } from '@inertiajs/react';
|
||||||
import { Separator } from '@radix-ui/react-separator';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type { FormEvent } from 'react';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
import AppLayout from '@/layouts/app-layout';
|
import AppLayout from '@/layouts/app-layout';
|
||||||
|
|
||||||
// Definisi tipe data props untuk TypeScript
|
// Definisi Tipe Data Employee yang diterima dari Controller
|
||||||
interface EmployeeProps {
|
interface Employee {
|
||||||
employee: {
|
id: number;
|
||||||
id: number;
|
nip: string;
|
||||||
nip: string;
|
gender: string;
|
||||||
position: string;
|
birth_date: string;
|
||||||
status: string;
|
place_of_birth: string;
|
||||||
join_date: string;
|
address: string;
|
||||||
user: {
|
phone_number: string;
|
||||||
name: string;
|
department_id: number;
|
||||||
email: string;
|
position_id: number;
|
||||||
} | null;
|
status: string;
|
||||||
|
join_date: string;
|
||||||
|
user: {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Edit({ employee }: EmployeeProps) {
|
interface PageProps {
|
||||||
// Inisialisasi form dengan data dari database
|
employee: Employee;
|
||||||
|
departments: { id: number; name: string }[];
|
||||||
|
positions: { id: number; name: string }[];
|
||||||
|
errors: Partial<Record<string, string>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Edit({ employee, departments, positions }: PageProps) {
|
||||||
|
// Inisialisasi Form dengan Data Lama (Pre-filled)
|
||||||
const { data, setData, put, processing, errors } = useForm({
|
const { data, setData, put, processing, errors } = useForm({
|
||||||
name: employee.user?.name || '',
|
name: employee.user.name,
|
||||||
email: employee.user?.email || '',
|
email: employee.user.email,
|
||||||
password: '', // Kosongkan untuk keamanan
|
nip: employee.nip,
|
||||||
nip: employee.nip || '',
|
gender: employee.gender,
|
||||||
position: employee.position || '',
|
birth_date: employee.birth_date,
|
||||||
status: employee.status || 'PKWT',
|
place_of_birth: employee.place_of_birth || '',
|
||||||
join_date: employee.join_date || '',
|
address: employee.address || '',
|
||||||
|
phone_number: employee.phone_number || '',
|
||||||
|
department_id: String(employee.department_id),
|
||||||
|
position_id: String(employee.position_id),
|
||||||
|
status: employee.status,
|
||||||
|
join_date: employee.join_date,
|
||||||
});
|
});
|
||||||
|
|
||||||
const submit = (e: FormEvent) => {
|
const submit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
put(`/admin/employees/${employee.id}`);
|
put(`/admin/employees/${employee.id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppLayout>
|
<AppLayout>
|
||||||
<Head title={`Edit Karyawan - ${employee.user?.name}`} />
|
<Head title={`Edit Karyawan: ${employee.user.name}`} />
|
||||||
<div className="p-8 max-w-2xl mx-auto">
|
|
||||||
|
<div className="p-4 md:p-8 max-w-5xl mx-auto">
|
||||||
|
<Button variant="outline" asChild className="mb-6">
|
||||||
|
<Link href="/admin/employees">← Kembali</Link>
|
||||||
|
</Button>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Edit Karyawan</CardTitle>
|
<CardTitle>Edit Data Karyawan</CardTitle>
|
||||||
<CardDescription>
|
<p className="text-sm text-muted-foreground">Perbarui informasi karyawan di bawah ini.</p>
|
||||||
Perbarui informasi data diri dan kepegawaian karyawan.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<Separator />
|
||||||
<form onSubmit={submit} className="space-y-4">
|
<CardContent className="pt-6">
|
||||||
{/* Data Profil */}
|
<form onSubmit={submit} className="space-y-8">
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="name">Nama Lengkap</Label>
|
{/* INFORMASI PRIBADI */}
|
||||||
<Input
|
<div className="space-y-4">
|
||||||
id="name"
|
<h3 className="text-lg font-medium flex items-center gap-2">
|
||||||
value={data.name}
|
<span className="bg-orange-100 text-orange-600 w-6 h-6 rounded-full flex items-center justify-center text-xs">1</span>
|
||||||
onChange={e => setData('name', e.target.value)}
|
Informasi Pribadi
|
||||||
/>
|
</h3>
|
||||||
{errors.name && <p className="text-xs text-red-500">{errors.name}</p>}
|
|
||||||
</div>
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
{/* Kolom Kiri */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Nama Lengkap</Label>
|
||||||
|
<Input
|
||||||
|
value={data.name}
|
||||||
|
onChange={e => setData('name', e.target.value)}
|
||||||
|
/>
|
||||||
|
{errors.name && <div className="text-red-500 text-xs">{errors.name}</div>}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="email">Email</Label>
|
<Label>Email</Label>
|
||||||
<Input
|
<Input
|
||||||
id="email"
|
type="email"
|
||||||
type="email"
|
value={data.email}
|
||||||
value={data.email}
|
onChange={e => setData('email', e.target.value)}
|
||||||
onChange={e => setData('email', e.target.value)}
|
/>
|
||||||
/>
|
{errors.email && <div className="text-red-500 text-xs">{errors.email}</div>}
|
||||||
{errors.email && <p className="text-xs text-red-500">{errors.email}</p>}
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="password">Password Baru (Kosongkan jika tidak diganti)</Label>
|
<Label>No. Telepon</Label>
|
||||||
<Input
|
<Input
|
||||||
id="password"
|
value={data.phone_number}
|
||||||
type="password"
|
onChange={e => setData('phone_number', e.target.value)}
|
||||||
value={data.password}
|
/>
|
||||||
onChange={e => setData('password', e.target.value)}
|
</div>
|
||||||
/>
|
</div>
|
||||||
{errors.password && <p className="text-xs text-red-500">{errors.password}</p>}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Separator className="my-4" />
|
{/* Kolom Kanan */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Tempat Lahir</Label>
|
||||||
|
<Input
|
||||||
|
value={data.place_of_birth}
|
||||||
|
onChange={e => setData('place_of_birth', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Tanggal Lahir</Label>
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
value={data.birth_date}
|
||||||
|
onChange={e => setData('birth_date', e.target.value)}
|
||||||
|
/>
|
||||||
|
{errors.birth_date && <div className="text-red-500 text-xs">{errors.birth_date}</div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Data Kepegawaian */}
|
<div className="space-y-2">
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<Label>Jenis Kelamin</Label>
|
||||||
<div className="space-y-2">
|
<Select value={data.gender} onValueChange={(val) => setData('gender', val)}>
|
||||||
<Label htmlFor="nip">NIP</Label>
|
<SelectTrigger><SelectValue placeholder="Pilih Gender" /></SelectTrigger>
|
||||||
<Input
|
<SelectContent>
|
||||||
id="nip"
|
<SelectItem value="L">Laki-laki</SelectItem>
|
||||||
value={data.nip}
|
<SelectItem value="P">Perempuan</SelectItem>
|
||||||
onChange={e => setData('nip', e.target.value)}
|
</SelectContent>
|
||||||
/>
|
</Select>
|
||||||
{errors.nip && <p className="text-xs text-red-500">{errors.nip}</p>}
|
{errors.gender && <div className="text-red-500 text-xs">{errors.gender}</div>}
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="join_date">Tanggal Masuk</Label>
|
<div className="space-y-2">
|
||||||
<Input
|
<Label>Alamat Lengkap</Label>
|
||||||
id="join_date"
|
<Input
|
||||||
type="date"
|
value={data.address}
|
||||||
value={data.join_date}
|
onChange={e => setData('address', e.target.value)}
|
||||||
onChange={e => setData('join_date', e.target.value)}
|
/>
|
||||||
/>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<Separator />
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Jabatan</Label>
|
{/* DATA KEPEGAWAIAN */}
|
||||||
<Select
|
<div className="space-y-4">
|
||||||
value={data.position}
|
<h3 className="text-lg font-medium flex items-center gap-2">
|
||||||
onValueChange={(val) => setData('position', val)}
|
<span className="bg-orange-100 text-orange-600 w-6 h-6 rounded-full flex items-center justify-center text-xs">2</span>
|
||||||
>
|
Data Kepegawaian
|
||||||
<SelectTrigger>
|
</h3>
|
||||||
<SelectValue placeholder="Pilih Jabatan" />
|
|
||||||
</SelectTrigger>
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<SelectContent>
|
{/* Kolom Kiri */}
|
||||||
<SelectItem value="Staff">Staff</SelectItem>
|
<div className="space-y-4">
|
||||||
<SelectItem value="Supervisor">Supervisor</SelectItem>
|
<div className="space-y-2">
|
||||||
<SelectItem value="Manager">Manager</SelectItem>
|
<Label>Nomor Induk (NIP)</Label>
|
||||||
<SelectItem value="HRD">HRD</SelectItem>
|
<Input
|
||||||
</SelectContent>
|
value={data.nip}
|
||||||
</Select>
|
onChange={e => setData('nip', e.target.value)}
|
||||||
</div>
|
/>
|
||||||
<div className="space-y-2">
|
{errors.nip && <div className="text-red-500 text-xs">{errors.nip}</div>}
|
||||||
<Label>Status Kerja</Label>
|
</div>
|
||||||
<Select
|
|
||||||
value={data.status}
|
<div className="space-y-2">
|
||||||
onValueChange={(val) => setData('status', val)}
|
<Label>Departemen</Label>
|
||||||
>
|
<Select value={data.department_id} onValueChange={(val) => setData('department_id', val)}>
|
||||||
<SelectTrigger>
|
<SelectTrigger><SelectValue placeholder="Pilih Departemen" /></SelectTrigger>
|
||||||
<SelectValue placeholder="Pilih Status" />
|
<SelectContent>
|
||||||
</SelectTrigger>
|
{departments.map((dept) => (
|
||||||
<SelectContent>
|
<SelectItem key={dept.id} value={String(dept.id)}>{dept.name}</SelectItem>
|
||||||
<SelectItem value="PKWT">PKWT</SelectItem>
|
))}
|
||||||
<SelectItem value="PKWTT">PKWTT</SelectItem>
|
</SelectContent>
|
||||||
</SelectContent>
|
</Select>
|
||||||
</Select>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Kolom Kanan */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Jabatan</Label>
|
||||||
|
<Select value={data.position_id} onValueChange={(val) => setData('position_id', val)}>
|
||||||
|
<SelectTrigger><SelectValue placeholder="Pilih Jabatan" /></SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{positions.map((pos) => (
|
||||||
|
<SelectItem key={pos.id} value={String(pos.id)}>{pos.name}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Status</Label>
|
||||||
|
<Select value={data.status} onValueChange={(val) => setData('status', val)}>
|
||||||
|
<SelectTrigger><SelectValue placeholder="Pilih Status" /></SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="PKWT">PKWT (Kontrak)</SelectItem>
|
||||||
|
<SelectItem value="PKWTT">PKWTT (Tetap)</SelectItem>
|
||||||
|
<SelectItem value="Magang">Magang</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Tanggal Masuk</Label>
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
value={data.join_date}
|
||||||
|
onChange={e => setData('join_date', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end gap-2 pt-4">
|
<Separator />
|
||||||
<Button variant="outline" asChild>
|
|
||||||
|
{/* TOMBOL AKSI */}
|
||||||
|
<div className="flex justify-end gap-4 pt-2">
|
||||||
|
<Button type="button" variant="ghost" asChild>
|
||||||
<Link href="/admin/employees">Batal</Link>
|
<Link href="/admin/employees">Batal</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" disabled={processing}>
|
<Button type="submit" disabled={processing} className="px-8">
|
||||||
Simpan Perubahan
|
Perbarui Data
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,34 @@
|
||||||
import { Head, Link, usePage } from '@inertiajs/react';
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import React from 'react';
|
import { Head, Link, usePage, router } from '@inertiajs/react';
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useDebounce } from 'use-debounce';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import AppLayout from '@/layouts/app-layout';
|
import AppLayout from '@/layouts/app-layout';
|
||||||
|
|
||||||
interface Employee {
|
interface Employee {
|
||||||
id: number;
|
id: number;
|
||||||
nip: string;
|
nip: string;
|
||||||
position: string;
|
|
||||||
status: string;
|
status: string;
|
||||||
|
join_date: string;
|
||||||
|
position: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
department: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
user: {
|
user: {
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
|
@ -20,6 +38,12 @@ interface Employee {
|
||||||
interface PageProps {
|
interface PageProps {
|
||||||
employees: {
|
employees: {
|
||||||
data: Employee[];
|
data: Employee[];
|
||||||
|
links: any[];
|
||||||
|
};
|
||||||
|
departments: { id: number; name: string }[];
|
||||||
|
filters: {
|
||||||
|
search?: string;
|
||||||
|
department_id?: string;
|
||||||
};
|
};
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
@ -27,68 +51,136 @@ interface PageProps {
|
||||||
interface SharedData {
|
interface SharedData {
|
||||||
flash: {
|
flash: {
|
||||||
success?: string;
|
success?: string;
|
||||||
|
error?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Index({ employees }: PageProps) {
|
export default function Index({ employees, departments, filters }: PageProps) {
|
||||||
const { flash } = usePage().props as unknown as SharedData;
|
const { flash } = usePage<any>().props as SharedData;
|
||||||
|
const [search, setSearch] = useState(filters.search || '');
|
||||||
|
const [departmentId, setDepartmentId] = useState(filters.department_id || 'all');
|
||||||
|
const [debouncedSearch] = useDebounce(search, 500);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (debouncedSearch !== filters.search || (departmentId !== 'all' && departmentId !== filters.department_id)) {
|
||||||
|
router.get(
|
||||||
|
'/admin/employees',
|
||||||
|
{
|
||||||
|
search: debouncedSearch,
|
||||||
|
department_id: departmentId === 'all' ? '' : departmentId
|
||||||
|
},
|
||||||
|
{ preserveState: true, replace: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [debouncedSearch, departmentId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppLayout>
|
<AppLayout>
|
||||||
<Head title="Manajemen Karyawan" />
|
<Head title="Manajemen Karyawan" />
|
||||||
<div className="p-8">
|
<div className="p-4 md:p-8 space-y-4">
|
||||||
{flash?.success && (
|
{flash?.success && (
|
||||||
<div className="mb-4 p-4 bg-green-50 text-green-700 border rounded-md">
|
<div className="p-4 bg-green-50 text-green-700 border border-green-200 rounded-md text-sm">
|
||||||
{flash.success}
|
{flash.success}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex flex-row items-center justify-between">
|
<CardHeader className="flex flex-row items-center justify-between pb-4">
|
||||||
<CardTitle>Daftar Karyawan</CardTitle>
|
<CardTitle className="text-xl font-bold">Daftar Karyawan</CardTitle>
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
<Link href="/admin/employees/create">Tambah Karyawan</Link>
|
<Link href="/admin/employees/create">+ Tambah Karyawan</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<Separator />
|
<Separator />
|
||||||
<CardContent className="pt-6">
|
|
||||||
<table className="w-full text-sm">
|
<div className="p-4 flex flex-col md:flex-row gap-4">
|
||||||
<thead>
|
<div className="w-full md:w-1/3">
|
||||||
<tr className="text-left border-b">
|
<Input
|
||||||
<th className="pb-4 font-medium">Karyawan</th>
|
placeholder="Cari Nama / NIP..."
|
||||||
<th className="pb-4 font-medium">NIP & Jabatan</th>
|
value={search}
|
||||||
<th className="pb-4 font-medium">Status</th>
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
<th className="pb-4 text-right font-medium">Aksi</th>
|
/>
|
||||||
</tr>
|
</div>
|
||||||
</thead>
|
<div className="w-full md:w-1/4">
|
||||||
<tbody>
|
<Select
|
||||||
{employees.data.map((employee) => (
|
value={departmentId}
|
||||||
<tr key={employee.id} className="border-b">
|
onValueChange={(val) => setDepartmentId(val)}
|
||||||
<td className="py-4">
|
>
|
||||||
<div className="font-bold">{employee.user?.name}</div>
|
<SelectTrigger>
|
||||||
<div className="text-xs text-muted-foreground">{employee.user?.email}</div>
|
<SelectValue placeholder="Filter Departemen" />
|
||||||
</td>
|
</SelectTrigger>
|
||||||
<td className="py-4">
|
<SelectContent>
|
||||||
<div>{employee.nip}</div>
|
<SelectItem value="all">Semua Departemen</SelectItem>
|
||||||
<Badge variant="outline">{employee.position}</Badge>
|
{departments.map((dept) => (
|
||||||
</td>
|
<SelectItem key={dept.id} value={String(dept.id)}>
|
||||||
<td className="py-4">
|
{dept.name}
|
||||||
<Badge variant={employee.status === 'PKWTT' ? 'default' : 'secondary'}>
|
</SelectItem>
|
||||||
{employee.status}
|
))}
|
||||||
</Badge>
|
</SelectContent>
|
||||||
</td>
|
</Select>
|
||||||
<td className="py-4 text-right">
|
</div>
|
||||||
<Button variant="ghost" asChild>
|
</div>
|
||||||
<Link href={`/admin/employees/${employee.id}/edit`}>Edit</Link>
|
|
||||||
</Button>
|
<CardContent className="pt-0">
|
||||||
<Button variant="ghost" className="text-red-600" asChild>
|
<div className="relative w-full overflow-auto">
|
||||||
<Link href={`/admin/employees/${employee.id}`} method="delete" as="button">Hapus</Link>
|
<table className="w-full text-sm text-left">
|
||||||
</Button>
|
<thead className="bg-zinc-50/50">
|
||||||
</td>
|
<tr className="border-b text-muted-foreground">
|
||||||
|
<th className="h-10 px-4 font-medium">Karyawan</th>
|
||||||
|
<th className="h-10 px-4 font-medium">Posisi & Dept</th>
|
||||||
|
<th className="h-10 px-4 font-medium">Status</th>
|
||||||
|
<th className="h-10 px-4 font-medium">Bergabung</th>
|
||||||
|
<th className="h-10 px-4 text-right font-medium">Aksi</th>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
</thead>
|
||||||
</tbody>
|
<tbody>
|
||||||
</table>
|
{employees.data.length > 0 ? (
|
||||||
|
employees.data.map((employee) => (
|
||||||
|
<tr key={employee.id} className="border-b hover:bg-zinc-50">
|
||||||
|
<td className="p-4">
|
||||||
|
<div className="font-bold">{employee.user?.name}</div>
|
||||||
|
<div className="text-xs text-muted-foreground">{employee.user?.email}</div>
|
||||||
|
<div className="text-xs text-zinc-400 mt-1">NIP: {employee.nip}</div>
|
||||||
|
</td>
|
||||||
|
<td className="p-4">
|
||||||
|
<div className="font-medium">{employee.position?.name || '-'}</div>
|
||||||
|
<Badge variant="outline" className="mt-1 font-normal text-xs">
|
||||||
|
{employee.department?.name || '-'}
|
||||||
|
</Badge>
|
||||||
|
</td>
|
||||||
|
<td className="p-4">
|
||||||
|
<Badge className={employee.status === 'PKWTT' ? 'bg-green-600' : 'bg-orange-500'}>
|
||||||
|
{employee.status}
|
||||||
|
</Badge>
|
||||||
|
</td>
|
||||||
|
<td className="p-4 text-muted-foreground">
|
||||||
|
{new Date(employee.join_date).toLocaleDateString('id-ID')}
|
||||||
|
</td>
|
||||||
|
<td className="p-4 text-right space-x-2">
|
||||||
|
<Button variant="ghost" size="sm" asChild>
|
||||||
|
<Link href={`/admin/employees/${employee.id}/edit`}>Edit</Link>
|
||||||
|
</Button>
|
||||||
|
<Link
|
||||||
|
href={`/admin/employees/${employee.id}`}
|
||||||
|
method="delete"
|
||||||
|
as="button"
|
||||||
|
className="inline-flex items-center justify-center rounded-md text-sm font-medium h-9 px-3 text-red-600 hover:bg-red-50"
|
||||||
|
>
|
||||||
|
Hapus
|
||||||
|
</Link>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={5} className="p-8 text-center text-muted-foreground">
|
||||||
|
Tidak ada data ditemukan.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
import { Head, useForm, Link } from '@inertiajs/react';
|
||||||
|
import React from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
|
import AppLayout from '@/layouts/app-layout';
|
||||||
|
|
||||||
|
export default function Create() {
|
||||||
|
const { data, setData, post, processing, errors } = useForm({
|
||||||
|
name: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const submit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
post('/admin/positions');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppLayout>
|
||||||
|
<Head title="Tambah Jabatan" />
|
||||||
|
|
||||||
|
<div className="p-4 md:p-8 w-full">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-bold tracking-tight">Tambah Jabatan</h2>
|
||||||
|
<p className="text-muted-foreground">Buat posisi atau level baru.</p>
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" asChild>
|
||||||
|
<Link href="/admin/positions">Kembali</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Form Jabatan</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<Separator />
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<form onSubmit={submit} className="space-y-6">
|
||||||
|
|
||||||
|
<div className="max-w-xl space-y-2">
|
||||||
|
<Label>Nama Jabatan <span className="text-red-500">*</span></Label>
|
||||||
|
<Input
|
||||||
|
placeholder="Contoh: Senior Backend Developer"
|
||||||
|
value={data.name}
|
||||||
|
onChange={e => setData('name', e.target.value)}
|
||||||
|
/>
|
||||||
|
{errors.name && <div className="text-red-500 text-xs">{errors.name}</div>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-start gap-4 pt-4">
|
||||||
|
<Button type="submit" disabled={processing}>
|
||||||
|
Simpan Jabatan
|
||||||
|
</Button>
|
||||||
|
<Button type="button" variant="ghost" asChild>
|
||||||
|
<Link href="/admin/positions">Batal</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</AppLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
import { Head, useForm, Link } from '@inertiajs/react';
|
||||||
|
import React from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
|
import AppLayout from '@/layouts/app-layout';
|
||||||
|
|
||||||
|
interface Position {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PageProps {
|
||||||
|
position: Position;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Edit({ position }: PageProps) {
|
||||||
|
const { data, setData, put, processing, errors } = useForm({
|
||||||
|
name: position.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
const submit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
put(`/admin/positions/${position.id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppLayout>
|
||||||
|
<Head title="Edit Jabatan" />
|
||||||
|
|
||||||
|
<div className="p-4 md:p-8 w-full">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-bold tracking-tight">Edit Jabatan</h2>
|
||||||
|
<p className="text-muted-foreground">Perbarui nama jabatan.</p>
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" asChild>
|
||||||
|
<Link href="/admin/positions">Kembali</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Form Edit Jabatan</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<Separator />
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<form onSubmit={submit} className="space-y-6">
|
||||||
|
|
||||||
|
<div className="max-w-xl space-y-2">
|
||||||
|
<Label>Nama Jabatan</Label>
|
||||||
|
<Input
|
||||||
|
value={data.name}
|
||||||
|
onChange={e => setData('name', e.target.value)}
|
||||||
|
/>
|
||||||
|
{errors.name && <div className="text-red-500 text-xs">{errors.name}</div>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-start gap-4 pt-4">
|
||||||
|
<Button type="submit" disabled={processing}>
|
||||||
|
Simpan Perubahan
|
||||||
|
</Button>
|
||||||
|
<Button type="button" variant="ghost" asChild>
|
||||||
|
<Link href="/admin/positions">Batal</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</AppLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import { Head, Link, usePage } from '@inertiajs/react';
|
||||||
|
import React from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
|
import AppLayout from '@/layouts/app-layout';
|
||||||
|
|
||||||
|
interface Position {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
employees_count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PageProps {
|
||||||
|
positions: Position[];
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SharedData {
|
||||||
|
flash: {
|
||||||
|
success?: string;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Index({ positions }: PageProps) {
|
||||||
|
const { flash } = usePage<any>().props as SharedData;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppLayout>
|
||||||
|
<Head title="Manajemen Jabatan" />
|
||||||
|
|
||||||
|
<div className="p-4 md:p-8 w-full space-y-4">
|
||||||
|
|
||||||
|
{flash?.success && (
|
||||||
|
<div className="p-4 bg-green-50 text-green-700 border border-green-200 rounded-md text-sm">
|
||||||
|
{flash.success}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{flash?.error && (
|
||||||
|
<div className="p-4 bg-red-50 text-red-700 border border-red-200 rounded-md text-sm">
|
||||||
|
{flash.error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-bold tracking-tight">Data Jabatan</h2>
|
||||||
|
<p className="text-muted-foreground">Kelola level dan posisi pekerjaan.</p>
|
||||||
|
</div>
|
||||||
|
<Button asChild>
|
||||||
|
<Link href="/admin/positions/create">+ Tambah Jabatan</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-4">
|
||||||
|
<CardTitle>Daftar Jabatan</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<Separator />
|
||||||
|
<CardContent className="pt-0">
|
||||||
|
<div className="relative w-full overflow-auto">
|
||||||
|
<table className="w-full text-sm text-left">
|
||||||
|
<thead className="text-muted-foreground bg-zinc-50/50">
|
||||||
|
<tr className="border-b">
|
||||||
|
<th className="h-12 px-4 font-medium">Nama Jabatan</th>
|
||||||
|
<th className="h-12 px-4 font-medium text-center">Total Karyawan</th>
|
||||||
|
<th className="h-12 px-4 font-medium text-right">Aksi</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{positions.length > 0 ? (
|
||||||
|
positions.map((pos) => (
|
||||||
|
<tr key={pos.id} className="border-b hover:bg-zinc-50">
|
||||||
|
<td className="p-4 font-bold text-gray-900">
|
||||||
|
{pos.name}
|
||||||
|
</td>
|
||||||
|
<td className="p-4 text-center">
|
||||||
|
<span className={`inline-flex items-center justify-center px-2.5 py-0.5 rounded-full text-xs font-medium ${pos.employees_count > 0 ? 'bg-blue-100 text-blue-800' : 'bg-gray-100 text-gray-600'}`}>
|
||||||
|
{pos.employees_count}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="p-4 text-right space-x-2">
|
||||||
|
<Button variant="ghost" size="sm" asChild>
|
||||||
|
<Link href={`/admin/positions/${pos.id}/edit`}>Edit</Link>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{pos.employees_count > 0 ? (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<span tabIndex={0} className="cursor-not-allowed inline-block">
|
||||||
|
<Button variant="ghost" size="sm" disabled className="text-red-300 opacity-50">Hapus</Button>
|
||||||
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="bg-red-50 text-red-700 border-red-200">
|
||||||
|
<p>Tidak bisa dihapus: Digunakan oleh {pos.employees_count} karyawan.</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
) : (
|
||||||
|
<Link
|
||||||
|
href={`/admin/positions/${pos.id}`}
|
||||||
|
method="delete"
|
||||||
|
as="button"
|
||||||
|
onClick={(e) => { if (!confirm('Hapus jabatan ini?')) e.preventDefault(); }}
|
||||||
|
className="inline-flex items-center justify-center rounded-md text-sm font-medium h-9 px-3 text-red-600 hover:bg-red-50"
|
||||||
|
>
|
||||||
|
Hapus
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={3} className="p-8 text-center text-muted-foreground">
|
||||||
|
Belum ada data jabatan.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</AppLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,36 +1,161 @@
|
||||||
import { Head } from '@inertiajs/react';
|
import { Head, Link } from '@inertiajs/react';
|
||||||
import { PlaceholderPattern } from '@/components/ui/placeholder-pattern';
|
|
||||||
import AppLayout from '@/layouts/app-layout';
|
import AppLayout from '@/layouts/app-layout';
|
||||||
import { dashboard } from '@/routes';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import type { BreadcrumbItem } from '@/types';
|
import { Users, Building2, Briefcase, UserPlus } from 'lucide-react';
|
||||||
|
import {
|
||||||
|
BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, ResponsiveContainer,
|
||||||
|
PieChart, Pie, Cell, Legend
|
||||||
|
} from 'recharts';
|
||||||
|
|
||||||
const breadcrumbs: BreadcrumbItem[] = [
|
interface DashboardProps {
|
||||||
{
|
stats: {
|
||||||
title: 'Dashboard',
|
total_employees: number;
|
||||||
href: dashboard().url,
|
total_departments: number;
|
||||||
},
|
total_positions: number;
|
||||||
];
|
};
|
||||||
|
genderData: any[];
|
||||||
|
deptData: any[];
|
||||||
|
latestEmployees: any[];
|
||||||
|
}
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function Dashboard({ stats, genderData, deptData, latestEmployees }: DashboardProps) {
|
||||||
return (
|
return (
|
||||||
<AppLayout breadcrumbs={breadcrumbs}>
|
<AppLayout breadcrumbs={[{ title: 'Dashboard', href: '/dashboard' }]}>
|
||||||
<Head title="Dashboard" />
|
<Head title="Dashboard" />
|
||||||
<div className="flex h-full flex-1 flex-col gap-4 overflow-x-auto rounded-xl p-4">
|
|
||||||
<div className="grid auto-rows-min gap-4 md:grid-cols-3">
|
<div className="flex flex-1 flex-col gap-4 p-4 md:p-8 pt-0">
|
||||||
<div className="relative aspect-video overflow-hidden rounded-xl border border-sidebar-border/70 dark:border-sidebar-border">
|
|
||||||
<PlaceholderPattern className="absolute inset-0 size-full stroke-neutral-900/20 dark:stroke-neutral-100/20" />
|
{/* CARD STATISTIK */}
|
||||||
</div>
|
<div className="grid gap-4 md:grid-cols-3">
|
||||||
<div className="relative aspect-video overflow-hidden rounded-xl border border-sidebar-border/70 dark:border-sidebar-border">
|
<Card>
|
||||||
<PlaceholderPattern className="absolute inset-0 size-full stroke-neutral-900/20 dark:stroke-neutral-100/20" />
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
</div>
|
<CardTitle className="text-sm font-medium">Total Karyawan</CardTitle>
|
||||||
<div className="relative aspect-video overflow-hidden rounded-xl border border-sidebar-border/70 dark:border-sidebar-border">
|
<Users className="h-4 w-4 text-muted-foreground" />
|
||||||
<PlaceholderPattern className="absolute inset-0 size-full stroke-neutral-900/20 dark:stroke-neutral-100/20" />
|
</CardHeader>
|
||||||
</div>
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{stats.total_employees}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">Pegawai aktif saat ini</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Total Departemen</CardTitle>
|
||||||
|
<Building2 className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{stats.total_departments}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">Divisi terdaftar</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Total Jabatan</CardTitle>
|
||||||
|
<Briefcase className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{stats.total_positions}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">Posisi pekerjaan</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative min-h-[100vh] flex-1 overflow-hidden rounded-xl border border-sidebar-border/70 md:min-h-min dark:border-sidebar-border">
|
|
||||||
<PlaceholderPattern className="absolute inset-0 size-full stroke-neutral-900/20 dark:stroke-neutral-100/20" />
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
|
||||||
|
|
||||||
|
{/* Grafik Batang */}
|
||||||
|
<Card className="col-span-4">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Karyawan per Departemen</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="pl-2">
|
||||||
|
<div className="h-[300px] w-full">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<BarChart data={deptData}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" vertical={false} />
|
||||||
|
<XAxis
|
||||||
|
dataKey="name"
|
||||||
|
stroke="#888888"
|
||||||
|
fontSize={12}
|
||||||
|
tickLine={false}
|
||||||
|
axisLine={false}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
stroke="#888888"
|
||||||
|
fontSize={12}
|
||||||
|
tickLine={false}
|
||||||
|
axisLine={false}
|
||||||
|
tickFormatter={(value) => `${value}`}
|
||||||
|
/>
|
||||||
|
<RechartsTooltip
|
||||||
|
cursor={{ fill: 'transparent' }}
|
||||||
|
contentStyle={{ borderRadius: '8px', border: 'none', boxShadow: '0 4px 12px rgba(0,0,0,0.1)' }}
|
||||||
|
/>
|
||||||
|
<Bar dataKey="employees" fill="#0f172a" radius={[4, 4, 0, 0]} barSize={40} />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Grafik Donat*/}
|
||||||
|
<Card className="col-span-3">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Komposisi Gender</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="h-[300px] w-full">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<PieChart>
|
||||||
|
<Pie
|
||||||
|
data={genderData}
|
||||||
|
cx="50%"
|
||||||
|
cy="50%"
|
||||||
|
innerRadius={60}
|
||||||
|
outerRadius={80}
|
||||||
|
paddingAngle={5}
|
||||||
|
dataKey="value"
|
||||||
|
>
|
||||||
|
{genderData.map((entry, index) => (
|
||||||
|
<Cell key={`cell-${index}`} fill={entry.fill} />
|
||||||
|
))}
|
||||||
|
</Pie>
|
||||||
|
<RechartsTooltip />
|
||||||
|
<Legend verticalAlign="bottom" height={36}/>
|
||||||
|
</PieChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* TABEL KARYAWAN TERBARU */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between">
|
||||||
|
<CardTitle>Karyawan Terbaru Bergabung</CardTitle>
|
||||||
|
<Link href="/admin/employees" className="text-sm text-blue-600 hover:underline">
|
||||||
|
Lihat Semua
|
||||||
|
</Link>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-8">
|
||||||
|
{latestEmployees.map((emp) => (
|
||||||
|
<div key={emp.id} className="flex items-center">
|
||||||
|
<div className="h-9 w-9 rounded-full bg-slate-100 flex items-center justify-center border">
|
||||||
|
<UserPlus className="h-5 w-5 text-slate-500" />
|
||||||
|
</div>
|
||||||
|
<div className="ml-4 space-y-1">
|
||||||
|
<p className="text-sm font-medium leading-none">{emp.user.name}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">{emp.position.name} • {emp.department.name}</p>
|
||||||
|
</div>
|
||||||
|
<div className="ml-auto font-medium text-xs text-muted-foreground">
|
||||||
|
{new Date(emp.join_date).toLocaleDateString('id-ID', { day: 'numeric', month: 'short', year: 'numeric' })}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Controllers\Admin\DepartmentController;
|
||||||
use App\Http\Controllers\Admin\EmployeeController;
|
use App\Http\Controllers\Admin\EmployeeController;
|
||||||
|
use App\Http\Controllers\Admin\PositionController;
|
||||||
|
use App\Http\Controllers\DashboardController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
use Laravel\Fortify\Features;
|
use Laravel\Fortify\Features;
|
||||||
|
|
@ -11,11 +14,13 @@
|
||||||
]);
|
]);
|
||||||
})->name('home');
|
})->name('home');
|
||||||
|
|
||||||
Route::get('dashboard', function () {
|
Route::get('dashboard', [DashboardController::class, 'index'])
|
||||||
return Inertia::render('dashboard');
|
->middleware(['auth', 'verified'])
|
||||||
})->middleware(['auth', 'verified'])->name('dashboard');
|
->name('dashboard');
|
||||||
|
|
||||||
Route::middleware(['auth', 'verified'])->prefix('admin')->name('admin.')->group(function () {
|
Route::middleware(['auth', 'verified'])->prefix('admin')->name('admin.')->group(function () {
|
||||||
|
Route::resource('departments', DepartmentController::class);
|
||||||
|
Route::resource('positions', PositionController::class);
|
||||||
Route::resource('employees', EmployeeController::class);
|
Route::resource('employees', EmployeeController::class);
|
||||||
});
|
});
|
||||||
require __DIR__.'/settings.php';
|
require __DIR__.'/settings.php';
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue