feat: refactor EmployeeController and EmployeeService to utilize Role enum for role options; implement transaction management in EmployeeService methods for create, update, toggle status, and delete operations; enhance Employee model date formatting methods; update frontend components for improved UI and code clarity
This commit is contained in:
parent
786d0fdb39
commit
f1c3658166
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
use App\Enums\EmploymentStatus;
|
use App\Enums\EmploymentStatus;
|
||||||
use App\Enums\Gender;
|
use App\Enums\Gender;
|
||||||
|
use App\Enums\Role;
|
||||||
use App\Http\Controllers\Concerns\FlashesEntityMessage;
|
use App\Http\Controllers\Concerns\FlashesEntityMessage;
|
||||||
use App\Http\Controllers\Concerns\ParsesDataTableQuery;
|
use App\Http\Controllers\Concerns\ParsesDataTableQuery;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
@ -46,7 +47,7 @@ public function index(Request $request): Response
|
|||||||
'employment_status' => $employmentStatus,
|
'employment_status' => $employmentStatus,
|
||||||
'is_active' => $isActive,
|
'is_active' => $isActive,
|
||||||
]),
|
]),
|
||||||
'roles' => $this->employeeService->assignableRoleOptions(),
|
'roles' => Role::assignableSelectOptions(),
|
||||||
'genders' => Gender::selectOptions(),
|
'genders' => Gender::selectOptions(),
|
||||||
'employmentStatuses' => EmploymentStatus::selectOptions(),
|
'employmentStatuses' => EmploymentStatus::selectOptions(),
|
||||||
]);
|
]);
|
||||||
@ -57,7 +58,7 @@ public function create(): Response
|
|||||||
return Inertia::render('admin/hr/employees/Create', [
|
return Inertia::render('admin/hr/employees/Create', [
|
||||||
'genders' => Gender::selectOptions(),
|
'genders' => Gender::selectOptions(),
|
||||||
'employmentStatuses' => EmploymentStatus::selectOptions(),
|
'employmentStatuses' => EmploymentStatus::selectOptions(),
|
||||||
'roles' => $this->employeeService->assignableRoleOptions(),
|
'roles' => Role::assignableSelectOptions(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,7 +78,7 @@ public function edit(User $user): Response
|
|||||||
return Inertia::render('admin/hr/employees/Edit', [
|
return Inertia::render('admin/hr/employees/Edit', [
|
||||||
'genders' => Gender::selectOptions(),
|
'genders' => Gender::selectOptions(),
|
||||||
'employmentStatuses' => EmploymentStatus::selectOptions(),
|
'employmentStatuses' => EmploymentStatus::selectOptions(),
|
||||||
'roles' => $this->employeeService->assignableRoleOptions(),
|
'roles' => Role::assignableSelectOptions(),
|
||||||
'employee' => $editData['employee'],
|
'employee' => $editData['employee'],
|
||||||
'profilePhoto' => $editData['profilePhoto'],
|
'profilePhoto' => $editData['profilePhoto'],
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -78,7 +78,7 @@ public function employmentStatusLabel(): Attribute
|
|||||||
public function joinDateFormatted(): Attribute
|
public function joinDateFormatted(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::make(
|
return Attribute::make(
|
||||||
get: fn () => $this->join_date ? Carbon::parse($this->join_date)->translatedFormat('l, d F Y') : '-',
|
get: fn () => $this->join_date?->translatedFormat('l, d F Y') ?? '-',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,7 +92,7 @@ public function joinDateInput(): Attribute
|
|||||||
public function resignDateFormatted(): Attribute
|
public function resignDateFormatted(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::make(
|
return Attribute::make(
|
||||||
get: fn () => $this->resign_date ? Carbon::parse($this->resign_date)->translatedFormat('l, d F Y') : null,
|
get: fn () => $this->resign_date?->translatedFormat('l, d F Y'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,8 @@
|
|||||||
use App\Models\Employee;
|
use App\Models\Employee;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Models\UserProfile;
|
use App\Models\UserProfile;
|
||||||
|
use App\Services\Concerns\RunsInTransaction;
|
||||||
|
use App\Services\Concerns\SyncsPhotos;
|
||||||
use App\Services\Media\MediaService;
|
use App\Services\Media\MediaService;
|
||||||
use App\Support\Media\MediaPresenter;
|
use App\Support\Media\MediaPresenter;
|
||||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||||
@ -17,6 +19,8 @@
|
|||||||
|
|
||||||
class EmployeeService
|
class EmployeeService
|
||||||
{
|
{
|
||||||
|
use RunsInTransaction, SyncsPhotos;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly MediaService $mediaService,
|
private readonly MediaService $mediaService,
|
||||||
) {}
|
) {}
|
||||||
@ -75,8 +79,8 @@ public function findForEdit(User $user): array
|
|||||||
|
|
||||||
public function create(array $validated): void
|
public function create(array $validated): void
|
||||||
{
|
{
|
||||||
try {
|
$this->runInTransaction(
|
||||||
DB::transaction(function () use ($validated): void {
|
function () use ($validated): void {
|
||||||
$user = User::create([
|
$user = User::create([
|
||||||
'email' => $validated['email'],
|
'email' => $validated['email'],
|
||||||
'username' => $validated['username'],
|
'username' => $validated['username'],
|
||||||
@ -104,26 +108,17 @@ public function create(array $validated): void
|
|||||||
}
|
}
|
||||||
|
|
||||||
$user->syncRoles([$validated['role']]);
|
$user->syncRoles([$validated['role']]);
|
||||||
});
|
},
|
||||||
} catch (ValidationException $e) {
|
'Gagal membuat karyawan',
|
||||||
throw $e;
|
);
|
||||||
} catch (\Throwable $e) {
|
|
||||||
Log::error('Gagal membuat karyawan: '.$e->getMessage(), [
|
|
||||||
'trace' => $e->getTraceAsString(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
throw ValidationException::withMessages([
|
|
||||||
'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update(User $user, array $validated): void
|
public function update(User $user, array $validated): void
|
||||||
{
|
{
|
||||||
$employee = $user->employee;
|
$employee = $user->employee;
|
||||||
|
|
||||||
try {
|
$this->runInTransaction(
|
||||||
DB::transaction(function () use ($validated, $user, $employee): void {
|
function () use ($validated, $user, $employee): void {
|
||||||
$user->update([
|
$user->update([
|
||||||
'email' => $validated['email'],
|
'email' => $validated['email'],
|
||||||
'username' => $validated['username'],
|
'username' => $validated['username'],
|
||||||
@ -166,73 +161,65 @@ public function update(User $user, array $validated): void
|
|||||||
}
|
}
|
||||||
|
|
||||||
$user->syncRoles([$validated['role']]);
|
$user->syncRoles([$validated['role']]);
|
||||||
});
|
},
|
||||||
} catch (ValidationException $e) {
|
'Gagal memperbarui karyawan',
|
||||||
throw $e;
|
);
|
||||||
} catch (\Throwable $e) {
|
|
||||||
Log::error('Gagal memperbarui karyawan: '.$e->getMessage(), [
|
|
||||||
'trace' => $e->getTraceAsString(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
throw ValidationException::withMessages([
|
|
||||||
'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function toggleStatus(User $user, array $validated): void
|
public function toggleStatus(User $user, array $validated): void
|
||||||
{
|
{
|
||||||
$user->update([
|
$this->runInTransaction(
|
||||||
'is_active' => $validated['is_active'],
|
function () use ($user, $validated): void {
|
||||||
]);
|
$user->update([
|
||||||
|
'is_active' => $validated['is_active'],
|
||||||
|
]);
|
||||||
|
|
||||||
if (! $validated['is_active']) {
|
if (! $validated['is_active']) {
|
||||||
DB::table('sessions')->where('user_id', $user->id)->delete();
|
DB::table('sessions')->where('user_id', $user->id)->delete();
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
'Gagal memperbarui status karyawan',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function resetPassword(User $user): void
|
public function resetPassword(User $user): void
|
||||||
{
|
{
|
||||||
$user->update([
|
$this->runInTransaction(
|
||||||
'password' => config('auth.password_default'),
|
function () use ($user): void {
|
||||||
]);
|
$user->update([
|
||||||
|
'password' => config('auth.password_default'),
|
||||||
|
]);
|
||||||
|
|
||||||
DB::table('sessions')->where('user_id', $user->id)->delete();
|
DB::table('sessions')->where('user_id', $user->id)->delete();
|
||||||
|
},
|
||||||
|
'Gagal mereset kata sandi karyawan',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete(User $user): void
|
public function delete(User $user): void
|
||||||
{
|
{
|
||||||
try {
|
$this->runInTransaction(
|
||||||
DB::transaction(function () use ($user): void {
|
function () use ($user): void {
|
||||||
$user->employee?->delete();
|
$user->employee?->delete();
|
||||||
$user->profile?->delete();
|
$user->profile?->delete();
|
||||||
$user->delete();
|
$user->delete();
|
||||||
});
|
},
|
||||||
} catch (ValidationException $e) {
|
'Gagal menghapus karyawan',
|
||||||
throw $e;
|
);
|
||||||
} catch (\Throwable $e) {
|
|
||||||
Log::error('Gagal menghapus karyawan: '.$e->getMessage(), [
|
|
||||||
'trace' => $e->getTraceAsString(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
throw ValidationException::withMessages([
|
|
||||||
'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function syncProfilePhoto(UserProfile $profile, array $validated): void
|
private function syncProfilePhoto(UserProfile $profile, array $validated): void
|
||||||
{
|
{
|
||||||
$s3Keys = ! empty($validated['profile_s3_key']) ? [$validated['profile_s3_key']] : null;
|
$this->syncPhotos(
|
||||||
$removeIds = $validated['remove_profile_photo_ids'] ?? null;
|
|
||||||
|
|
||||||
$this->mediaService->syncCollection(
|
|
||||||
$profile,
|
$profile,
|
||||||
'profile_photo',
|
[
|
||||||
null,
|
'photos' => $validated['profile_photo'] ?? null,
|
||||||
$removeIds,
|
'remove_media_ids' => $validated['remove_profile_photo_ids'] ?? null,
|
||||||
1,
|
's3_keys' => ! empty($validated['profile_s3_key']) ? [$validated['profile_s3_key']] : null,
|
||||||
s3Keys: $s3Keys,
|
],
|
||||||
|
maxPhotos: 1,
|
||||||
|
required: false,
|
||||||
|
collection: 'profile_photo',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Head } from '@inertiajs/vue3';
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
import CreateButton from '@/components/button/CreateButton.vue';
|
import CreateButton from '@/components/button/CreateButton.vue';
|
||||||
import { DataTable } from '@/components/data-table';
|
import { DataTable } from '@/components/data-table';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
@ -9,12 +11,10 @@ import {
|
|||||||
} from '@/composables/useDataTableQuery';
|
} from '@/composables/useDataTableQuery';
|
||||||
import { ActiveStatus } from '@/constants/active-status';
|
import { ActiveStatus } from '@/constants/active-status';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
|
import { index, create } from '@/routes/admin/hr/employees';
|
||||||
import type { DataTableFilterDef, DataTableSort } from '@/types/data-table';
|
import type { DataTableFilterDef, DataTableSort } from '@/types/data-table';
|
||||||
import type { EnumOption, PaginatedEmployees } from '@/types/employee';
|
import type { EnumOption, PaginatedEmployees } from '@/types/employee';
|
||||||
import { Head } from '@inertiajs/vue3';
|
|
||||||
import { computed, ref, watch } from 'vue';
|
|
||||||
import { columns } from './table/columns';
|
import { columns } from './table/columns';
|
||||||
import { index, create } from '@/routes/admin/hr/employees';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
employees: PaginatedEmployees;
|
employees: PaginatedEmployees;
|
||||||
@ -112,37 +112,24 @@ watch(
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
||||||
<Head title="Pegawai" />
|
<Head title="Pegawai" />
|
||||||
|
|
||||||
<AdminLayout>
|
<AdminLayout>
|
||||||
<div
|
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"
|
|
||||||
>
|
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<h2 class="text-2xl font-bold tracking-tight">Pegawai</h2>
|
<h2 class="text-2xl font-bold tracking-tight">Pegawai</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CreateButton
|
<CreateButton v-if="can('employees.create')" :href="create.url()" />
|
||||||
v-if="can('employees.create')"
|
|
||||||
:href="create.url()"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card class="min-w-0">
|
<Card class="min-w-0">
|
||||||
<CardContent class="min-w-0">
|
<CardContent class="min-w-0">
|
||||||
<DataTable
|
<DataTable v-model:search="search" :columns="columns" :data="employees.data" :pagination="pagination"
|
||||||
v-model:search="search"
|
:pagination-links="employees.links" :sort="currentSort" :filter-defs="filterDefs"
|
||||||
:columns="columns"
|
:filter-values="filterValues" @sort-change="setSort" @filter-change="setFilter"
|
||||||
:data="employees.data"
|
@filters-reset="resetFilters" />
|
||||||
:pagination="pagination"
|
|
||||||
:pagination-links="employees.links"
|
|
||||||
:sort="currentSort"
|
|
||||||
:filter-defs="filterDefs"
|
|
||||||
:filter-values="filterValues"
|
|
||||||
@sort-change="setSort"
|
|
||||||
@filter-change="setFilter"
|
|
||||||
@filters-reset="resetFilters"
|
|
||||||
/>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</AdminLayout>
|
</AdminLayout>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useForm } from '@inertiajs/vue3';
|
import { useForm } from '@inertiajs/vue3';
|
||||||
import { Save } from '@lucide/vue';
|
import { Save } from '@lucide/vue';
|
||||||
import { ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { toast } from 'vue-sonner';
|
import { toast } from 'vue-sonner';
|
||||||
import { PhoneNumberInput } from '@/components/form/phone-number-input';
|
import { PhoneNumberInput } from '@/components/form/phone-number-input';
|
||||||
import { RupiahInput } from '@/components/form/rupiah-input';
|
import { RupiahInput } from '@/components/form/rupiah-input';
|
||||||
@ -131,8 +131,8 @@ function submit() {
|
|||||||
<FieldSet class="grid gap-4 md:grid-cols-3">
|
<FieldSet class="grid gap-4 md:grid-cols-3">
|
||||||
<Field>
|
<Field>
|
||||||
<FieldLabel for="email" required>Email</FieldLabel>
|
<FieldLabel for="email" required>Email</FieldLabel>
|
||||||
<Input id="email" v-model="form.email" type="email"
|
<Input id="email" v-model="form.email" type="email" placeholder="Masukkan email"
|
||||||
placeholder="Masukkan email" :maxlength="FIELD_LIMITS.email" />
|
:maxlength="FIELD_LIMITS.email" />
|
||||||
<FieldError :errors="formErrors(form, 'email')" />
|
<FieldError :errors="formErrors(form, 'email')" />
|
||||||
</Field>
|
</Field>
|
||||||
<Field>
|
<Field>
|
||||||
@ -201,8 +201,7 @@ function submit() {
|
|||||||
:errors="formErrors(form, 'profile_photo')" class="md:col-span-3" />
|
:errors="formErrors(form, 'profile_photo')" class="md:col-span-3" />
|
||||||
<Field class="md:col-span-3">
|
<Field class="md:col-span-3">
|
||||||
<FieldLabel for="address">Alamat</FieldLabel>
|
<FieldLabel for="address">Alamat</FieldLabel>
|
||||||
<Textarea id="address" v-model="form.address"
|
<Textarea id="address" v-model="form.address" placeholder="Masukkan alamat" rows="3" />
|
||||||
placeholder="Masukkan alamat" rows="3" />
|
|
||||||
<FieldError :errors="formErrors(form, 'address')" />
|
<FieldError :errors="formErrors(form, 'address')" />
|
||||||
</Field>
|
</Field>
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import type { ColumnDef } from '@tanstack/vue-table';
|
|||||||
import { h } from 'vue';
|
import { h } from 'vue';
|
||||||
import { DataTableColumnHeader } from '@/components/data-table';
|
import { DataTableColumnHeader } from '@/components/data-table';
|
||||||
import MediaThumbnailCell from '@/components/media/MediaThumbnailCell.vue';
|
import MediaThumbnailCell from '@/components/media/MediaThumbnailCell.vue';
|
||||||
|
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import type { EmployeeListItem } from '@/types/employee';
|
import type { EmployeeListItem } from '@/types/employee';
|
||||||
import type { MediaItem } from '@/types/media';
|
import type { MediaItem } from '@/types/media';
|
||||||
@ -9,24 +10,24 @@ import DataTableActions from './data-table-actions.vue';
|
|||||||
import EmployeeStatusToggle from './employee-status-toggle.vue';
|
import EmployeeStatusToggle from './employee-status-toggle.vue';
|
||||||
|
|
||||||
export const columns: ColumnDef<EmployeeListItem>[] = [
|
export const columns: ColumnDef<EmployeeListItem>[] = [
|
||||||
{
|
|
||||||
id: 'photo',
|
|
||||||
enableSorting: false,
|
|
||||||
header: () => 'Foto',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const photoUrl = row.original.profile?.profile_photo_url;
|
|
||||||
const items: MediaItem[] = photoUrl
|
|
||||||
? [{ id: 0, url: photoUrl, thumb_url: photoUrl }]
|
|
||||||
: [];
|
|
||||||
|
|
||||||
return h(MediaThumbnailCell, { items, maxVisible: 1 });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
accessorKey: 'profile.full_name',
|
accessorKey: 'profile.full_name',
|
||||||
enableSorting: true,
|
enableSorting: true,
|
||||||
header: () => h(DataTableColumnHeader, { title: 'Nama', column: 'full_name' }),
|
header: () => h(DataTableColumnHeader, { title: 'Nama', column: 'full_name' }),
|
||||||
cell: ({ row }) => row.original.profile?.full_name ?? '-',
|
cell: ({ row }) => {
|
||||||
|
const photoUrl = row.original.profile?.profile_photo_url;
|
||||||
|
const fullName = row.original.profile?.full_name ?? '-';
|
||||||
|
|
||||||
|
const photoEl = photoUrl
|
||||||
|
? h('div', { class: 'size-8 overflow-hidden rounded-full' }, [
|
||||||
|
h(MediaThumbnailCell, { items: [{ id: 0, url: photoUrl, thumb_url: photoUrl } as MediaItem], maxVisible: 1 }),
|
||||||
|
])
|
||||||
|
: h(Avatar, { class: 'size-8' }, () => [
|
||||||
|
h(AvatarFallback, () => (row.original.username ?? '').slice(0, 2).toUpperCase()),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return h('div', { class: 'flex items-center gap-2' }, [photoEl, h('span', {}, fullName)]);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'account',
|
id: 'account',
|
||||||
|
|||||||
@ -628,13 +628,7 @@
|
|||||||
Route::prefix('employees')->name('employees.')
|
Route::prefix('employees')->name('employees.')
|
||||||
->middleware('permission:'.Permission::EMPLOYEES_VIEW->value)
|
->middleware('permission:'.Permission::EMPLOYEES_VIEW->value)
|
||||||
->group(function () {
|
->group(function () {
|
||||||
Route::post('{user}/reset-password', [EmployeeController::class, 'resetPassword'])
|
Route::get('/', [EmployeeController::class, 'index'])->name('index');
|
||||||
->middleware('permission:'.Permission::EMPLOYEES_RESET_PASSWORD->value)
|
|
||||||
->name('reset_password');
|
|
||||||
|
|
||||||
Route::patch('{user}/toggle-status', [EmployeeController::class, 'toggleStatus'])
|
|
||||||
->middleware('permission:'.Permission::EMPLOYEES_TOGGLE_STATUS->value)
|
|
||||||
->name('toggle_status');
|
|
||||||
|
|
||||||
Route::get('create', [EmployeeController::class, 'create'])
|
Route::get('create', [EmployeeController::class, 'create'])
|
||||||
->middleware('permission:'.Permission::EMPLOYEES_CREATE->value)
|
->middleware('permission:'.Permission::EMPLOYEES_CREATE->value)
|
||||||
@ -656,7 +650,13 @@
|
|||||||
->middleware('permission:'.Permission::EMPLOYEES_DELETE->value)
|
->middleware('permission:'.Permission::EMPLOYEES_DELETE->value)
|
||||||
->name('destroy');
|
->name('destroy');
|
||||||
|
|
||||||
Route::get('/', [EmployeeController::class, 'index'])->name('index');
|
Route::post('{user}/reset-password', [EmployeeController::class, 'resetPassword'])
|
||||||
|
->middleware('permission:'.Permission::EMPLOYEES_RESET_PASSWORD->value)
|
||||||
|
->name('reset_password');
|
||||||
|
|
||||||
|
Route::patch('{user}/toggle-status', [EmployeeController::class, 'toggleStatus'])
|
||||||
|
->middleware('permission:'.Permission::EMPLOYEES_TOGGLE_STATUS->value)
|
||||||
|
->name('toggle_status');
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::prefix('leave-requests')->name('leave_requests.')
|
Route::prefix('leave-requests')->name('leave_requests.')
|
||||||
|
|||||||
@ -6,11 +6,15 @@
|
|||||||
use App\Models\UserProfile;
|
use App\Models\UserProfile;
|
||||||
use Database\Seeders\RolePermissionSeeder;
|
use Database\Seeders\RolePermissionSeeder;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
uses(RefreshDatabase::class);
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
$this->seed(RolePermissionSeeder::class);
|
$this->seed(RolePermissionSeeder::class);
|
||||||
|
$disk = config('filesystems.default') === 's3' ? 's3' : config('filesystems.default', 'public');
|
||||||
|
Storage::fake($disk);
|
||||||
|
Storage::fake('public');
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── Helper ───────────────────────────────────────────────
|
// ─── Helper ───────────────────────────────────────────────
|
||||||
@ -41,7 +45,7 @@ function createEmployeeWithProfile(): User
|
|||||||
'gender' => fake()->randomElement(['male', 'female']),
|
'gender' => fake()->randomElement(['male', 'female']),
|
||||||
]);
|
]);
|
||||||
Employee::factory()->create(['user_id' => $user->id]);
|
Employee::factory()->create(['user_id' => $user->id]);
|
||||||
$user->assignRole('marketing');
|
$user->assignRole('marketing-offline');
|
||||||
|
|
||||||
return $user;
|
return $user;
|
||||||
}
|
}
|
||||||
@ -70,7 +74,7 @@ function validEmployeePayload(): array
|
|||||||
'gender' => 'male',
|
'gender' => 'male',
|
||||||
'birth_date' => '1995-01-15',
|
'birth_date' => '1995-01-15',
|
||||||
'address' => 'Jl. Merdeka No. 1',
|
'address' => 'Jl. Merdeka No. 1',
|
||||||
'role' => 'marketing',
|
'role' => 'marketing-offline',
|
||||||
'join_date' => '2024-01-01',
|
'join_date' => '2024-01-01',
|
||||||
'employment_status' => 'full_time',
|
'employment_status' => 'full_time',
|
||||||
'base_salary' => 5000000,
|
'base_salary' => 5000000,
|
||||||
@ -129,7 +133,7 @@ function validEmployeePayload(): array
|
|||||||
createEmployeeWithProfile();
|
createEmployeeWithProfile();
|
||||||
|
|
||||||
$this->actingAs($user)
|
$this->actingAs($user)
|
||||||
->get(route('admin.hr.employees.index', ['role' => 'marketing']))
|
->get(route('admin.hr.employees.index', ['role' => 'marketing-offline']))
|
||||||
->assertOk();
|
->assertOk();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -347,6 +351,24 @@ function validEmployeePayload(): array
|
|||||||
expect($newUser->profile->full_name)->toBe('Pegawai Baru');
|
expect($newUser->profile->full_name)->toBe('Pegawai Baru');
|
||||||
expect($newUser->employee->base_salary)->toBe(5000000);
|
expect($newUser->employee->base_salary)->toBe(5000000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('creating employee with profile photo s3 key links the photo', function () {
|
||||||
|
$user = createEmployeeUserWithPermission(PermissionEnum::EMPLOYEES_VIEW, PermissionEnum::EMPLOYEES_CREATE);
|
||||||
|
|
||||||
|
$disk = config('filesystems.default') === 's3' ? 's3' : config('filesystems.default', 'public');
|
||||||
|
$imageContent = \Illuminate\Http\UploadedFile::fake()->image('avatar.jpg')->get();
|
||||||
|
Storage::disk($disk)->put('fake-s3-key.jpg', $imageContent);
|
||||||
|
|
||||||
|
$payload = validEmployeePayload();
|
||||||
|
$payload['profile_s3_key'] = 'fake-s3-key.jpg';
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->post(route('admin.hr.employees.store'), $payload);
|
||||||
|
|
||||||
|
$newUser = User::where('email', 'pegawai@example.com')->first();
|
||||||
|
expect($newUser)->not->toBeNull();
|
||||||
|
expect($newUser->profile->getMedia('profile_photo')->count())->toBe(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── Edit ─────────────────────────────────────────────────
|
// ─── Edit ─────────────────────────────────────────────────
|
||||||
@ -430,6 +452,34 @@ function validEmployeePayload(): array
|
|||||||
->put(route('admin.hr.employees.update', $employee), $payload)
|
->put(route('admin.hr.employees.update', $employee), $payload)
|
||||||
->assertRedirect(route('admin.hr.employees.index'));
|
->assertRedirect(route('admin.hr.employees.index'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('updating employee can upload new photo and delete existing photo', function () {
|
||||||
|
$user = createEmployeeUserWithPermission(PermissionEnum::EMPLOYEES_VIEW, PermissionEnum::EMPLOYEES_UPDATE);
|
||||||
|
|
||||||
|
$employee = createEmployeeWithProfile();
|
||||||
|
|
||||||
|
$imageContent = \Illuminate\Http\UploadedFile::fake()->image('initial.jpg')->get();
|
||||||
|
Storage::disk('public')->put('initial.jpg', $imageContent);
|
||||||
|
$employee->profile->addMediaFromDisk('initial.jpg', 'public')->toMediaCollection('profile_photo');
|
||||||
|
expect($employee->profile->fresh()->getMedia('profile_photo')->count())->toBe(1);
|
||||||
|
$mediaId = $employee->profile->fresh()->getFirstMedia('profile_photo')->id;
|
||||||
|
|
||||||
|
$disk = config('filesystems.default') === 's3' ? 's3' : config('filesystems.default', 'public');
|
||||||
|
$imageContent2 = \Illuminate\Http\UploadedFile::fake()->image('updated.jpg')->get();
|
||||||
|
Storage::disk($disk)->put('updated.jpg', $imageContent2);
|
||||||
|
$payload = validEmployeePayload();
|
||||||
|
$payload['email'] = $employee->email;
|
||||||
|
$payload['username'] = $employee->username;
|
||||||
|
$payload['profile_s3_key'] = 'updated.jpg';
|
||||||
|
$payload['remove_profile_photo_ids'] = [$mediaId];
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->put(route('admin.hr.employees.update', $employee), $payload);
|
||||||
|
|
||||||
|
$freshProfile = $employee->fresh()->profile;
|
||||||
|
expect($freshProfile->getMedia('profile_photo')->count())->toBe(1);
|
||||||
|
expect($freshProfile->getFirstMedia('profile_photo')->id)->not->toBe($mediaId);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── Toggle Status ────────────────────────────────────────
|
// ─── Toggle Status ────────────────────────────────────────
|
||||||
@ -565,3 +615,36 @@ function validEmployeePayload(): array
|
|||||||
$this->assertSoftDeleted('employees', ['id' => $employeeId]);
|
$this->assertSoftDeleted('employees', ['id' => $employeeId]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ─── Employee Model ───────────────────────────────────────
|
||||||
|
|
||||||
|
describe('Employee Model Attributes', function () {
|
||||||
|
test('join_date_formatted returns translated formatted date or fallback', function () {
|
||||||
|
$employee = Employee::factory()->create(['join_date' => '2024-01-15']);
|
||||||
|
expect($employee->join_date_formatted)->toBe('Senin, 15 Januari 2024');
|
||||||
|
|
||||||
|
$employeeNull = Employee::factory()->make(['join_date' => null]);
|
||||||
|
expect($employeeNull->join_date_formatted)->toBe('-');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resign_date_formatted returns translated formatted date or null', function () {
|
||||||
|
$employee = Employee::factory()->create(['resign_date' => '2024-06-20']);
|
||||||
|
expect($employee->resign_date_formatted)->toBe('Kamis, 20 Juni 2024');
|
||||||
|
|
||||||
|
$employeeNull = Employee::factory()->make(['resign_date' => null]);
|
||||||
|
expect($employeeNull->resign_date_formatted)->toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('join_date_input returns date formatted as Y-m-d', function () {
|
||||||
|
$employee = Employee::factory()->create(['join_date' => '2024-01-15']);
|
||||||
|
expect($employee->join_date_input)->toBe('2024-01-15');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('base_salary_formatted returns formatted rupiah or fallback', function () {
|
||||||
|
$employee = Employee::factory()->create(['base_salary' => 5000000]);
|
||||||
|
expect($employee->base_salary_formatted)->toBe('Rp 5.000.000');
|
||||||
|
|
||||||
|
$employeeNull = Employee::factory()->make(['base_salary' => null]);
|
||||||
|
expect($employeeNull->base_salary_formatted)->toBe('-');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user