feat: enhance Employee management with filtering options and improved data handling
- Updated EmployeeController to accept request filters for employment status, activity status, and gender. - Modified EmployeeService to support filtering in the getAll method. - Enhanced Employee index page with a filter toolbar for better user experience. - Fixed route parameter naming for employee-related routes. - Added comprehensive tests for employee index functionality and filtering capabilities.
This commit is contained in:
parent
5f0b62638b
commit
bc810c93d2
@ -7,6 +7,7 @@
|
||||
use App\Models\User;
|
||||
use App\Services\Admin\HR\EmployeeService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
@ -16,10 +17,11 @@ public function __construct(
|
||||
private EmployeeService $service
|
||||
) {}
|
||||
|
||||
public function index(): Response
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
return Inertia::render('admin/hr/employee/index', [
|
||||
'employees' => $this->service->getAll(),
|
||||
'employees' => $this->service->getAll($request->only(['employment_status', 'is_active', 'gender'])),
|
||||
'filters' => $request->only(['employment_status', 'is_active', 'gender']),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -66,8 +68,8 @@ public function destroy(User $user): RedirectResponse
|
||||
|
||||
public function toggleActive(User $user): RedirectResponse
|
||||
{
|
||||
$this->service->toggleActive($user);
|
||||
$status = $user->fresh()->is_active ? 'diaktifkan' : 'dinonaktifkan';
|
||||
$employee = $this->service->toggleActive($user);
|
||||
$status = $employee->is_active ? 'diaktifkan' : 'dinonaktifkan';
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => "Pegawai berhasil {$status}."]);
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ public function authorize(): bool
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$userId = $this->route('employee')?->id;
|
||||
$userId = $this->route('user')?->id;
|
||||
|
||||
return [
|
||||
'email' => [
|
||||
|
||||
@ -8,14 +8,17 @@
|
||||
|
||||
class EmployeeService
|
||||
{
|
||||
public function getAll(): Collection
|
||||
public function getAll(array $filters = []): Collection
|
||||
{
|
||||
return User::select('id', 'email', 'username', 'is_active')
|
||||
->whereHas('employee')
|
||||
->with([
|
||||
'userProfile' => fn ($q) => $q->select('id', 'user_id', 'full_name', 'phone_number'),
|
||||
'userProfile' => fn ($q) => $q->select('id', 'user_id', 'full_name', 'phone_number', 'gender'),
|
||||
'employee' => fn ($q) => $q->select('id', 'user_id', 'join_date', 'employment_status', 'base_salary'),
|
||||
])
|
||||
->when($filters['employment_status'] ?? null, fn ($q, $status) => $q->whereHas('employee', fn ($eq) => $eq->where('employment_status', $status)))
|
||||
->when(isset($filters['is_active']) && $filters['is_active'] !== '', fn ($q) => $q->where('is_active', filter_var($filters['is_active'], FILTER_VALIDATE_BOOLEAN)))
|
||||
->when($filters['gender'] ?? null, fn ($q, $gender) => $q->whereHas('userProfile', fn ($uq) => $uq->where('gender', $gender)))
|
||||
->latest()
|
||||
->get();
|
||||
}
|
||||
|
||||
@ -1,20 +1,53 @@
|
||||
import { ConfirmDialog } from '@/components/confirm-dialog';
|
||||
import { DataTable } from '@/components/data-table';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { destroy, create as employeeCreate, edit as employeeEdit, index as employeeIndex, toggleActive, resetPassword as resetPasswordRoute } from '@/routes/admin/hr/employees';
|
||||
import { Head, router } from '@inertiajs/react';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { Filter, Plus, X } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import type { Employee } from './columns';
|
||||
import { createEmployeeColumns } from './columns';
|
||||
|
||||
type Props = {
|
||||
employees: Employee[];
|
||||
filters: {
|
||||
employment_status?: string;
|
||||
is_active?: string;
|
||||
gender?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export default function EmployeeIndex({ employees }: Props) {
|
||||
export default function EmployeeIndex({ employees, filters }: Props) {
|
||||
const [deleting, setDeleting] = useState<Employee | null>(null);
|
||||
const [resetPasswordTarget, setResetPasswordTarget] = useState<Employee | null>(null);
|
||||
const [filterOpen, setFilterOpen] = useState(false);
|
||||
|
||||
const hasActiveFilters = filters.employment_status || filters.is_active;
|
||||
|
||||
function applyFilter(key: string, value: string) {
|
||||
const newFilters = { ...filters };
|
||||
|
||||
if (value === '' || value === 'all') {
|
||||
delete newFilters[key as keyof typeof newFilters];
|
||||
} else {
|
||||
newFilters[key as keyof typeof newFilters] = value;
|
||||
}
|
||||
|
||||
router.get(employeeIndex.url(), newFilters, {
|
||||
preserveState: true,
|
||||
replace: true,
|
||||
});
|
||||
}
|
||||
|
||||
function clearFilters() {
|
||||
router.get(employeeIndex.url(), {}, {
|
||||
preserveState: true,
|
||||
replace: true,
|
||||
});
|
||||
setFilterOpen(false);
|
||||
}
|
||||
|
||||
function handleDelete() {
|
||||
if (!deleting) {
|
||||
@ -45,6 +78,100 @@ export default function EmployeeIndex({ employees }: Props) {
|
||||
toggleActiveUrl: (id) => toggleActive.url(id),
|
||||
});
|
||||
|
||||
const filterToolbar = (
|
||||
<Popover open={filterOpen} onOpenChange={setFilterOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" size="sm">
|
||||
<Filter className="h-4 w-4" />
|
||||
Filter
|
||||
{hasActiveFilters && (
|
||||
<span className="ml-1 flex h-5 w-5 items-center justify-center rounded-full bg-primary text-xs text-primary-foreground">
|
||||
{Object.values(filters).filter(Boolean).length}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-64" align="end">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium">Filter</span>
|
||||
{hasActiveFilters && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 px-2 text-xs"
|
||||
onClick={clearFilters}
|
||||
>
|
||||
<X className="mr-1 h-3 w-3" />
|
||||
Hapus Semua
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-xs text-muted-foreground">
|
||||
Status Karyawan
|
||||
</label>
|
||||
<Select
|
||||
value={filters.employment_status ?? 'all'}
|
||||
onValueChange={(value) => applyFilter('employment_status', value)}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Semua Status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Semua Status</SelectItem>
|
||||
<SelectItem value="full_time">Full Time</SelectItem>
|
||||
<SelectItem value="part_time">Part Time</SelectItem>
|
||||
<SelectItem value="contract">Kontrak</SelectItem>
|
||||
<SelectItem value="internship">Magang</SelectItem>
|
||||
<SelectItem value="resigned">Keluar</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-xs text-muted-foreground">
|
||||
Status Aktif
|
||||
</label>
|
||||
<Select
|
||||
value={filters.is_active ?? 'all'}
|
||||
onValueChange={(value) => applyFilter('is_active', value)}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Semua" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Semua</SelectItem>
|
||||
<SelectItem value="1">Aktif</SelectItem>
|
||||
<SelectItem value="0">Tidak Aktif</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-xs text-muted-foreground">
|
||||
Jenis Kelamin
|
||||
</label>
|
||||
<Select
|
||||
value={filters.gender ?? 'all'}
|
||||
onValueChange={(value) => applyFilter('gender', value)}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Semua" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Semua</SelectItem>
|
||||
<SelectItem value="male">Laki-laki</SelectItem>
|
||||
<SelectItem value="female">Perempuan</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head title="Pegawai" />
|
||||
@ -70,6 +197,7 @@ export default function EmployeeIndex({ employees }: Props) {
|
||||
searchKey="full_name"
|
||||
searchPlaceholder="Cari pegawai..."
|
||||
emptyText="Belum ada data pegawai."
|
||||
toolbar={filterToolbar}
|
||||
/>
|
||||
|
||||
<ConfirmDialog
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
use App\Http\Controllers\Admin\Finance\ExpenseController;
|
||||
use App\Http\Controllers\Admin\Master\CategoryController;
|
||||
use App\Http\Controllers\Admin\Master\CustomerController;
|
||||
use App\Http\Controllers\Admin\Master\ProductController;
|
||||
use App\Http\Controllers\Admin\Master\SupplierController;
|
||||
use App\Http\Controllers\Admin\HR\EmployeeController;
|
||||
use App\Http\Controllers\Admin\HR\LeaveRequestController;
|
||||
@ -36,9 +37,9 @@
|
||||
});
|
||||
|
||||
Route::prefix('admin/hr')->name('admin.hr.')->group(function () {
|
||||
Route::resource('employees', EmployeeController::class)->except(['show']);
|
||||
Route::post('employees/{employee}/toggle-active', [EmployeeController::class, 'toggleActive'])->name('employees.toggle-active');
|
||||
Route::post('employees/{employee}/reset-password', [EmployeeController::class, 'resetPassword'])->name('employees.reset-password');
|
||||
Route::resource('employees', EmployeeController::class)->except(['show'])->parameters(['employees' => 'user']);
|
||||
Route::post('employees/{user}/toggle-active', [EmployeeController::class, 'toggleActive'])->name('employees.toggle-active');
|
||||
Route::post('employees/{user}/reset-password', [EmployeeController::class, 'resetPassword'])->name('employees.reset-password');
|
||||
|
||||
Route::resource('leave-requests', LeaveRequestController::class)->except(['show', 'create', 'edit']);
|
||||
Route::post('leave-requests/{leaveRequest}/approve', [LeaveRequestController::class, 'approve'])->name('leave-requests.approve');
|
||||
|
||||
1034
tests/Feature/Admin/HR/EmployeeTest.php
Normal file
1034
tests/Feature/Admin/HR/EmployeeTest.php
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user