Enhance employee and user profile models by adding formatted date attributes and relationships. Introduce enum labels for EmployeeStatus, EmploymentStatus, and Gender. Update routes for employee management with new HR controller actions.
This commit is contained in:
parent
960bd71fd0
commit
926da48a8e
@ -2,9 +2,22 @@
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use App\Traits\ProvidesEnumOptions;
|
||||
|
||||
enum EmployeeStatus: string
|
||||
{
|
||||
use ProvidesEnumOptions;
|
||||
|
||||
case ACTIVE = 'active';
|
||||
case INACTIVE = 'inactive';
|
||||
case RESIGNED = 'resigned';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::ACTIVE => 'Aktif',
|
||||
self::INACTIVE => 'Tidak Aktif',
|
||||
self::RESIGNED => 'Resign',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,10 +2,24 @@
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use App\Traits\ProvidesEnumOptions;
|
||||
|
||||
enum EmploymentStatus: string
|
||||
{
|
||||
use ProvidesEnumOptions;
|
||||
|
||||
case FULL_TIME = 'full_time';
|
||||
case PART_TIME = 'part_time';
|
||||
case CONTRACT = 'contract';
|
||||
case TEMPORARY = 'temporary';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::FULL_TIME => 'Penuh Waktu',
|
||||
self::PART_TIME => 'Paruh Waktu',
|
||||
self::CONTRACT => 'Kontrak',
|
||||
self::TEMPORARY => 'Sementara',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,8 +2,20 @@
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use App\Traits\ProvidesEnumOptions;
|
||||
|
||||
enum Gender: string
|
||||
{
|
||||
use ProvidesEnumOptions;
|
||||
|
||||
case MALE = 'male';
|
||||
case FEMALE = 'female';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::MALE => 'Laki-laki',
|
||||
self::FEMALE => 'Perempuan',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
274
app/Http/Controllers/Admin/Hr/EmployeeController.php
Normal file
274
app/Http/Controllers/Admin/Hr/EmployeeController.php
Normal file
@ -0,0 +1,274 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Hr;
|
||||
|
||||
use App\Enums\EmployeeStatus;
|
||||
use App\Enums\EmploymentStatus;
|
||||
use App\Enums\Gender;
|
||||
use App\Http\Controllers\Concerns\ParsesDataTableQuery;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Hr\EmployeeRequest;
|
||||
use App\Models\Employee;
|
||||
use App\Models\User;
|
||||
use App\Models\UserProfile;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class EmployeeController extends Controller
|
||||
{
|
||||
use ParsesDataTableQuery;
|
||||
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$tableQuery = $this->parseDataTableQuery($request);
|
||||
$search = $tableQuery['search'];
|
||||
$sort = $tableQuery['sort'];
|
||||
$direction = $tableQuery['direction'];
|
||||
|
||||
$employmentStatus = $request->string('employment_status')->toString();
|
||||
$employeeStatus = $request->string('employee_status')->toString();
|
||||
|
||||
$query = User::query()
|
||||
->with(['profile', 'employee'])
|
||||
->whereHas('employee')
|
||||
->when($search !== '', function ($query) use ($search): void {
|
||||
$query->where(function ($query) use ($search): void {
|
||||
$query->where('email', 'like', "%{$search}%")
|
||||
->orWhere('username', 'like', "%{$search}%")
|
||||
->orWhereHas('profile', function ($query) use ($search): void {
|
||||
$query->where('full_name', 'like', "%{$search}%")
|
||||
->orWhere('phone_number', 'like', "%{$search}%");
|
||||
})
|
||||
->orWhereHas('employee', function ($query) use ($search): void {
|
||||
$query->where('employee_code', 'like', "%{$search}%");
|
||||
});
|
||||
});
|
||||
})
|
||||
->when(
|
||||
$employmentStatus !== '',
|
||||
fn ($query) => $query->whereHas('employee', fn ($query) => $query->where('employment_status', $employmentStatus))
|
||||
)
|
||||
->when(
|
||||
$employeeStatus !== '',
|
||||
fn ($query) => $query->whereHas('employee', fn ($query) => $query->where('employee_status', $employeeStatus))
|
||||
);
|
||||
|
||||
$this->applySorting($query, $sort, $direction);
|
||||
|
||||
$employees = $query
|
||||
->paginate(10)
|
||||
->withQueryString()
|
||||
->through(fn (User $user) => $this->transformEmployee($user));
|
||||
|
||||
return Inertia::render('admin/hr/employees/Index', [
|
||||
'employees' => $employees,
|
||||
'filters' => $this->dataTableFilters($tableQuery, [
|
||||
'employment_status' => $employmentStatus,
|
||||
'employee_status' => $employeeStatus,
|
||||
]),
|
||||
'employmentStatuses' => EmploymentStatus::selectOptions(),
|
||||
'employeeStatuses' => EmployeeStatus::selectOptions(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(): Response
|
||||
{
|
||||
return Inertia::render('admin/hr/employees/Create', [
|
||||
'genders' => Gender::selectOptions(),
|
||||
'employmentStatuses' => EmploymentStatus::selectOptions(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(EmployeeRequest $request): RedirectResponse
|
||||
{
|
||||
$validated = $request->validated();
|
||||
|
||||
DB::transaction(function () use ($validated): void {
|
||||
$user = User::create([
|
||||
'email' => $validated['email'],
|
||||
'username' => $validated['username'],
|
||||
'password' => Hash::make(config('auth.password_default')),
|
||||
]);
|
||||
|
||||
UserProfile::create([
|
||||
'user_id' => $user->id,
|
||||
'full_name' => $validated['full_name'],
|
||||
'phone_number' => $validated['phone_number'],
|
||||
'gender' => $validated['gender'],
|
||||
'birth_date' => $validated['birth_date'],
|
||||
'address' => $validated['address'],
|
||||
]);
|
||||
|
||||
Employee::create([
|
||||
'user_id' => $user->id,
|
||||
'join_date' => $validated['join_date'],
|
||||
'employment_status' => $validated['employment_status'],
|
||||
'base_salary' => $validated['base_salary'],
|
||||
]);
|
||||
});
|
||||
|
||||
Inertia::flash('success', 'Pegawai berhasil ditambahkan.');
|
||||
|
||||
return redirect()->route('admin.hr.employees.index');
|
||||
}
|
||||
|
||||
public function edit(User $user): Response
|
||||
{
|
||||
$user->load(['profile', 'employee']);
|
||||
|
||||
return Inertia::render('admin/hr/employees/Edit', [
|
||||
'genders' => Gender::selectOptions(),
|
||||
'employmentStatuses' => EmploymentStatus::selectOptions(),
|
||||
'employee' => $this->transformEmployee($user),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(EmployeeRequest $request, User $user): RedirectResponse
|
||||
{
|
||||
$validated = $request->validated();
|
||||
$employee = $user->employee;
|
||||
|
||||
DB::transaction(function () use ($validated, $user, $employee): void {
|
||||
$user->email = $validated['email'];
|
||||
$user->username = $validated['username'];
|
||||
$user->save();
|
||||
|
||||
$profile = $user->profile;
|
||||
$profile->full_name = $validated['full_name'];
|
||||
$profile->phone_number = $validated['phone_number'];
|
||||
$profile->gender = $validated['gender'];
|
||||
$profile->birth_date = $validated['birth_date'];
|
||||
$profile->address = $validated['address'];
|
||||
$profile->save();
|
||||
|
||||
$employee->join_date = $validated['join_date'];
|
||||
$employee->employment_status = $validated['employment_status'];
|
||||
$employee->base_salary = $validated['base_salary'];
|
||||
$employee->save();
|
||||
});
|
||||
|
||||
Inertia::flash('success', 'Data pegawai berhasil diperbarui.');
|
||||
|
||||
return redirect()->route('admin.hr.employees.index');
|
||||
}
|
||||
|
||||
public function toggleStatus(Request $request, User $user): RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'is_active' => ['required', 'boolean'],
|
||||
]);
|
||||
|
||||
$user->is_active = $validated['is_active'];
|
||||
$user->save();
|
||||
|
||||
if (! $validated['is_active']) {
|
||||
DB::table('sessions')->where('user_id', $user->id)->delete();
|
||||
}
|
||||
|
||||
Inertia::flash('success', 'Status pegawai berhasil diperbarui.');
|
||||
|
||||
return back();
|
||||
}
|
||||
|
||||
public function resetPassword(User $user): RedirectResponse
|
||||
{
|
||||
$user->password = config('auth.password_default');
|
||||
$user->save();
|
||||
|
||||
DB::table('sessions')->where('user_id', $user->id)->delete();
|
||||
|
||||
Inertia::flash('success', 'Kata sandi berhasil direset. Pengguna telah logout dari semua sesi.');
|
||||
|
||||
return back();
|
||||
}
|
||||
|
||||
public function destroy(User $user): RedirectResponse
|
||||
{
|
||||
DB::transaction(function () use ($user): void {
|
||||
$user->employee?->delete();
|
||||
$user->profile?->delete();
|
||||
$user->delete();
|
||||
});
|
||||
|
||||
Inertia::flash('success', 'Pegawai berhasil dihapus.');
|
||||
|
||||
return redirect()->route('admin.hr.employees.index');
|
||||
}
|
||||
|
||||
private function applySorting(Builder $query, string $sort, string $direction): void
|
||||
{
|
||||
$employeeSorts = [
|
||||
'employee_code',
|
||||
'join_date',
|
||||
'base_salary',
|
||||
'employment_status',
|
||||
'employee_status',
|
||||
];
|
||||
|
||||
if (in_array($sort, $employeeSorts, true)) {
|
||||
$query->orderBy(
|
||||
Employee::select($sort)
|
||||
->whereColumn('employees.user_id', 'users.id')
|
||||
->limit(1),
|
||||
$direction
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($sort === 'full_name') {
|
||||
$query->orderBy(
|
||||
UserProfile::select('full_name')
|
||||
->whereColumn('user_profiles.user_id', 'users.id')
|
||||
->limit(1),
|
||||
$direction
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($sort === 'email') {
|
||||
$query->orderBy('email', $direction);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$query->latest();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function transformEmployee(User $user): array
|
||||
{
|
||||
$employee = $user->employee;
|
||||
$profile = $user->profile;
|
||||
|
||||
return [
|
||||
'id' => $user->id,
|
||||
'employee_code' => $employee->employee_code,
|
||||
'join_date' => $employee->join_date_formatted,
|
||||
'resign_date' => $employee->resign_date_formatted,
|
||||
'employment_status' => $employee->employment_status?->value,
|
||||
'employment_status_label' => $employee->employment_status?->label(),
|
||||
'employee_status' => $employee->employee_status?->value,
|
||||
'employee_status_label' => $employee->employee_status?->label(),
|
||||
'base_salary' => $employee->base_salary,
|
||||
'base_salary_formatted' => $employee->base_salary_formatted,
|
||||
'email' => $user->email,
|
||||
'username' => $user->username,
|
||||
'is_active' => $user->is_active,
|
||||
'full_name' => $profile->full_name,
|
||||
'phone_number' => $profile->phone_number,
|
||||
'gender' => $profile->gender?->value,
|
||||
'gender_label' => $profile->gender?->label(),
|
||||
'birth_date' => $profile->birth_date_formatted,
|
||||
'address' => $profile->address,
|
||||
];
|
||||
}
|
||||
}
|
||||
33
app/Http/Controllers/Concerns/ParsesDataTableQuery.php
Normal file
33
app/Http/Controllers/Concerns/ParsesDataTableQuery.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Concerns;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
trait ParsesDataTableQuery
|
||||
{
|
||||
/**
|
||||
* @return array{search: string, sort: string, direction: 'asc'|'desc'}
|
||||
*/
|
||||
protected function parseDataTableQuery(Request $request): array
|
||||
{
|
||||
return [
|
||||
'search' => $request->string('search')->trim()->toString(),
|
||||
'sort' => $request->string('sort')->toString(),
|
||||
'direction' => $request->string('direction')->toString() === 'desc' ? 'desc' : 'asc',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{search: string, sort: string, direction: 'asc'|'desc'} $query
|
||||
* @return array{search: string, sort: string, direction: 'asc'|'desc'|null}
|
||||
*/
|
||||
protected function dataTableFilters(array $query, array $extra = []): array
|
||||
{
|
||||
return array_merge([
|
||||
'search' => $query['search'],
|
||||
'sort' => $query['sort'],
|
||||
'direction' => $query['sort'] !== '' ? $query['direction'] : null,
|
||||
], $extra);
|
||||
}
|
||||
}
|
||||
42
app/Http/Requests/Admin/Hr/EmployeeRequest.php
Normal file
42
app/Http/Requests/Admin/Hr/EmployeeRequest.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Hr;
|
||||
|
||||
use App\Enums\EmploymentStatus;
|
||||
use App\Enums\Gender;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class EmployeeRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return auth()->check();
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$this->merge([
|
||||
'base_salary' => $this->integer('base_salary'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'email' => ['required', 'email', 'max:100', Rule::unique('users', 'email')->ignore($this->route('user')?->id)],
|
||||
'username' => ['required', 'string', 'max:20', 'alpha_dash', Rule::unique('users', 'username')->ignore($this->route('user')?->id)],
|
||||
'full_name' => ['required', 'string', 'max:200'],
|
||||
'phone_number' => ['nullable', 'string', 'regex:/^08\d{8,11}$/'],
|
||||
'gender' => ['nullable', Rule::enum(Gender::class)],
|
||||
'birth_date' => ['nullable', 'date', 'before:today'],
|
||||
'address' => ['nullable', 'string'],
|
||||
'join_date' => ['required', 'date'],
|
||||
'employment_status' => ['required', Rule::enum(EmploymentStatus::class)],
|
||||
'base_salary' => ['required', 'integer', 'min:0'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -4,8 +4,11 @@
|
||||
|
||||
use App\Enums\EmployeeStatus;
|
||||
use App\Enums\EmploymentStatus;
|
||||
use App\Observers\EmployeeObserver;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Attributes\Appends;
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
|
||||
use Illuminate\Database\Eloquent\Attributes\Scope;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
@ -14,7 +17,8 @@
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
#[Guarded(['id'])]
|
||||
#[Appends(['base_salary_formatted'])]
|
||||
#[Appends(['base_salary_formatted', 'join_date_formatted', 'resign_date_formatted'])]
|
||||
#[ObservedBy([EmployeeObserver::class])]
|
||||
class Employee extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
@ -74,12 +78,26 @@ public function temporary(Builder $query): void
|
||||
public function baseSalaryFormatted(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn (int $value) => number_format($value, 0, ',', '.'),
|
||||
get: fn () => number_format($this->base_salary, 0, ',', '.'),
|
||||
);
|
||||
}
|
||||
|
||||
public function profile(): BelongsTo
|
||||
public function joinDateFormatted(): Attribute
|
||||
{
|
||||
return $this->belongsTo(UserProfile::class);
|
||||
return Attribute::make(
|
||||
get: fn () => Carbon::parse($this->join_date)->format('l, d F Y'),
|
||||
);
|
||||
}
|
||||
|
||||
public function resignDateFormatted(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => $this->resign_date ? Carbon::parse($this->resign_date)->format('l, d F Y') : null,
|
||||
);
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,4 +29,9 @@ public function profile(): HasOne
|
||||
{
|
||||
return $this->hasOne(UserProfile::class);
|
||||
}
|
||||
|
||||
public function employee(): HasOne
|
||||
{
|
||||
return $this->hasOne(Employee::class);
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,13 +3,16 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\Gender;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Attributes\Appends;
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
#[Guarded(['id'])]
|
||||
#[Appends(['birth_date_formatted'])]
|
||||
class UserProfile extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
@ -22,9 +25,11 @@ protected function casts(): array
|
||||
];
|
||||
}
|
||||
|
||||
public function employee(): HasOne
|
||||
protected function birthDateFormatted(): Attribute
|
||||
{
|
||||
return $this->hasOne(Employee::class);
|
||||
return Attribute::make(
|
||||
get: fn () => Carbon::parse($this->birth_date)->format('l, d F Y'),
|
||||
);
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
|
||||
35
app/Observers/EmployeeObserver.php
Normal file
35
app/Observers/EmployeeObserver.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Observers;
|
||||
|
||||
use App\Models\Employee;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class EmployeeObserver
|
||||
{
|
||||
public function creating(Employee $employee): void
|
||||
{
|
||||
if (filled($employee->employee_code)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$year = now()->format('Y');
|
||||
$prefix = "EMP-{$year}-";
|
||||
|
||||
$employee->employee_code = DB::transaction(function () use ($prefix): string {
|
||||
$latestCode = Employee::withTrashed()
|
||||
->where('employee_code', 'like', $prefix.'%')
|
||||
->lockForUpdate()
|
||||
->orderByDesc('employee_code')
|
||||
->value('employee_code');
|
||||
|
||||
$sequence = 1;
|
||||
|
||||
if ($latestCode !== null) {
|
||||
$sequence = (int) substr($latestCode, strlen($prefix)) + 1;
|
||||
}
|
||||
|
||||
return $prefix.str_pad((string) $sequence, 4, '0', STR_PAD_LEFT);
|
||||
});
|
||||
}
|
||||
}
|
||||
25
app/Traits/ProvidesEnumOptions.php
Normal file
25
app/Traits/ProvidesEnumOptions.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Traits;
|
||||
|
||||
trait ProvidesEnumOptions
|
||||
{
|
||||
public static function options(): array
|
||||
{
|
||||
return collect(static::cases())
|
||||
->mapWithKeys(fn ($case) => [
|
||||
$case->value => $case->label(),
|
||||
])
|
||||
->all();
|
||||
}
|
||||
|
||||
public static function selectOptions(): array
|
||||
{
|
||||
return collect(static::cases())
|
||||
->map(fn ($case) => [
|
||||
'value' => $case->value,
|
||||
'label' => $case->label(),
|
||||
])
|
||||
->all();
|
||||
}
|
||||
}
|
||||
195
resources/js/components/hr/EmployeeForm.vue
Normal file
195
resources/js/components/hr/EmployeeForm.vue
Normal file
@ -0,0 +1,195 @@
|
||||
<script setup lang="ts">
|
||||
import { useForm } from '@inertiajs/vue3';
|
||||
import { Save } from '@lucide/vue';
|
||||
import { toast } from 'vue-sonner';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { DatePicker } from '@/components/ui/date-picker';
|
||||
import {
|
||||
Field,
|
||||
FieldError,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldSet,
|
||||
} from '@/components/ui/field';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { PhoneNumberInput } from '@/components/ui/phone-number-input';
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||
import { RupiahInput } from '@/components/ui/rupiah-input';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import type { EmployeeFormData, EnumOption } from '@/types/employee';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
genders: EnumOption[];
|
||||
employmentStatuses: EnumOption[];
|
||||
initialData?: Partial<EmployeeFormData>;
|
||||
submitUrl: string;
|
||||
method?: 'post' | 'put';
|
||||
submitLabel?: string;
|
||||
employeeId?: number;
|
||||
employeeName?: string | null;
|
||||
}>(),
|
||||
{
|
||||
method: 'post',
|
||||
submitLabel: 'Simpan',
|
||||
},
|
||||
);
|
||||
|
||||
const form = useForm<EmployeeFormData>({
|
||||
email: props.initialData?.email ?? '',
|
||||
username: props.initialData?.username ?? '',
|
||||
full_name: props.initialData?.full_name ?? '',
|
||||
phone_number: props.initialData?.phone_number ?? '',
|
||||
gender: props.initialData?.gender ?? '',
|
||||
birth_date: props.initialData?.birth_date ?? '',
|
||||
address: props.initialData?.address ?? '',
|
||||
join_date: props.initialData?.join_date ?? '',
|
||||
employment_status: props.initialData?.employment_status ?? 'full_time',
|
||||
base_salary: props.initialData?.base_salary ?? '',
|
||||
});
|
||||
|
||||
function submit() {
|
||||
const options = {
|
||||
onError: () => {
|
||||
toast.error('Gagal menyimpan data. Periksa kembali formulir.');
|
||||
},
|
||||
};
|
||||
|
||||
if (props.method === 'put') {
|
||||
form.put(props.submitUrl, options);
|
||||
} else {
|
||||
form.post(props.submitUrl, options);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form @submit.prevent="submit">
|
||||
<div class="grid gap-6">
|
||||
<Card>
|
||||
<CardHeader class="flex flex-row items-start justify-between gap-4">
|
||||
<div class="space-y-1">
|
||||
<CardTitle>Data Akun</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<FieldSet class="grid gap-4 md:grid-cols-2">
|
||||
<Field>
|
||||
<FieldLabel for="email" required>Email</FieldLabel>
|
||||
<Input id="email" v-model="form.email" type="email" placeholder="nama@perusahaan.com" />
|
||||
<FieldError :errors="form.errors.email ? [form.errors.email] : []" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel for="username" required>Username</FieldLabel>
|
||||
<Input id="username" v-model="form.username" type="text" placeholder="username" />
|
||||
<FieldError :errors="form.errors.username ? [form.errors.username] : []" />
|
||||
</Field>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Data Pribadi</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<FieldSet class="grid gap-4 md:grid-cols-3">
|
||||
<Field class="md:col-span-3">
|
||||
<FieldLabel for="full_name" required>Nama Lengkap</FieldLabel>
|
||||
<Input id="full_name" v-model="form.full_name" type="text"
|
||||
placeholder="Nama lengkap pegawai" />
|
||||
<FieldError :errors="form.errors.full_name ? [form.errors.full_name] : []" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel for="phone_number">Nomor Telepon</FieldLabel>
|
||||
<PhoneNumberInput id="phone_number" v-model="form.phone_number" />
|
||||
<FieldError :errors="form.errors.phone_number ? [form.errors.phone_number] : []" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel for="birth_date">Tanggal Lahir</FieldLabel>
|
||||
<DatePicker id="birth_date" v-model="form.birth_date"
|
||||
placeholder="Pilih tanggal lahir" />
|
||||
<FieldError :errors="form.errors.birth_date ? [form.errors.birth_date] : []" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel>Jenis Kelamin</FieldLabel>
|
||||
<RadioGroup v-model="form.gender" class="flex flex-wrap gap-4 pt-1">
|
||||
<div v-for="option in genders" :key="option.value" class="flex items-center gap-2">
|
||||
<RadioGroupItem :id="`gender-${option.value}`" :value="option.value" />
|
||||
<Label :for="`gender-${option.value}`" class="font-normal">
|
||||
{{ option.label }}
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
<FieldError :errors="form.errors.gender ? [form.errors.gender] : []" />
|
||||
</Field>
|
||||
<Field class="md:col-span-3">
|
||||
<FieldLabel for="address">Alamat</FieldLabel>
|
||||
<Textarea id="address" v-model="form.address" placeholder="Alamat lengkap" rows="3" />
|
||||
<FieldError :errors="form.errors.address ? [form.errors.address] : []" />
|
||||
</Field>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Data Kepegawaian</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<FieldSet class="grid gap-4 md:grid-cols-3">
|
||||
<Field>
|
||||
<FieldLabel for="join_date" required>Tanggal Bergabung</FieldLabel>
|
||||
<DatePicker id="join_date" v-model="form.join_date"
|
||||
placeholder="Pilih tanggal bergabung" />
|
||||
<FieldError :errors="form.errors.join_date ? [form.errors.join_date] : []" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel for="base_salary" required>Gaji Pokok</FieldLabel>
|
||||
<RupiahInput id="base_salary" v-model="form.base_salary" />
|
||||
<FieldError :errors="form.errors.base_salary ? [form.errors.base_salary] : []" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel for="employment_status" required>Status Kepegawaian</FieldLabel>
|
||||
<Select v-model="form.employment_status">
|
||||
<SelectTrigger id="employment_status" class="w-full">
|
||||
<SelectValue placeholder="Pilih status kepegawaian" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="option in employmentStatuses" :key="option.value"
|
||||
:value="option.value">
|
||||
{{ option.label }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FieldError
|
||||
:errors="form.errors.employment_status ? [form.errors.employment_status] : []" />
|
||||
</Field>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<Button type="submit" :disabled="form.processing">
|
||||
<Save class="size-4" />
|
||||
{{ form.processing ? 'Menyimpan...' : submitLabel }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
74
resources/js/components/hr/ResetPasswordDialog.vue
Normal file
74
resources/js/components/hr/ResetPasswordDialog.vue
Normal file
@ -0,0 +1,74 @@
|
||||
<script setup lang="ts">
|
||||
import { router } from '@inertiajs/vue3';
|
||||
import { ref } from 'vue';
|
||||
import { toast } from 'vue-sonner';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
const props = defineProps<{
|
||||
employeeId: number;
|
||||
employeeName?: string | null;
|
||||
}>();
|
||||
|
||||
const open = ref(false);
|
||||
const processing = ref(false);
|
||||
|
||||
function resetPassword() {
|
||||
processing.value = true;
|
||||
|
||||
router.post(`/admin/hr/employees/${props.employeeId}/reset-password`, {}, {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
open.value = false;
|
||||
},
|
||||
onError: () => {
|
||||
toast.error('Gagal mereset kata sandi.');
|
||||
},
|
||||
onFinish: () => {
|
||||
processing.value = false;
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AlertDialog v-model:open="open">
|
||||
<AlertDialogTrigger as-child>
|
||||
<Button variant="outline" type="button">
|
||||
Reset Kata Sandi
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Reset kata sandi pegawai?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Kata sandi
|
||||
<span class="text-foreground font-medium">{{ employeeName ?? 'pegawai' }}</span>
|
||||
akan direset ke kata sandi default sistem. Pengguna akan otomatis logout dari
|
||||
semua sesi aktif.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel :disabled="processing">
|
||||
Batal
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
:disabled="processing"
|
||||
@click.prevent="resetPassword"
|
||||
>
|
||||
{{ processing ? 'Memproses...' : 'Reset Kata Sandi' }}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</template>
|
||||
146
resources/js/components/hr/employees/columns.ts
Normal file
146
resources/js/components/hr/employees/columns.ts
Normal file
@ -0,0 +1,146 @@
|
||||
import type { ColumnDef } from '@tanstack/vue-table';
|
||||
import { h } from 'vue';
|
||||
import { DataTableColumnHeader } from '@/components/data-table';
|
||||
import DataTableActions from '@/components/hr/employees/data-table-actions.vue';
|
||||
import EmployeeStatusToggle from '@/components/hr/employees/employee-status-toggle.vue';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import type { EmployeeListItem } from '@/types/employee';
|
||||
|
||||
function employeeStatusVariant(status: string): 'default' | 'secondary' | 'destructive' {
|
||||
if (status === 'active') {
|
||||
return 'default';
|
||||
}
|
||||
|
||||
if (status === 'resigned') {
|
||||
return 'destructive';
|
||||
}
|
||||
|
||||
return 'secondary';
|
||||
}
|
||||
|
||||
function formatDate(value: string | null): string {
|
||||
if (!value) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
const date = new Date(value);
|
||||
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return date.toLocaleDateString('id-ID', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
});
|
||||
}
|
||||
|
||||
export const columns: ColumnDef<EmployeeListItem>[] = [
|
||||
{
|
||||
accessorKey: 'employee_code',
|
||||
enableSorting: true,
|
||||
header: () => h(DataTableColumnHeader, { title: 'Kode', column: 'employee_code' }),
|
||||
cell: ({ row }) => h('span', { class: 'font-medium' }, row.getValue('employee_code')),
|
||||
},
|
||||
{
|
||||
accessorKey: 'full_name',
|
||||
enableSorting: true,
|
||||
header: () => h(DataTableColumnHeader, { title: 'Nama', column: 'full_name' }),
|
||||
},
|
||||
{
|
||||
id: 'account',
|
||||
accessorKey: 'email',
|
||||
enableSorting: true,
|
||||
header: () => h(DataTableColumnHeader, { title: 'Akun', column: 'email' }),
|
||||
cell: ({ row }) => h('div', { class: 'space-y-0.5' }, [
|
||||
h('div', { class: 'font-medium' }, row.original.email ?? '-'),
|
||||
h('div', { class: 'text-muted-foreground text-xs' }, row.original.username ?? '-'),
|
||||
]),
|
||||
},
|
||||
{
|
||||
accessorKey: 'phone_number',
|
||||
enableSorting: false,
|
||||
header: () => h(DataTableColumnHeader, { title: 'Telepon', column: 'phone_number' }),
|
||||
cell: ({ row }) => row.original.phone_number ?? '-',
|
||||
},
|
||||
{
|
||||
accessorKey: 'gender_label',
|
||||
enableSorting: false,
|
||||
header: () => h(DataTableColumnHeader, { title: 'Jenis Kelamin', column: 'gender' }),
|
||||
cell: ({ row }) => row.original.gender_label ?? '-',
|
||||
},
|
||||
{
|
||||
accessorKey: 'birth_date',
|
||||
enableSorting: false,
|
||||
header: () => h(DataTableColumnHeader, { title: 'Tanggal Lahir', column: 'birth_date' }),
|
||||
cell: ({ row }) => formatDate(row.original.birth_date),
|
||||
},
|
||||
{
|
||||
accessorKey: 'address',
|
||||
enableSorting: false,
|
||||
header: () => h(DataTableColumnHeader, { title: 'Alamat', column: 'address' }),
|
||||
cell: ({ row }) => h(
|
||||
'span',
|
||||
{ class: 'block max-w-[200px] truncate', title: row.original.address ?? undefined },
|
||||
row.original.address ?? '-',
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'employment_dates',
|
||||
accessorKey: 'join_date',
|
||||
enableSorting: true,
|
||||
header: () => h(DataTableColumnHeader, { title: 'Masa Kepegawaian', column: 'join_date' }),
|
||||
cell: ({ row }) => h('div', { class: 'space-y-0.5 text-sm' }, [
|
||||
h('div', {}, [
|
||||
h('span', { class: 'text-muted-foreground' }, 'Bergabung: '),
|
||||
formatDate(row.original.join_date),
|
||||
]),
|
||||
h('div', {}, [
|
||||
h('span', { class: 'text-muted-foreground' }, 'Resign: '),
|
||||
row.original.resign_date ?? '-',
|
||||
]),
|
||||
]),
|
||||
},
|
||||
{
|
||||
accessorKey: 'employment_status_label',
|
||||
enableSorting: true,
|
||||
header: () => h(DataTableColumnHeader, { title: 'Status Kepegawaian', column: 'employment_status' }),
|
||||
},
|
||||
{
|
||||
accessorKey: 'employee_status_label',
|
||||
enableSorting: true,
|
||||
header: () => h(DataTableColumnHeader, { title: 'Status Pegawai', column: 'employee_status' }),
|
||||
cell: ({ row }) => h(
|
||||
Badge,
|
||||
{ variant: employeeStatusVariant(row.original.employee_status) },
|
||||
() => row.original.employee_status_label,
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'base_salary_formatted',
|
||||
enableSorting: true,
|
||||
header: () => h(DataTableColumnHeader, {
|
||||
title: 'Gaji Pokok',
|
||||
column: 'base_salary',
|
||||
class: 'justify-end',
|
||||
}),
|
||||
cell: ({ row }) => h(
|
||||
'div',
|
||||
{ class: 'text-right' },
|
||||
`Rp ${row.original.base_salary_formatted}`,
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'status_toggle',
|
||||
enableSorting: false,
|
||||
header: () => h(DataTableColumnHeader, { title: 'Status', column: 'is_active' }),
|
||||
cell: ({ row }) => h(EmployeeStatusToggle, { employee: row.original }),
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
cell: ({ row }) => h(DataTableActions, { employee: row.original }),
|
||||
},
|
||||
];
|
||||
@ -0,0 +1,54 @@
|
||||
<script setup lang="ts">
|
||||
import { router } from '@inertiajs/vue3';
|
||||
import { ref, watch } from 'vue';
|
||||
import { toast } from 'vue-sonner';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import type { EmployeeListItem } from '@/types/employee';
|
||||
|
||||
const props = defineProps<{
|
||||
employee: EmployeeListItem;
|
||||
}>();
|
||||
|
||||
const isActive = ref(props.employee.is_active);
|
||||
const processing = ref(false);
|
||||
|
||||
watch(
|
||||
() => props.employee.is_active,
|
||||
(value) => {
|
||||
isActive.value = value;
|
||||
},
|
||||
);
|
||||
|
||||
function toggleStatus(checked: boolean) {
|
||||
const previous = isActive.value;
|
||||
isActive.value = checked;
|
||||
processing.value = true;
|
||||
|
||||
router.patch(`/admin/hr/employees/${props.employee.id}/toggle-status`, {
|
||||
is_active: checked,
|
||||
}, {
|
||||
preserveScroll: true,
|
||||
onError: () => {
|
||||
isActive.value = previous;
|
||||
toast.error('Gagal memperbarui status pegawai.');
|
||||
},
|
||||
onFinish: () => {
|
||||
processing.value = false;
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center gap-2">
|
||||
<Switch
|
||||
:model-value="isActive"
|
||||
:disabled="processing || employee.employee_status === 'resigned'"
|
||||
@update:model-value="toggleStatus"
|
||||
/>
|
||||
<Badge :variant="isActive ? 'default' : 'secondary'">
|
||||
{{ isActive ? 'Aktif' : 'Nonaktif' }}
|
||||
</Badge>
|
||||
</div>
|
||||
</template>
|
||||
@ -11,7 +11,7 @@ const props = defineProps<{
|
||||
<main
|
||||
data-slot="sidebar-inset"
|
||||
:class="cn(
|
||||
'bg-background relative flex w-full flex-1 flex-col',
|
||||
'bg-background relative flex w-full min-w-0 flex-1 flex-col',
|
||||
'md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2',
|
||||
props.class,
|
||||
)"
|
||||
|
||||
1
resources/js/components/ui/switch/index.ts
Normal file
1
resources/js/components/ui/switch/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default as Switch } from "./Switch.vue"
|
||||
@ -30,7 +30,7 @@ const page = usePage();
|
||||
<UserNav />
|
||||
</div>
|
||||
</header>
|
||||
<div class="flex flex-1 flex-col gap-4 p-4">
|
||||
<div class="flex min-w-0 flex-1 flex-col gap-4 p-4">
|
||||
<slot />
|
||||
</div>
|
||||
</SidebarInset>
|
||||
|
||||
38
resources/js/pages/admin/hr/employees/Create.vue
Normal file
38
resources/js/pages/admin/hr/employees/Create.vue
Normal file
@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import { Head, Link } from '@inertiajs/vue3';
|
||||
import { ArrowLeft } from '@lucide/vue';
|
||||
import EmployeeForm from '@/components/hr/EmployeeForm.vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||
import type { EnumOption } from '@/types/employee';
|
||||
|
||||
defineProps<{
|
||||
genders: EnumOption[];
|
||||
employmentStatuses: EnumOption[];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<Head title="Tambah Pegawai" />
|
||||
|
||||
<AdminLayout title="Tambah Pegawai">
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="space-y-1">
|
||||
<h2 class="text-2xl font-bold tracking-tight">
|
||||
Tambah Pegawai
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<Button variant="outline" as-child class="shrink-0 self-start sm:self-center">
|
||||
<Link href="/admin/hr/employees">
|
||||
<ArrowLeft class="size-4" />
|
||||
Kembali
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<EmployeeForm submit-url="/admin/hr/employees" method="post" submit-label="Simpan" :genders="genders"
|
||||
:employment-statuses="employmentStatuses" />
|
||||
</AdminLayout>
|
||||
</template>
|
||||
54
resources/js/pages/admin/hr/employees/Edit.vue
Normal file
54
resources/js/pages/admin/hr/employees/Edit.vue
Normal file
@ -0,0 +1,54 @@
|
||||
<script setup lang="ts">
|
||||
import { Head, Link } from '@inertiajs/vue3';
|
||||
import { ArrowLeft } from '@lucide/vue';
|
||||
import { computed } from 'vue';
|
||||
import EmployeeForm from '@/components/hr/EmployeeForm.vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||
import type { EmployeeListItem, EnumOption } from '@/types/employee';
|
||||
|
||||
const props = defineProps<{
|
||||
employee: EmployeeListItem;
|
||||
genders: EnumOption[];
|
||||
employmentStatuses: EnumOption[];
|
||||
}>();
|
||||
|
||||
const initialData = computed(() => ({
|
||||
email: props.employee.email ?? '',
|
||||
username: props.employee.username ?? '',
|
||||
full_name: props.employee.full_name ?? '',
|
||||
phone_number: props.employee.phone_number ?? '',
|
||||
gender: props.employee.gender ?? '',
|
||||
birth_date: props.employee.birth_date ?? '',
|
||||
address: props.employee.address ?? '',
|
||||
join_date: props.employee.join_date ?? '',
|
||||
employment_status: props.employee.employment_status,
|
||||
base_salary: String(props.employee.base_salary),
|
||||
}));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<Head title="Ubah Pegawai" />
|
||||
|
||||
<AdminLayout title="Ubah Pegawai">
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="space-y-1">
|
||||
<h2 class="text-2xl font-bold tracking-tight">
|
||||
Ubah Pegawai
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<Button variant="outline" as-child class="shrink-0 self-start sm:self-center">
|
||||
<Link href="/admin/hr/employees">
|
||||
<ArrowLeft class="size-4" />
|
||||
Kembali
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<EmployeeForm :submit-url="`/admin/hr/employees/${employee.id}`" method="put" submit-label="Perbarui"
|
||||
:initial-data="initialData" :employee-id="employee.id" :employee-name="employee.full_name"
|
||||
:genders="genders" :employment-statuses="employmentStatuses" />
|
||||
</AdminLayout>
|
||||
</template>
|
||||
125
resources/js/pages/admin/hr/employees/Index.vue
Normal file
125
resources/js/pages/admin/hr/employees/Index.vue
Normal file
@ -0,0 +1,125 @@
|
||||
<script setup lang="ts">
|
||||
import { Head, Link } from '@inertiajs/vue3';
|
||||
import { Plus } from '@lucide/vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { DataTable } from '@/components/data-table';
|
||||
import { columns } from '@/components/hr/employees/columns';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { useDataTableQuery, useDataTableQuerySync } from '@/composables/useDataTableQuery';
|
||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||
import type { DataTableFilterDef, DataTableSort } from '@/types/data-table';
|
||||
import type { EnumOption, PaginatedEmployees } from '@/types/employee';
|
||||
|
||||
const props = defineProps<{
|
||||
employees: PaginatedEmployees;
|
||||
filters: {
|
||||
search: string;
|
||||
sort?: string;
|
||||
direction?: 'asc' | 'desc';
|
||||
employment_status?: string;
|
||||
employee_status?: string;
|
||||
};
|
||||
employmentStatuses: EnumOption[];
|
||||
employeeStatuses: EnumOption[];
|
||||
}>();
|
||||
|
||||
const search = ref(props.filters.search ?? '');
|
||||
|
||||
const { query, setSearch, setSort, setFilter, resetFilters, syncFromServer } = useDataTableQuery({
|
||||
url: '/admin/hr/employees',
|
||||
initial: { ...props.filters },
|
||||
filterKeys: ['employment_status', 'employee_status'],
|
||||
});
|
||||
|
||||
useDataTableQuerySync(() => props.filters, syncFromServer);
|
||||
|
||||
const filterDefs = computed<DataTableFilterDef[]>(() => [
|
||||
{
|
||||
key: 'employment_status',
|
||||
label: 'Status Kepegawaian',
|
||||
type: 'select',
|
||||
options: props.employmentStatuses,
|
||||
},
|
||||
{
|
||||
key: 'employee_status',
|
||||
label: 'Status Pegawai',
|
||||
type: 'select',
|
||||
options: props.employeeStatuses,
|
||||
},
|
||||
]);
|
||||
|
||||
const filterValues = computed(() => ({
|
||||
employment_status: query.value.employment_status ?? '',
|
||||
employee_status: query.value.employee_status ?? '',
|
||||
}));
|
||||
|
||||
const currentSort = computed<DataTableSort | null>(() => {
|
||||
if (!query.value.sort || !query.value.direction) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
column: query.value.sort,
|
||||
direction: query.value.direction,
|
||||
};
|
||||
});
|
||||
|
||||
const pagination = computed(() => ({
|
||||
currentPage: props.employees.current_page,
|
||||
perPage: props.employees.per_page,
|
||||
lastPage: props.employees.last_page,
|
||||
total: props.employees.total,
|
||||
}));
|
||||
|
||||
watch(search, (value) => {
|
||||
setSearch(value);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.filters.search,
|
||||
(value) => {
|
||||
search.value = value ?? '';
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<Head title="Pegawai" />
|
||||
|
||||
<AdminLayout>
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="space-y-1">
|
||||
<h2 class="text-2xl font-bold tracking-tight">
|
||||
Pegawai
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<Button as-child class="shrink-0 self-start sm:self-center">
|
||||
<Link href="/admin/hr/employees/create">
|
||||
<Plus class="size-4" />
|
||||
Tambah
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Card class="min-w-0">
|
||||
<CardContent class="min-w-0">
|
||||
<DataTable
|
||||
v-model:search="search"
|
||||
:columns="columns"
|
||||
:data="employees.data"
|
||||
:pagination="pagination"
|
||||
:pagination-links="employees.links"
|
||||
:sort="currentSort"
|
||||
:filter-defs="filterDefs"
|
||||
:filter-values="filterValues"
|
||||
@sort-change="setSort"
|
||||
@filter-change="setFilter"
|
||||
@filters-reset="resetFilters"
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\Admin\DashboardController;
|
||||
use App\Http\Controllers\Admin\Hr\EmployeeController;
|
||||
use App\Http\Controllers\Auth\LoginController;
|
||||
use App\Http\Controllers\Auth\LogoutController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
@ -17,5 +18,15 @@
|
||||
|
||||
Route::prefix('admin')->name('admin.')->group(function () {
|
||||
Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard');
|
||||
|
||||
Route::prefix('hr')->name('hr.')->group(function () {
|
||||
Route::post('employees/{user}/reset-password', [EmployeeController::class, 'resetPassword'])
|
||||
->name('employees.reset-password');
|
||||
Route::patch('employees/{user}/toggle-status', [EmployeeController::class, 'toggleStatus'])
|
||||
->name('employees.toggle-status');
|
||||
Route::resource('employees', EmployeeController::class)
|
||||
->except(['show'])
|
||||
->parameters(['employees' => 'user']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user