Add expenses management features including Expense model, ExpenseController for CRUD operations, and corresponding permissions in the Permission and Role enums. Update routes for expenses functionality and enhance UI components for expenses navigation in the sidebar.

This commit is contained in:
Yoga Pangestu 2026-06-10 23:56:17 +07:00
parent 0e8166a7ff
commit e2f0feed9d
17 changed files with 748 additions and 2 deletions

View File

@ -49,6 +49,11 @@ enum Permission: string
case CASH_UPDATE = 'cash.update';
case CASH_DELETE = 'cash.delete';
case EXPENSES_VIEW = 'expenses.view';
case EXPENSES_CREATE = 'expenses.create';
case EXPENSES_UPDATE = 'expenses.update';
case EXPENSES_DELETE = 'expenses.delete';
public function label(): string
{
return match ($this) {
@ -92,6 +97,11 @@ public function label(): string
self::CASH_DEPOSIT => 'Setor Kas',
self::CASH_UPDATE => 'Ubah Setor Kas',
self::CASH_DELETE => 'Hapus Setor Kas',
self::EXPENSES_VIEW => 'Lihat Pengeluaran',
self::EXPENSES_CREATE => 'Tambah Pengeluaran',
self::EXPENSES_UPDATE => 'Ubah Pengeluaran',
self::EXPENSES_DELETE => 'Hapus Pengeluaran',
};
}
@ -112,6 +122,8 @@ public function group(): string
self::RAW_MATERIALS_VIEW, self::RAW_MATERIALS_CREATE, self::RAW_MATERIALS_UPDATE,
self::RAW_MATERIALS_DELETE, self::RAW_MATERIALS_TOGGLE_STATUS => 'Bahan Baku',
self::CASH_VIEW, self::CASH_DEPOSIT, self::CASH_UPDATE, self::CASH_DELETE => 'Kas',
self::EXPENSES_VIEW, self::EXPENSES_CREATE, self::EXPENSES_UPDATE,
self::EXPENSES_DELETE => 'Pengeluaran',
};
}

View File

@ -70,6 +70,10 @@ public function permissions(): array
Permission::CASH_DEPOSIT,
Permission::CASH_UPDATE,
Permission::CASH_DELETE,
Permission::EXPENSES_VIEW,
Permission::EXPENSES_CREATE,
Permission::EXPENSES_UPDATE,
Permission::EXPENSES_DELETE,
],
self::ADMIN_TOKO => [
Permission::DASHBOARD_VIEW,
@ -99,6 +103,10 @@ public function permissions(): array
Permission::CASH_DEPOSIT,
Permission::CASH_UPDATE,
Permission::CASH_DELETE,
Permission::EXPENSES_VIEW,
Permission::EXPENSES_CREATE,
Permission::EXPENSES_UPDATE,
Permission::EXPENSES_DELETE,
],
self::ADMIN_BAHAN_BAKU => [
Permission::DASHBOARD_VIEW,

View File

@ -0,0 +1,59 @@
<?php
namespace App\Http\Controllers\Admin\Finance;
use App\Http\Controllers\Concerns\ParsesDataTableQuery;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\Finance\ExpenseRequest;
use App\Models\Expense;
use App\Services\Finance\ExpenseService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
class ExpenseController extends Controller
{
use ParsesDataTableQuery;
public function __construct(
private readonly ExpenseService $expenseService,
) {}
public function index(Request $request): Response
{
$tableQuery = $this->parseDataTableQuery($request);
return Inertia::render('admin/finance/expenses/Index', [
'expenses' => $this->expenseService->paginateForIndex($tableQuery),
'filters' => $this->dataTableFilters($tableQuery),
]);
}
public function store(ExpenseRequest $request): RedirectResponse
{
$this->expenseService->create($request->validated(), $request->user());
Inertia::flash('success', 'Pengeluaran berhasil ditambahkan.');
return redirect()->route('admin.finance.expenses.index');
}
public function update(ExpenseRequest $request, Expense $expense): RedirectResponse
{
$this->expenseService->update($expense, $request->validated());
Inertia::flash('success', 'Pengeluaran berhasil diperbarui.');
return redirect()->route('admin.finance.expenses.index');
}
public function destroy(Expense $expense): RedirectResponse
{
$this->expenseService->delete($expense);
Inertia::flash('success', 'Pengeluaran berhasil dihapus.');
return redirect()->route('admin.finance.expenses.index');
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Http\Requests\Admin\Finance;
use App\Enums\Permission;
use Illuminate\Foundation\Http\FormRequest;
class ExpenseRequest extends FormRequest
{
public function authorize(): bool
{
$permission = $this->isMethod('POST')
? Permission::EXPENSES_CREATE
: Permission::EXPENSES_UPDATE;
return $this->user()?->can($permission->value) ?? false;
}
/**
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'amount' => ['required', 'integer', 'min:1'],
'description' => ['required', 'string', 'max:500'],
];
}
}

View File

@ -80,6 +80,7 @@ public static function labelForReferenceType(?string $referenceType): string
}
return match ($referenceType) {
Expense::class => 'Pengeluaran',
default => class_basename($referenceType),
};
}

55
app/Models/Expense.php Normal file
View File

@ -0,0 +1,55 @@
<?php
namespace App\Models;
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\SoftDeletes;
#[Guarded(['id'])]
#[Appends(['amount_formatted', 'created_at_formatted', 'created_by_name'])]
class Expense extends Model
{
use SoftDeletes;
protected function casts(): array
{
return [
'amount' => 'integer',
];
}
public function amountFormatted(): Attribute
{
return Attribute::make(
get: fn () => 'Rp '.number_format($this->amount, 0, ',', '.'),
);
}
public function createdAtFormatted(): Attribute
{
return Attribute::make(
get: fn () => $this->created_at?->translatedFormat('l, d F Y H:i'),
);
}
public function createdByName(): Attribute
{
return Attribute::make(
get: fn () => $this->createdBy?->profile?->full_name ?? $this->createdBy?->username,
);
}
public function cashTransaction(): BelongsTo
{
return $this->belongsTo(CashTransaction::class);
}
public function createdBy(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by_id');
}
}

View File

@ -70,4 +70,9 @@ public function cashTransactions(): HasMany
{
return $this->hasMany(CashTransaction::class, 'created_by_id');
}
public function expenses(): HasMany
{
return $this->hasMany(Expense::class, 'created_by_id');
}
}

View File

@ -164,6 +164,42 @@ public function deleteTransaction(CashTransaction $transaction): void
});
}
public function updateReferencedTransaction(
CashTransaction $transaction,
int $amount,
string $description,
): void {
DB::transaction(function () use ($transaction, $amount, $description): void {
CashAccount::query()->lockForUpdate()->findOrFail($transaction->cash_account_id);
$transaction->amount = $amount;
$transaction->description = $description;
$transaction->save();
$this->recalculateBalances($transaction->cashAccount);
$account = $transaction->cashAccount->fresh();
if ($account->balance < 0) {
throw ValidationException::withMessages([
'amount' => 'Saldo kas tidak mencukupi.',
]);
}
});
}
public function deleteReferencedTransaction(CashTransaction $transaction): void
{
DB::transaction(function () use ($transaction): void {
CashAccount::query()->lockForUpdate()->findOrFail($transaction->cash_account_id);
$account = $transaction->cashAccount;
$transaction->delete();
$this->recalculateBalances($account);
});
}
private function ensureEditable(CashTransaction $transaction): void
{
if ($transaction->reference_type !== null) {

View File

@ -0,0 +1,109 @@
<?php
namespace App\Services\Finance;
use App\Models\Expense;
use App\Models\User;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;
class ExpenseService
{
public function __construct(
private readonly CashService $cashService,
) {}
/**
* @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery
*/
public function paginateForIndex(array $tableQuery): LengthAwarePaginator
{
$query = Expense::query()
->with(['createdBy.profile'])
->when($tableQuery['search'] !== '', function (Builder $query) use ($tableQuery): void {
$search = $tableQuery['search'];
$query->where(function (Builder $query) use ($search): void {
$query->where('description', 'like', "%{$search}%");
});
});
$this->applySorting($query, $tableQuery['sort'], $tableQuery['direction']);
return $query
->paginate(10)
->withQueryString();
}
/**
* @param array{amount: int, description: string} $validated
*/
public function create(array $validated, User $user): void
{
DB::transaction(function () use ($validated, $user): void {
$amount = (int) $validated['amount'];
$description = $validated['description'];
$expense = Expense::create([
'amount' => $amount,
'description' => $description,
'created_by_id' => $user->id,
]);
$cashTransaction = $this->cashService->recordOutgoing(
$expense,
$amount,
$description,
$user,
);
$expense->cash_transaction_id = $cashTransaction->id;
$expense->save();
});
}
/**
* @param array{amount: int, description: string} $validated
*/
public function update(Expense $expense, array $validated): void
{
DB::transaction(function () use ($expense, $validated): void {
$amount = (int) $validated['amount'];
$description = $validated['description'];
$expense->amount = $amount;
$expense->description = $description;
$expense->save();
if ($expense->cashTransaction) {
$this->cashService->updateReferencedTransaction(
$expense->cashTransaction,
$amount,
$description,
);
}
});
}
public function delete(Expense $expense): void
{
DB::transaction(function () use ($expense): void {
if ($expense->cashTransaction) {
$this->cashService->deleteReferencedTransaction($expense->cashTransaction);
}
$expense->delete();
});
}
private function applySorting(Builder $query, string $sort, string $direction): void
{
if (in_array($sort, ['created_at', 'amount', 'description'], true)) {
$query->orderBy($sort, $direction);
return;
}
$query->latest();
}
}

View File

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('expenses', function (Blueprint $table) {
$table->id();
$table->foreignId('cash_transaction_id')->nullable()->unique()->constrained()->restrictOnDelete();
$table->foreignId('created_by_id')->constrained('users')->restrictOnDelete();
$table->unsignedBigInteger('amount');
$table->string('description', 200);
$table->timestamp('created_at')->useCurrent();
$table->timestamp('updated_at')->useCurrent()->useCurrentOnUpdate();
$table->softDeletes();
});
}
public function down(): void
{
Schema::dropIfExists('expenses');
}
};

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import { Link, usePage } from '@inertiajs/vue3';
import { FolderTree, Layers, LayoutDashboard, Package, User, UserCheck, Users, Wallet } from '@lucide/vue';
import { FolderTree, Layers, LayoutDashboard, Package, Receipt, User, UserCheck, Users, Wallet } from '@lucide/vue';
import { computed } from 'vue';
import {
Sidebar,
@ -27,6 +27,8 @@ const isRawMaterialsActive = computed(() => page.url.startsWith('/admin/master/r
const isSuppliersActive = computed(() => page.url.startsWith('/admin/master/suppliers'));
const isCustomersActive = computed(() => page.url.startsWith('/admin/master/customers'));
const isCashActive = computed(() => page.url.startsWith('/admin/finance/cash'));
const isExpensesActive = computed(() => page.url.startsWith('/admin/finance/expenses'));
const showFinanceMenu = computed(() => can('cash.view') || can('expenses.view'));
const showMasterMenu = computed(() => (
can('categories.view') || can('products.view') || can('raw-materials.view')
|| can('suppliers.view') || can('customers.view')
@ -111,7 +113,7 @@ const showMasterMenu = computed(() => (
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
<SidebarGroup>
<SidebarGroup v-if="showFinanceMenu">
<SidebarGroupLabel>Keuangan</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
@ -123,6 +125,14 @@ const showMasterMenu = computed(() => (
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem v-if="can('expenses.view')">
<SidebarMenuButton as-child tooltip="Pengeluaran" :is-active="isExpensesActive">
<Link href="/admin/finance/expenses">
<Receipt />
<span>Pengeluaran</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>

View File

@ -0,0 +1,130 @@
<script setup lang="ts">
import { useForm } from '@inertiajs/vue3';
import { Save } from '@lucide/vue';
import { computed, watch } from 'vue';
import { toast } from 'vue-sonner';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
Field,
FieldError,
FieldGroup,
FieldLabel,
FieldSet,
} from '@/components/ui/field';
import { RupiahInput } from '@/components/ui/rupiah-input';
import { Textarea } from '@/components/ui/textarea';
import { parseRupiah } from '@/lib/rupiah';
import type { ExpenseFormData, ExpenseListItem } from '@/types/expense';
const open = defineModel<boolean>('open', { default: false });
const props = defineProps<{
expense?: ExpenseListItem | null;
}>();
const isEditing = computed(() => props.expense != null);
const form = useForm<ExpenseFormData>({
amount: '',
description: '',
});
function resetForm() {
form.reset();
form.clearErrors();
}
function populateForm(expense: ExpenseListItem | null | undefined) {
resetForm();
if (!expense) {
return;
}
form.amount = String(expense.amount);
form.description = expense.description;
}
watch(
() => props.expense,
(expense) => {
populateForm(expense);
},
);
watch(open, (isOpen) => {
if (isOpen) {
populateForm(props.expense);
} else {
resetForm();
}
});
function submit() {
const options = {
preserveScroll: true,
onSuccess: () => {
open.value = false;
},
onError: () => {
toast.error('Gagal menyimpan data. Periksa kembali formulir.');
},
};
form.transform((data) => ({
...data,
amount: parseRupiah(data.amount),
}));
if (isEditing.value && props.expense) {
form.put(`/admin/finance/expenses/${props.expense.id}`, options);
} else {
form.post('/admin/finance/expenses', options);
}
}
</script>
<template>
<Dialog v-model:open="open">
<DialogContent class="sm:max-w-md">
<DialogHeader>
<DialogTitle>{{ isEditing ? 'Ubah Pengeluaran' : 'Tambah Pengeluaran' }}</DialogTitle>
</DialogHeader>
<form @submit.prevent="submit">
<FieldGroup>
<FieldSet class="grid gap-4">
<Field>
<FieldLabel for="expense-amount" required>Jumlah</FieldLabel>
<RupiahInput id="expense-amount" v-model="form.amount" autofocus />
<FieldError :errors="form.errors.amount ? [form.errors.amount] : []" />
</Field>
<Field>
<FieldLabel for="expense-description" required>Keterangan</FieldLabel>
<Textarea id="expense-description" v-model="form.description"
placeholder="Contoh: Pembelian perlengkapan toko" rows="3" />
<FieldError :errors="form.errors.description ? [form.errors.description] : []" />
</Field>
</FieldSet>
</FieldGroup>
<DialogFooter class="mt-6">
<Button type="button" variant="outline" :disabled="form.processing" @click="open = false">
Batal
</Button>
<Button type="submit" :disabled="form.processing">
<Save class="size-4" />
{{ form.processing ? 'Menyimpan...' : 'Simpan' }}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</template>

View File

@ -0,0 +1,39 @@
import type { ColumnDef } from '@tanstack/vue-table';
import { h } from 'vue';
import DataTableActions from '@/components/admin/finance/expenses/data-table-actions.vue';
import { DataTableColumnHeader } from '@/components/data-table';
import type { ExpenseListItem } from '@/types/expense';
export function createColumns(onEdit: (expense: ExpenseListItem) => void): ColumnDef<ExpenseListItem>[] {
return [
{
accessorKey: 'created_at_formatted',
enableSorting: true,
header: () => h(DataTableColumnHeader, { title: 'Tanggal', column: 'created_at' }),
},
{
accessorKey: 'amount_formatted',
enableSorting: true,
header: () => h(DataTableColumnHeader, { title: 'Jumlah', column: 'amount' }),
},
{
accessorKey: 'description',
enableSorting: true,
header: () => h(DataTableColumnHeader, { title: 'Keterangan', column: 'description' }),
},
{
accessorKey: 'created_by_name',
enableSorting: false,
header: () => 'Oleh',
},
{
id: 'actions',
enableSorting: false,
enableHiding: false,
cell: ({ row }) => h(DataTableActions, {
expense: row.original,
onEdit: () => onEdit(row.original),
}),
},
];
}

View File

@ -0,0 +1,70 @@
<script setup lang="ts">
import { router } from '@inertiajs/vue3';
import { Pencil, Trash2 } from '@lucide/vue';
import { ref } from 'vue';
import { toast } from 'vue-sonner';
import ConfirmDialog from '@/components/ConfirmDialog.vue';
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { useCan } from '@/composables/useCan';
import type { ExpenseListItem } from '@/types/expense';
const props = defineProps<{
expense: ExpenseListItem;
}>();
const emit = defineEmits<{
edit: [expense: ExpenseListItem];
}>();
const { can } = useCan();
const deleteConfirmOpen = ref(false);
const deleteProcessing = ref(false);
function destroyExpense() {
deleteProcessing.value = true;
router.delete(`/admin/finance/expenses/${props.expense.id}`, {
preserveScroll: true,
onSuccess: () => {
deleteConfirmOpen.value = false;
},
onError: () => {
toast.error('Gagal menghapus pengeluaran.');
},
onFinish: () => {
deleteProcessing.value = false;
},
});
}
</script>
<template>
<div class="flex items-center justify-end gap-1">
<Tooltip v-if="can('expenses.update')">
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="size-8" @click="emit('edit', expense)">
<Pencil class="size-4" />
<span class="sr-only">Ubah</span>
</Button>
</TooltipTrigger>
<TooltipContent>Ubah</TooltipContent>
</Tooltip>
<Tooltip v-if="can('expenses.delete')">
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="text-destructive hover:text-destructive size-8"
@click="deleteConfirmOpen = true">
<Trash2 class="size-4" />
<span class="sr-only">Hapus</span>
</Button>
</TooltipTrigger>
<TooltipContent>Hapus</TooltipContent>
</Tooltip>
</div>
<ConfirmDialog v-if="can('expenses.delete')" v-model:open="deleteConfirmOpen" title="Hapus pengeluaran?"
:description="`Pengeluaran ${expense.amount_formatted} akan dihapus. Saldo kas akan disesuaikan.`"
confirm-label="Hapus" cancel-label="Batal" destructive :loading="deleteProcessing" @confirm="destroyExpense" />
</template>

View File

@ -0,0 +1,108 @@
<script setup lang="ts">
import { Head } from '@inertiajs/vue3';
import { Plus } from '@lucide/vue';
import { computed, ref, watch } from 'vue';
import ExpenseFormModal from '@/components/admin/finance/expenses/ExpenseFormModal.vue';
import { createColumns } from '@/components/admin/finance/expenses/columns';
import { DataTable } from '@/components/data-table';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { useCan } from '@/composables/useCan';
import { useDataTableQuery, useDataTableQuerySync } from '@/composables/useDataTableQuery';
import AdminLayout from '@/layouts/AdminLayout.vue';
import type { ExpenseListItem, PaginatedExpenses } from '@/types/expense';
import type { DataTableSort } from '@/types/data-table';
const props = defineProps<{
expenses: PaginatedExpenses;
filters: {
search: string;
sort?: string;
direction?: 'asc' | 'desc';
};
}>();
const { can } = useCan();
const search = ref(props.filters.search ?? '');
const formModalOpen = ref(false);
const editingExpense = ref<ExpenseListItem | null>(null);
const { query, setSearch, setSort, resetFilters, syncFromServer } = useDataTableQuery({
url: '/admin/finance/expenses',
initial: { ...props.filters },
});
useDataTableQuerySync(() => props.filters, syncFromServer);
const columns = computed(() => createColumns(openEditModal));
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.expenses.current_page,
perPage: props.expenses.per_page,
lastPage: props.expenses.last_page,
total: props.expenses.total,
}));
function openCreateModal() {
editingExpense.value = null;
formModalOpen.value = true;
}
function openEditModal(expense: ExpenseListItem) {
editingExpense.value = expense;
formModalOpen.value = true;
}
watch(search, (value) => {
setSearch(value);
});
watch(
() => props.filters.search,
(value) => {
search.value = value ?? '';
},
);
</script>
<template>
<Head title="Pengeluaran" />
<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">
Pengeluaran
</h2>
</div>
<Button v-if="can('expenses.create')" class="shrink-0 self-start sm:self-center" @click="openCreateModal">
<Plus class="size-4" />
Tambah
</Button>
</div>
<Card class="min-w-0">
<CardContent class="min-w-0">
<DataTable v-model:search="search" :columns="columns" :data="expenses.data" :pagination="pagination"
:pagination-links="expenses.links" :sort="currentSort" @sort-change="setSort"
@filters-reset="resetFilters" />
</CardContent>
</Card>
<ExpenseFormModal v-if="can('expenses.create') || can('expenses.update')" v-model:open="formModalOpen"
:expense="editingExpense" />
</AdminLayout>
</template>

View File

@ -0,0 +1,26 @@
export type ExpenseListItem = {
id: number;
amount: number;
amount_formatted: string;
description: string;
created_at_formatted: string;
created_by_name: string;
};
export type ExpenseFormData = {
amount: string;
description: string;
};
export type PaginatedExpenses = {
data: ExpenseListItem[];
current_page: number;
last_page: number;
per_page: number;
total: number;
links: Array<{
url: string | null;
label: string;
active: boolean;
}>;
};

View File

@ -3,6 +3,7 @@
use App\Enums\Permission;
use App\Http\Controllers\Admin\DashboardController;
use App\Http\Controllers\Admin\Finance\CashController;
use App\Http\Controllers\Admin\Finance\ExpenseController;
use App\Http\Controllers\Admin\Hr\EmployeeController;
use App\Http\Controllers\Admin\Master\CategoryController;
use App\Http\Controllers\Admin\Master\CustomerController;
@ -176,6 +177,24 @@
->middleware('permission:'.Permission::CASH_DELETE->value)
->name('transactions.destroy');
});
Route::prefix('expenses')->name('expenses.')
->middleware('permission:'.Permission::EXPENSES_VIEW->value)
->group(function () {
Route::get('/', [ExpenseController::class, 'index'])->name('index');
Route::post('/', [ExpenseController::class, 'store'])
->middleware('permission:'.Permission::EXPENSES_CREATE->value)
->name('store');
Route::put('{expense}', [ExpenseController::class, 'update'])
->middleware('permission:'.Permission::EXPENSES_UPDATE->value)
->name('update');
Route::delete('{expense}', [ExpenseController::class, 'destroy'])
->middleware('permission:'.Permission::EXPENSES_DELETE->value)
->name('destroy');
});
});
Route::prefix('hr')->name('hr.')->middleware('permission:'.Permission::EMPLOYEES_VIEW->value)->group(function () {