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;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Department;
|
||||
use App\Models\Employee;
|
||||
use App\Models\Position;
|
||||
use App\Models\User;
|
||||
use App\Http\Requests\StoreEmployeeRequest;
|
||||
use App\Http\Requests\UpdateEmployeeRequest;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class EmployeeController extends Controller
|
||||
{
|
||||
public function index()
|
||||
public function index(Request $request)
|
||||
{
|
||||
$employees = Employee::with('user')
|
||||
->latest()
|
||||
->paginate(10);
|
||||
$query = Employee::with(['user', 'department', 'position']);
|
||||
|
||||
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', [
|
||||
'employees' => $employees
|
||||
'employees' => $query->latest()->paginate(10)->withQueryString(),
|
||||
'departments' => Department::all(),
|
||||
'filters' => $request->only(['search', 'department_id']),
|
||||
]);
|
||||
}
|
||||
|
||||
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) {
|
||||
$user = User::create([
|
||||
'name' => $request->name,
|
||||
'email' => $request->email,
|
||||
'password' => Hash::make($request->password),
|
||||
'role' => 'employee',
|
||||
]);
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'email' => 'required|email|unique:users,email',
|
||||
'nip' => 'required|string|unique:employees,nip',
|
||||
'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',
|
||||
]);
|
||||
|
||||
$user->employee()->create([
|
||||
'nip' => $request->nip,
|
||||
'position' => $request->position,
|
||||
'status' => $request->status,
|
||||
'join_date' => $request->join_date,
|
||||
]);
|
||||
});
|
||||
$user = User::create([
|
||||
'name' => $validated['name'],
|
||||
'email' => $validated['email'],
|
||||
'password' => Hash::make('password123'),
|
||||
]);
|
||||
|
||||
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')
|
||||
->with('success', 'Karyawan berhasil ditambahkan.');
|
||||
->with('success', 'Data karyawan berhasil ditambahkan.');
|
||||
}
|
||||
|
||||
public function edit(Employee $employee)
|
||||
{
|
||||
$employee->load('user');
|
||||
$employee->load(['user', 'department', 'position']);
|
||||
|
||||
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) {
|
||||
// Update User Data
|
||||
$userData = [
|
||||
'name' => $request->name,
|
||||
'email' => $request->email,
|
||||
];
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'email' => ['required', 'email', Rule::unique('users')->ignore($employee->user_id)],
|
||||
'nip' => ['required', 'string', Rule::unique('employees')->ignore($employee->id)],
|
||||
'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')) {
|
||||
$userData['password'] = Hash::make($request->password);
|
||||
}
|
||||
$employee->user->update([
|
||||
'name' => $validated['name'],
|
||||
'email' => $validated['email'],
|
||||
]);
|
||||
|
||||
$employee->user->update($userData);
|
||||
|
||||
// Update Employee Data
|
||||
$employee->update([
|
||||
'nip' => $request->nip,
|
||||
'position' => $request->position,
|
||||
'status' => $request->status,
|
||||
'join_date' => $request->join_date,
|
||||
]);
|
||||
});
|
||||
$employee->update($validated);
|
||||
|
||||
return redirect()->route('admin.employees.index')
|
||||
->with('success', 'Data karyawan diperbarui.');
|
||||
|
|
@ -89,7 +127,7 @@ public function update(UpdateEmployeeRequest $request, Employee $employee)
|
|||
|
||||
public function destroy(Employee $employee)
|
||||
{
|
||||
$employee->user->delete();
|
||||
$employee->user->delete();
|
||||
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;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Employee extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'nip',
|
||||
'position',
|
||||
'status',
|
||||
'phone',
|
||||
'name',
|
||||
'gender',
|
||||
'place_of_birth',
|
||||
'birth_date',
|
||||
'address',
|
||||
'phone_number',
|
||||
'department_id',
|
||||
'position_id',
|
||||
'status',
|
||||
'join_date'
|
||||
];
|
||||
|
||||
public function user(): BelongsTo
|
||||
// Relasi ke User
|
||||
public function user()
|
||||
{
|
||||
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.
|
||||
*/
|
||||
public function up(): void
|
||||
public function up()
|
||||
{
|
||||
Schema::create('employees', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||
$table->string('nip')->unique();
|
||||
$table->string('position');
|
||||
$table->enum('status', ['PKWT', 'PKWTT']);
|
||||
$table->string('phone')->nullable();
|
||||
|
||||
// Data Pribadi
|
||||
$table->string('nip')->unique();
|
||||
$table->string('name');
|
||||
$table->enum('gender', ['L', 'P']);
|
||||
$table->string('place_of_birth')->nullable();
|
||||
$table->date('birth_date');
|
||||
$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();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
use App\Models\Department;
|
||||
use App\Models\Position;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
|
@ -20,5 +22,17 @@ public function run(): void
|
|||
'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",
|
||||
"laravel-vite-plugin": "^2.0",
|
||||
"lucide-react": "^0.475.0",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"recharts": "^3.7.0",
|
||||
"tailwind-merge": "^3.0.1",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5.7.2",
|
||||
"use-debounce": "^10.1.0",
|
||||
"vite": "^7.0.4"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
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 { NavUser } from '@/components/nav-user';
|
||||
import {
|
||||
|
|
@ -18,13 +18,23 @@ import AppLogo from './app-logo';
|
|||
const mainNavItems: NavItem[] = [
|
||||
{
|
||||
title: 'Dashboard',
|
||||
href: dashboard(),
|
||||
href: '/dashboard',
|
||||
icon: LayoutGrid,
|
||||
},
|
||||
{
|
||||
title: 'Manajemen Karyawan',
|
||||
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 React from 'react';
|
||||
import type { FormEvent } from 'react';
|
||||
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 { 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';
|
||||
|
||||
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({
|
||||
name: '',
|
||||
email: '',
|
||||
password: '',
|
||||
nip: '',
|
||||
position: '',
|
||||
status: 'PKWT',
|
||||
name: '',
|
||||
email: '',
|
||||
nip: '',
|
||||
gender: '',
|
||||
birth_date: '',
|
||||
place_of_birth: '',
|
||||
address: '',
|
||||
phone_number: '',
|
||||
department_id: '',
|
||||
position_id: '',
|
||||
status: '',
|
||||
join_date: '',
|
||||
});
|
||||
|
||||
const submit = (e: FormEvent) => {
|
||||
const submit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
post('/admin/employees');
|
||||
};
|
||||
|
||||
return (
|
||||
<AppLayout>
|
||||
<Head title="Tambah Karyawan" />
|
||||
<div className="p-8 max-w-2xl mx-auto">
|
||||
<Head title="Tambah Karyawan Baru" />
|
||||
|
||||
<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>
|
||||
<CardHeader>
|
||||
<CardTitle>Tambah Karyawan</CardTitle>
|
||||
<CardDescription>Masukkan data detail karyawan baru.</CardDescription>
|
||||
<CardTitle>Form Data Karyawan</CardTitle>
|
||||
<p className="text-sm text-muted-foreground">Lengkapi form di bawah ini untuk menambahkan karyawan baru.</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={submit} 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 && <p className="text-xs text-red-500">{errors.name}</p>}
|
||||
</div>
|
||||
<Separator />
|
||||
<CardContent className="pt-6">
|
||||
<form onSubmit={submit} className="space-y-8">
|
||||
|
||||
{/* INFORMASI PRIBADI */}
|
||||
<div className="space-y-4">
|
||||
<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">
|
||||
<Label>Email</Label>
|
||||
<Input type="email" value={data.email} onChange={e => setData('email', e.target.value)} />
|
||||
{errors.email && <p className="text-xs text-red-500">{errors.email}</p>}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Email</Label>
|
||||
<Input
|
||||
type="email"
|
||||
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">
|
||||
<Label>Password Default</Label>
|
||||
<Input type="password" value={data.password} onChange={e => setData('password', e.target.value)} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>No. Telepon</Label>
|
||||
<Input
|
||||
value={data.phone_number}
|
||||
onChange={e => setData('phone_number', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>NIP</Label>
|
||||
<Input value={data.nip} onChange={e => setData('nip', e.target.value)} />
|
||||
</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)} />
|
||||
{/* 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>
|
||||
|
||||
<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 className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Jabatan</Label>
|
||||
<Select onValueChange={(val) => setData('position', val)}>
|
||||
<SelectTrigger><SelectValue placeholder="Pilih" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Staff">Staff</SelectItem>
|
||||
<SelectItem value="Supervisor">Supervisor</SelectItem>
|
||||
<SelectItem value="Manager">Manager</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Status</Label>
|
||||
<Select onValueChange={(val) => setData('status', val)}>
|
||||
<SelectTrigger><SelectValue placeholder="Pilih" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="PKWT">PKWT</SelectItem>
|
||||
<SelectItem value="PKWTT">PKWTT</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Separator />
|
||||
|
||||
{/* DATA KEPEGAWAIAN */}
|
||||
<div className="space-y-4">
|
||||
<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">2</span>
|
||||
Data Kepegawaian
|
||||
</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>Nomor Induk (NIP)</Label>
|
||||
<Input
|
||||
placeholder="Contoh: 2024001"
|
||||
value={data.nip}
|
||||
onChange={e => setData('nip', e.target.value)}
|
||||
/>
|
||||
{errors.nip && <div className="text-red-500 text-xs">{errors.nip}</div>}
|
||||
</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 className="flex justify-end gap-2 pt-4">
|
||||
<Button variant="outline" asChild><Link href="/admin/employees">Batal</Link></Button>
|
||||
<Button type="submit" disabled={processing}>Simpan</Button>
|
||||
<Separator />
|
||||
|
||||
{/* 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>
|
||||
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -1,159 +1,247 @@
|
|||
import { Head, useForm, Link } from '@inertiajs/react';
|
||||
import { Separator } from '@radix-ui/react-separator';
|
||||
import React from 'react';
|
||||
import type { FormEvent } from 'react';
|
||||
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 { 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';
|
||||
|
||||
// Definisi tipe data props untuk TypeScript
|
||||
interface EmployeeProps {
|
||||
employee: {
|
||||
id: number;
|
||||
nip: string;
|
||||
position: string;
|
||||
status: string;
|
||||
join_date: string;
|
||||
user: {
|
||||
name: string;
|
||||
email: string;
|
||||
} | null;
|
||||
// Definisi Tipe Data Employee yang diterima dari Controller
|
||||
interface Employee {
|
||||
id: number;
|
||||
nip: string;
|
||||
gender: string;
|
||||
birth_date: string;
|
||||
place_of_birth: string;
|
||||
address: string;
|
||||
phone_number: string;
|
||||
department_id: number;
|
||||
position_id: number;
|
||||
status: string;
|
||||
join_date: string;
|
||||
user: {
|
||||
name: string;
|
||||
email: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default function Edit({ employee }: EmployeeProps) {
|
||||
// Inisialisasi form dengan data dari database
|
||||
interface PageProps {
|
||||
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({
|
||||
name: employee.user?.name || '',
|
||||
email: employee.user?.email || '',
|
||||
password: '', // Kosongkan untuk keamanan
|
||||
nip: employee.nip || '',
|
||||
position: employee.position || '',
|
||||
status: employee.status || 'PKWT',
|
||||
join_date: employee.join_date || '',
|
||||
name: employee.user.name,
|
||||
email: employee.user.email,
|
||||
nip: employee.nip,
|
||||
gender: employee.gender,
|
||||
birth_date: employee.birth_date,
|
||||
place_of_birth: employee.place_of_birth || '',
|
||||
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();
|
||||
put(`/admin/employees/${employee.id}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<AppLayout>
|
||||
<Head title={`Edit Karyawan - ${employee.user?.name}`} />
|
||||
<div className="p-8 max-w-2xl mx-auto">
|
||||
<Head title={`Edit Karyawan: ${employee.user.name}`} />
|
||||
|
||||
<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>
|
||||
<CardHeader>
|
||||
<CardTitle>Edit Karyawan</CardTitle>
|
||||
<CardDescription>
|
||||
Perbarui informasi data diri dan kepegawaian karyawan.
|
||||
</CardDescription>
|
||||
<CardTitle>Edit Data Karyawan</CardTitle>
|
||||
<p className="text-sm text-muted-foreground">Perbarui informasi karyawan di bawah ini.</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={submit} className="space-y-4">
|
||||
{/* Data Profil */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name">Nama Lengkap</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={data.name}
|
||||
onChange={e => setData('name', e.target.value)}
|
||||
/>
|
||||
{errors.name && <p className="text-xs text-red-500">{errors.name}</p>}
|
||||
</div>
|
||||
<Separator />
|
||||
<CardContent className="pt-6">
|
||||
<form onSubmit={submit} className="space-y-8">
|
||||
|
||||
{/* INFORMASI PRIBADI */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium flex items-center gap-2">
|
||||
<span className="bg-orange-100 text-orange-600 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
|
||||
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 htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={data.email}
|
||||
onChange={e => setData('email', e.target.value)}
|
||||
/>
|
||||
{errors.email && <p className="text-xs text-red-500">{errors.email}</p>}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Email</Label>
|
||||
<Input
|
||||
type="email"
|
||||
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">
|
||||
<Label htmlFor="password">Password Baru (Kosongkan jika tidak diganti)</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
value={data.password}
|
||||
onChange={e => setData('password', e.target.value)}
|
||||
/>
|
||||
{errors.password && <p className="text-xs text-red-500">{errors.password}</p>}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>No. Telepon</Label>
|
||||
<Input
|
||||
value={data.phone_number}
|
||||
onChange={e => setData('phone_number', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</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="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="nip">NIP</Label>
|
||||
<Input
|
||||
id="nip"
|
||||
value={data.nip}
|
||||
onChange={e => setData('nip', e.target.value)}
|
||||
/>
|
||||
{errors.nip && <p className="text-xs text-red-500">{errors.nip}</p>}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="join_date">Tanggal Masuk</Label>
|
||||
<Input
|
||||
id="join_date"
|
||||
type="date"
|
||||
value={data.join_date}
|
||||
onChange={e => setData('join_date', e.target.value)}
|
||||
/>
|
||||
<div className="space-y-2">
|
||||
<Label>Jenis Kelamin</Label>
|
||||
<Select value={data.gender} 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 className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Jabatan</Label>
|
||||
<Select
|
||||
value={data.position}
|
||||
onValueChange={(val) => setData('position', val)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Pilih Jabatan" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Staff">Staff</SelectItem>
|
||||
<SelectItem value="Supervisor">Supervisor</SelectItem>
|
||||
<SelectItem value="Manager">Manager</SelectItem>
|
||||
<SelectItem value="HRD">HRD</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Status Kerja</Label>
|
||||
<Select
|
||||
value={data.status}
|
||||
onValueChange={(val) => setData('status', val)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Pilih Status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="PKWT">PKWT</SelectItem>
|
||||
<SelectItem value="PKWTT">PKWTT</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Separator />
|
||||
|
||||
{/* DATA KEPEGAWAIAN */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium flex items-center gap-2">
|
||||
<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
|
||||
</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>Nomor Induk (NIP)</Label>
|
||||
<Input
|
||||
value={data.nip}
|
||||
onChange={e => setData('nip', e.target.value)}
|
||||
/>
|
||||
{errors.nip && <div className="text-red-500 text-xs">{errors.nip}</div>}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Departemen</Label>
|
||||
<Select value={data.department_id} 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>
|
||||
</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 className="flex justify-end gap-2 pt-4">
|
||||
<Button variant="outline" asChild>
|
||||
<Separator />
|
||||
|
||||
{/* 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}>
|
||||
Simpan Perubahan
|
||||
<Button type="submit" disabled={processing} className="px-8">
|
||||
Perbarui Data
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,34 @@
|
|||
import { Head, Link, usePage } from '@inertiajs/react';
|
||||
import React from 'react';
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
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 { Button } from '@/components/ui/button';
|
||||
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 AppLayout from '@/layouts/app-layout';
|
||||
|
||||
interface Employee {
|
||||
id: number;
|
||||
nip: string;
|
||||
position: string;
|
||||
status: string;
|
||||
join_date: string;
|
||||
position: {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
department: {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
user: {
|
||||
name: string;
|
||||
email: string;
|
||||
|
|
@ -20,6 +38,12 @@ interface Employee {
|
|||
interface PageProps {
|
||||
employees: {
|
||||
data: Employee[];
|
||||
links: any[];
|
||||
};
|
||||
departments: { id: number; name: string }[];
|
||||
filters: {
|
||||
search?: string;
|
||||
department_id?: string;
|
||||
};
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
|
@ -27,68 +51,136 @@ interface PageProps {
|
|||
interface SharedData {
|
||||
flash: {
|
||||
success?: string;
|
||||
error?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default function Index({ employees }: PageProps) {
|
||||
const { flash } = usePage().props as unknown as SharedData;
|
||||
export default function Index({ employees, departments, filters }: PageProps) {
|
||||
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 (
|
||||
<AppLayout>
|
||||
<Head title="Manajemen Karyawan" />
|
||||
<div className="p-8">
|
||||
<div className="p-4 md:p-8 space-y-4">
|
||||
{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}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardTitle>Daftar Karyawan</CardTitle>
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-4">
|
||||
<CardTitle className="text-xl font-bold">Daftar Karyawan</CardTitle>
|
||||
<Button asChild>
|
||||
<Link href="/admin/employees/create">Tambah Karyawan</Link>
|
||||
<Link href="/admin/employees/create">+ Tambah Karyawan</Link>
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<Separator />
|
||||
<CardContent className="pt-6">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="text-left border-b">
|
||||
<th className="pb-4 font-medium">Karyawan</th>
|
||||
<th className="pb-4 font-medium">NIP & Jabatan</th>
|
||||
<th className="pb-4 font-medium">Status</th>
|
||||
<th className="pb-4 text-right font-medium">Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{employees.data.map((employee) => (
|
||||
<tr key={employee.id} className="border-b">
|
||||
<td className="py-4">
|
||||
<div className="font-bold">{employee.user?.name}</div>
|
||||
<div className="text-xs text-muted-foreground">{employee.user?.email}</div>
|
||||
</td>
|
||||
<td className="py-4">
|
||||
<div>{employee.nip}</div>
|
||||
<Badge variant="outline">{employee.position}</Badge>
|
||||
</td>
|
||||
<td className="py-4">
|
||||
<Badge variant={employee.status === 'PKWTT' ? 'default' : 'secondary'}>
|
||||
{employee.status}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="py-4 text-right">
|
||||
<Button variant="ghost" asChild>
|
||||
<Link href={`/admin/employees/${employee.id}/edit`}>Edit</Link>
|
||||
</Button>
|
||||
<Button variant="ghost" className="text-red-600" asChild>
|
||||
<Link href={`/admin/employees/${employee.id}`} method="delete" as="button">Hapus</Link>
|
||||
</Button>
|
||||
</td>
|
||||
|
||||
<div className="p-4 flex flex-col md:flex-row gap-4">
|
||||
<div className="w-full md:w-1/3">
|
||||
<Input
|
||||
placeholder="Cari Nama / NIP..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full md:w-1/4">
|
||||
<Select
|
||||
value={departmentId}
|
||||
onValueChange={(val) => setDepartmentId(val)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Filter Departemen" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Semua Departemen</SelectItem>
|
||||
{departments.map((dept) => (
|
||||
<SelectItem key={dept.id} value={String(dept.id)}>
|
||||
{dept.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CardContent className="pt-0">
|
||||
<div className="relative w-full overflow-auto">
|
||||
<table className="w-full text-sm text-left">
|
||||
<thead className="bg-zinc-50/50">
|
||||
<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>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
{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>
|
||||
</Card>
|
||||
</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 { PlaceholderPattern } from '@/components/ui/placeholder-pattern';
|
||||
import { Head, Link } from '@inertiajs/react';
|
||||
import AppLayout from '@/layouts/app-layout';
|
||||
import { dashboard } from '@/routes';
|
||||
import type { BreadcrumbItem } from '@/types';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
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[] = [
|
||||
{
|
||||
title: 'Dashboard',
|
||||
href: dashboard().url,
|
||||
},
|
||||
];
|
||||
interface DashboardProps {
|
||||
stats: {
|
||||
total_employees: number;
|
||||
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 (
|
||||
<AppLayout breadcrumbs={breadcrumbs}>
|
||||
<AppLayout breadcrumbs={[{ title: 'Dashboard', href: '/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="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" />
|
||||
</div>
|
||||
<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" />
|
||||
</div>
|
||||
<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" />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 flex-col gap-4 p-4 md:p-8 pt-0">
|
||||
|
||||
{/* CARD STATISTIK */}
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Total Karyawan</CardTitle>
|
||||
<Users className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<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 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>
|
||||
|
||||
{/* 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>
|
||||
</AppLayout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
<?php
|
||||
|
||||
use App\Http\Controllers\Admin\DepartmentController;
|
||||
use App\Http\Controllers\Admin\EmployeeController;
|
||||
use App\Http\Controllers\Admin\PositionController;
|
||||
use App\Http\Controllers\DashboardController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Inertia\Inertia;
|
||||
use Laravel\Fortify\Features;
|
||||
|
|
@ -11,11 +14,13 @@
|
|||
]);
|
||||
})->name('home');
|
||||
|
||||
Route::get('dashboard', function () {
|
||||
return Inertia::render('dashboard');
|
||||
})->middleware(['auth', 'verified'])->name('dashboard');
|
||||
Route::get('dashboard', [DashboardController::class, 'index'])
|
||||
->middleware(['auth', 'verified'])
|
||||
->name('dashboard');
|
||||
|
||||
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);
|
||||
});
|
||||
require __DIR__.'/settings.php';
|
||||
|
|
|
|||
Loading…
Reference in New Issue