Feat: Dashboard, Manajemen Positions, and Manajemen Departments

This commit is contained in:
IlhamIslamy 2026-02-26 03:17:08 +07:00
parent 0ef81fb590
commit ae9e0a2eb0
25 changed files with 3950 additions and 324 deletions

View File

@ -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.');
}
}

View File

@ -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.');
}
}
}

View File

@ -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.');
}
}

View File

@ -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,
]);
}
}

18
app/Models/Department.php Normal file
View File

@ -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);
}
}

View File

@ -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);
}
}

18
app/Models/Position.php Normal file
View File

@ -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);
}
}

View File

@ -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();
});
}

View File

@ -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');
}
};

View File

@ -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');
}
};

View File

@ -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']);
}
}

2209
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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": {

View File

@ -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,
},
];

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}
}

View File

@ -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';