feat: enhance employee advance management by adding status filtering, improving pagination, and implementing new modal components for better user interaction

This commit is contained in:
Yoga Pangestu 2026-06-21 00:41:56 +07:00
parent 5eda6b53e0
commit 50eaced126
8 changed files with 44 additions and 73 deletions

View File

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

View File

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

View File

@ -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<EmployeeAdvancePageProps>();
@ -23,9 +23,10 @@ const rejectModalOpen = ref(false);
const editingEmployeeAdvance = ref<EmployeeAdvanceListItem | null>(null);
const rejectingEmployeeAdvance = ref<EmployeeAdvanceListItem | null>(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<DataTableSort | null>(() => {
};
});
const filterDefs = computed<DataTableFilterDef[]>(() => [
{
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(
<Card v-if="can('employee-advances.verify')">
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle class="text-sm font-medium text-muted-foreground">
Total Piutang Kasbon
Total Kasbon
</CardTitle>
<HandCoins class="size-4 text-muted-foreground" />
</CardHeader>
@ -111,7 +130,7 @@ watch(
{{ summary.outstanding_amount_formatted }}
</div>
<p class="mt-1 text-sm text-muted-foreground">
{{ summary.outstanding_count }} kasbon belum lunas
{{ summary.outstanding_count }} pegawai yang berhutang
</p>
</CardContent>
</Card>
@ -120,7 +139,8 @@ watch(
<CardContent class="min-w-0 pt-6">
<DataTable v-model:search="search" :columns="columns" :data="employeeAdvances.data"
:pagination="pagination" :pagination-links="employeeAdvances.links" :sort="currentSort"
@sort-change="setSort" @filters-reset="resetFilters" />
:filter-defs="filterDefs" :filter-values="filterValues" @sort-change="setSort"
@filter-change="setFilter" @filters-reset="resetFilters" />
</CardContent>
</Card>

View File

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

View File

@ -50,5 +50,6 @@ export type EmployeeAdvancePageProps = {
search: string;
sort?: string;
direction?: 'asc' | 'desc';
status?: string;
};
};