From 50eaced1263973fd8470a15116e1fcb3058355d3 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Sun, 21 Jun 2026 00:41:56 +0700 Subject: [PATCH] feat: enhance employee advance management by adding status filtering, improving pagination, and implementing new modal components for better user interaction --- .../Finance/EmployeeAdvanceController.php | 27 +++------- .../Finance/EmployeeAdvanceService.php | 51 +++---------------- .../admin/finance/employee-advances/Index.vue | 36 ++++++++++--- .../form}/EmployeeAdvanceFormModal.vue | 0 .../form}/RejectEmployeeAdvanceModal.vue | 0 .../employee-advances/table}/columns.ts | 2 +- .../table}/data-table-actions.vue | 0 resources/js/types/employee-advance.ts | 1 + 8 files changed, 44 insertions(+), 73 deletions(-) rename resources/js/{components/admin/finance/employee-advances => pages/admin/finance/employee-advances/form}/EmployeeAdvanceFormModal.vue (100%) rename resources/js/{components/admin/finance/employee-advances => pages/admin/finance/employee-advances/form}/RejectEmployeeAdvanceModal.vue (100%) rename resources/js/{components/admin/finance/employee-advances => pages/admin/finance/employee-advances/table}/columns.ts (96%) rename resources/js/{components/admin/finance/employee-advances => pages/admin/finance/employee-advances/table}/data-table-actions.vue (100%) diff --git a/app/Http/Controllers/Admin/Finance/EmployeeAdvanceController.php b/app/Http/Controllers/Admin/Finance/EmployeeAdvanceController.php index 9f95b00..d0fcc62 100644 --- a/app/Http/Controllers/Admin/Finance/EmployeeAdvanceController.php +++ b/app/Http/Controllers/Admin/Finance/EmployeeAdvanceController.php @@ -26,16 +26,19 @@ public function __construct( public function index(Request $request): Response { $tableQuery = $this->parseDataTableQuery($request); + $status = $request->string('status')->toString(); $user = $request->user(); return Inertia::render('admin/finance/employee-advances/Index', [ - 'employeeAdvances' => $this->employeeAdvanceService->paginateForIndex($tableQuery, $user), + 'employeeAdvances' => $this->employeeAdvanceService->paginateForIndex($tableQuery, $user, $status), 'summary' => $this->employeeAdvanceService->outstandingSummary($user), 'authEmployeeId' => $user?->employee?->id, 'canSubmit' => $user?->can(Permission::EMPLOYEE_ADVANCES_CREATE->value) && ! $user->can(Permission::EMPLOYEE_ADVANCES_VERIFY->value) && $user->employee !== null, - 'filters' => $this->dataTableFilters($tableQuery), + 'filters' => $this->dataTableFilters($tableQuery, [ + 'status' => $status, + ]), ]); } @@ -59,8 +62,6 @@ public function update(EmployeeAdvanceRequest $request, EmployeeAdvance $employe public function destroy(EmployeeAdvance $employeeAdvance): RedirectResponse { - abort_unless(! auth()->user()?->can(Permission::EMPLOYEE_ADVANCES_VERIFY->value), 403); - $this->employeeAdvanceService->delete($employeeAdvance, auth()->user()); $this->flashDeleted('Kasbon'); @@ -70,12 +71,7 @@ public function destroy(EmployeeAdvance $employeeAdvance): RedirectResponse public function approve(EmployeeAdvance $employeeAdvance): RedirectResponse { - $user = auth()->user(); - if ($user && ! $user->hasAnyRole(['owner', 'developer', 'direktur'])) { - abort(403); - } - - $this->employeeAdvanceService->approve($employeeAdvance, $user); + $this->employeeAdvanceService->approve($employeeAdvance, auth()->user()); $this->flashSuccess('Kasbon berhasil disetujui dan dicairkan dari kas.'); @@ -84,15 +80,10 @@ public function approve(EmployeeAdvance $employeeAdvance): RedirectResponse public function reject(RejectEmployeeAdvanceRequest $request, EmployeeAdvance $employeeAdvance): RedirectResponse { - $user = auth()->user(); - if ($user && ! $user->hasAnyRole(['owner', 'developer', 'direktur'])) { - abort(403); - } - $this->employeeAdvanceService->reject( $employeeAdvance, $request->validated('reason'), - $user, + auth()->user() ); $this->flashSuccess('Kasbon berhasil ditolak.'); @@ -102,9 +93,7 @@ public function reject(RejectEmployeeAdvanceRequest $request, EmployeeAdvance $e public function pay(EmployeeAdvance $employeeAdvance): RedirectResponse { - $user = auth()->user(); - - $this->employeeAdvanceService->pay($employeeAdvance, $user); + $this->employeeAdvanceService->pay($employeeAdvance, auth()->user()); $this->flashSuccess('Kasbon berhasil dilunasi.'); diff --git a/app/Services/Finance/EmployeeAdvanceService.php b/app/Services/Finance/EmployeeAdvanceService.php index 7066fac..1214aed 100644 --- a/app/Services/Finance/EmployeeAdvanceService.php +++ b/app/Services/Finance/EmployeeAdvanceService.php @@ -3,6 +3,7 @@ namespace App\Services\Finance; use App\Enums\EmployeeAdvanceStatus; +use App\Enums\Role; use App\Models\EmployeeAdvance; use App\Models\User; use App\Services\Concerns\ResolvesAuthEmployee; @@ -10,14 +11,11 @@ use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Facades\DB; -use Illuminate\Validation\ValidationException; class EmployeeAdvanceService { use ResolvesAuthEmployee; - private const MAX_PHOTOS = 1; - public function __construct( private readonly CashService $cashService, private readonly PushNotificationService $pushNotificationService, @@ -29,12 +27,8 @@ public function __construct( public function outstandingSummary(?User $user = null): array { $query = EmployeeAdvance::query() - ->where('status', EmployeeAdvanceStatus::APPROVED); - - if ($user !== null && ! $user->hasAnyRole(['owner', 'developer', 'direktur'])) { - $employeeId = $user->employee?->id ?? -1; - $query->where('employee_id', $employeeId); - } + ->where('status', EmployeeAdvanceStatus::APPROVED) + ->when(! $user->hasAnyRole([Role::OWNER->value, Role::DEVELOPER->value, Role::DIREKTUR->value]), fn (Builder $query) => $query->where('employee_id', $user->employee?->id ?? -1)); $outstandingAmount = (int) $query->sum('amount'); @@ -50,14 +44,15 @@ public function outstandingSummary(?User $user = null): array /** * @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery */ - public function paginateForIndex(array $tableQuery, User $user): LengthAwarePaginator + public function paginateForIndex(array $tableQuery, User $user, string $status = ''): LengthAwarePaginator { $query = EmployeeAdvance::query() ->with(['employee.user.profile', 'rejection']) - ->when(! $user->hasAnyRole(['owner', 'developer', 'direktur']), function (Builder $query) use ($user): void { + ->when(! $user->hasAnyRole([Role::OWNER->value, Role::DEVELOPER->value, Role::DIREKTUR->value]), function (Builder $query) use ($user): void { $employeeId = $user->employee?->id ?? -1; $query->where('employee_id', $employeeId); }) + ->when($status !== '', fn (Builder $query) => $query->where('status', $status)) ->when($tableQuery['search'] !== '', function (Builder $query) use ($tableQuery): void { $search = $tableQuery['search']; $query->where(function (Builder $query) use ($search): void { @@ -105,9 +100,6 @@ public function create(array $validated, User $user): void */ public function update(EmployeeAdvance $employeeAdvance, array $validated, User $user): void { - $this->ensureOwnedBySubmitter($employeeAdvance, $user); - $this->ensurePending($employeeAdvance, 'Kasbon hanya dapat diubah saat status menunggu.'); - DB::transaction(function () use ($employeeAdvance, $validated): void { $employeeAdvance->amount = (int) $validated['amount']; $employeeAdvance->description = $validated['description']; @@ -118,16 +110,11 @@ public function update(EmployeeAdvance $employeeAdvance, array $validated, User public function delete(EmployeeAdvance $employeeAdvance, User $user): void { - $this->ensureOwnedBySubmitter($employeeAdvance, $user); - $this->ensurePending($employeeAdvance, 'Kasbon hanya dapat dihapus saat status menunggu.'); - $employeeAdvance->delete(); } public function approve(EmployeeAdvance $employeeAdvance, User $user): void { - $this->ensurePending($employeeAdvance, 'Kasbon ini sudah diverifikasi.'); - DB::transaction(function () use ($employeeAdvance, $user): void { $employeeAdvance->loadMissing('employee.user.profile'); @@ -163,8 +150,6 @@ public function approve(EmployeeAdvance $employeeAdvance, User $user): void public function reject(EmployeeAdvance $employeeAdvance, string $reason, User $user): void { - $this->ensurePending($employeeAdvance, 'Kasbon ini sudah diverifikasi.'); - DB::transaction(function () use ($employeeAdvance, $user, $reason): void { $employeeAdvance->status = EmployeeAdvanceStatus::REJECTED; $employeeAdvance->verified_at = now(); @@ -190,12 +175,6 @@ public function reject(EmployeeAdvance $employeeAdvance, string $reason, User $u public function pay(EmployeeAdvance $employeeAdvance, User $user): void { - if ($employeeAdvance->status !== EmployeeAdvanceStatus::APPROVED) { - throw ValidationException::withMessages([ - 'employee_advance' => 'Kasbon hanya dapat dilunasi setelah disetujui.', - ]); - } - DB::transaction(function () use ($employeeAdvance, $user): void { $employeeAdvance->loadMissing('employee.user.profile'); @@ -219,24 +198,6 @@ public function pay(EmployeeAdvance $employeeAdvance, User $user): void }); } - private function ensureOwnedBySubmitter(EmployeeAdvance $employeeAdvance, User $user): void - { - if ($user->employee?->id !== $employeeAdvance->employee_id) { - throw ValidationException::withMessages([ - 'employee_advance' => 'Anda tidak memiliki akses untuk mengubah kasbon ini.', - ]); - } - } - - private function ensurePending(EmployeeAdvance $employeeAdvance, string $message): void - { - if ($employeeAdvance->status !== EmployeeAdvanceStatus::PENDING) { - throw ValidationException::withMessages([ - 'employee_advance' => $message, - ]); - } - } - private function applySorting(Builder $query, string $sort, string $direction): void { if (in_array($sort, ['created_at', 'amount', 'due_date', 'status', 'description'], true)) { diff --git a/resources/js/pages/admin/finance/employee-advances/Index.vue b/resources/js/pages/admin/finance/employee-advances/Index.vue index 685a399..d646bf4 100644 --- a/resources/js/pages/admin/finance/employee-advances/Index.vue +++ b/resources/js/pages/admin/finance/employee-advances/Index.vue @@ -2,16 +2,16 @@ import { Head } from '@inertiajs/vue3'; import { HandCoins, Plus } from '@lucide/vue'; import { computed, ref, watch } from 'vue'; -import { createColumns } from '@/components/admin/finance/employee-advances/columns'; -import EmployeeAdvanceFormModal from '@/components/admin/finance/employee-advances/EmployeeAdvanceFormModal.vue'; -import RejectEmployeeAdvanceModal from '@/components/admin/finance/employee-advances/RejectEmployeeAdvanceModal.vue'; +import EmployeeAdvanceFormModal from './form/EmployeeAdvanceFormModal.vue'; +import RejectEmployeeAdvanceModal from './form/RejectEmployeeAdvanceModal.vue'; +import { createColumns } from './table/columns'; import { DataTable } from '@/components/data-table'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { useCan } from '@/composables/useCan'; import { useDataTableQuery, useDataTableQuerySync } from '@/composables/useDataTableQuery'; import AdminLayout from '@/layouts/AdminLayout.vue'; -import type { DataTableSort } from '@/types/data-table'; +import type { DataTableFilterDef, DataTableSort } from '@/types/data-table'; import type { EmployeeAdvanceListItem, EmployeeAdvancePageProps } from '@/types/employee-advance'; const props = defineProps(); @@ -23,9 +23,10 @@ const rejectModalOpen = ref(false); const editingEmployeeAdvance = ref(null); const rejectingEmployeeAdvance = ref(null); -const { query, setSearch, setSort, resetFilters, syncFromServer } = useDataTableQuery({ +const { query, setSearch, setSort, setFilter, resetFilters, syncFromServer } = useDataTableQuery({ url: '/admin/finance/employee-advances', initial: { ...props.filters }, + filterKeys: ['status'], }); useDataTableQuerySync(() => props.filters, syncFromServer); @@ -47,6 +48,24 @@ const currentSort = computed(() => { }; }); +const filterDefs = computed(() => [ + { + key: 'status', + label: 'Status', + type: 'select', + options: [ + { value: 'pending', label: 'Menunggu' }, + { value: 'approved', label: 'Disetujui' }, + { value: 'rejected', label: 'Ditolak' }, + { value: 'paid', label: 'Lunas' }, + ], + }, +]); + +const filterValues = computed(() => ({ + status: query.value.status ?? '', +})); + const pagination = computed(() => ({ currentPage: props.employeeAdvances.current_page, perPage: props.employeeAdvances.per_page, @@ -102,7 +121,7 @@ watch( - Total Piutang Kasbon + Total Kasbon @@ -111,7 +130,7 @@ watch( {{ summary.outstanding_amount_formatted }}

- {{ summary.outstanding_count }} kasbon belum lunas + {{ summary.outstanding_count }} pegawai yang berhutang

@@ -120,7 +139,8 @@ watch( + :filter-defs="filterDefs" :filter-values="filterValues" @sort-change="setSort" + @filter-change="setFilter" @filters-reset="resetFilters" /> diff --git a/resources/js/components/admin/finance/employee-advances/EmployeeAdvanceFormModal.vue b/resources/js/pages/admin/finance/employee-advances/form/EmployeeAdvanceFormModal.vue similarity index 100% rename from resources/js/components/admin/finance/employee-advances/EmployeeAdvanceFormModal.vue rename to resources/js/pages/admin/finance/employee-advances/form/EmployeeAdvanceFormModal.vue diff --git a/resources/js/components/admin/finance/employee-advances/RejectEmployeeAdvanceModal.vue b/resources/js/pages/admin/finance/employee-advances/form/RejectEmployeeAdvanceModal.vue similarity index 100% rename from resources/js/components/admin/finance/employee-advances/RejectEmployeeAdvanceModal.vue rename to resources/js/pages/admin/finance/employee-advances/form/RejectEmployeeAdvanceModal.vue diff --git a/resources/js/components/admin/finance/employee-advances/columns.ts b/resources/js/pages/admin/finance/employee-advances/table/columns.ts similarity index 96% rename from resources/js/components/admin/finance/employee-advances/columns.ts rename to resources/js/pages/admin/finance/employee-advances/table/columns.ts index 6a280d0..68e3514 100644 --- a/resources/js/components/admin/finance/employee-advances/columns.ts +++ b/resources/js/pages/admin/finance/employee-advances/table/columns.ts @@ -1,9 +1,9 @@ import type { ColumnDef } from '@tanstack/vue-table'; import { h } from 'vue'; -import DataTableActions from '@/components/admin/finance/employee-advances/data-table-actions.vue'; import { DataTableColumnHeader } from '@/components/data-table'; import { Badge } from '@/components/ui/badge'; import type { EmployeeAdvanceListItem } from '@/types/employee-advance'; +import DataTableActions from './data-table-actions.vue'; function statusVariant(status: string): 'default' | 'secondary' | 'destructive' | 'outline' { switch (status) { diff --git a/resources/js/components/admin/finance/employee-advances/data-table-actions.vue b/resources/js/pages/admin/finance/employee-advances/table/data-table-actions.vue similarity index 100% rename from resources/js/components/admin/finance/employee-advances/data-table-actions.vue rename to resources/js/pages/admin/finance/employee-advances/table/data-table-actions.vue diff --git a/resources/js/types/employee-advance.ts b/resources/js/types/employee-advance.ts index cb7b4c9..816686e 100644 --- a/resources/js/types/employee-advance.ts +++ b/resources/js/types/employee-advance.ts @@ -50,5 +50,6 @@ export type EmployeeAdvancePageProps = { search: string; sort?: string; direction?: 'asc' | 'desc'; + status?: string; }; };