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:
parent
5eda6b53e0
commit
50eaced126
@ -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.');
|
||||
|
||||
|
||||
@ -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)) {
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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) {
|
||||
@ -50,5 +50,6 @@ export type EmployeeAdvancePageProps = {
|
||||
search: string;
|
||||
sort?: string;
|
||||
direction?: 'asc' | 'desc';
|
||||
status?: string;
|
||||
};
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user