Add category management routes, permissions, and UI components. Integrate new permissions for categories in the Permission and Role enums, and update the sidebar to include category navigation. Enhance the composer configuration by adding the Spatie Laravel Sluggable package for slug generation.
This commit is contained in:
parent
370398412b
commit
432bbd110a
@ -17,6 +17,11 @@ enum Permission: string
|
||||
case EMPLOYEES_RESET_PASSWORD = 'employees.reset-password';
|
||||
case EMPLOYEES_TOGGLE_STATUS = 'employees.toggle-status';
|
||||
|
||||
case CATEGORIES_VIEW = 'categories.view';
|
||||
case CATEGORIES_CREATE = 'categories.create';
|
||||
case CATEGORIES_UPDATE = 'categories.update';
|
||||
case CATEGORIES_DELETE = 'categories.delete';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
@ -28,6 +33,11 @@ public function label(): string
|
||||
self::EMPLOYEES_DELETE => 'Hapus Pegawai',
|
||||
self::EMPLOYEES_RESET_PASSWORD => 'Reset Kata Sandi Pegawai',
|
||||
self::EMPLOYEES_TOGGLE_STATUS => 'Ubah Status Pegawai',
|
||||
|
||||
self::CATEGORIES_VIEW => 'Lihat Kategori',
|
||||
self::CATEGORIES_CREATE => 'Tambah Kategori',
|
||||
self::CATEGORIES_UPDATE => 'Ubah Kategori',
|
||||
self::CATEGORIES_DELETE => 'Hapus Kategori',
|
||||
};
|
||||
}
|
||||
|
||||
@ -37,6 +47,8 @@ public function group(): string
|
||||
self::DASHBOARD_VIEW => 'Umum',
|
||||
self::EMPLOYEES_VIEW, self::EMPLOYEES_CREATE, self::EMPLOYEES_UPDATE,
|
||||
self::EMPLOYEES_DELETE, self::EMPLOYEES_RESET_PASSWORD, self::EMPLOYEES_TOGGLE_STATUS => 'Pegawai',
|
||||
self::CATEGORIES_VIEW, self::CATEGORIES_CREATE, self::CATEGORIES_UPDATE,
|
||||
self::CATEGORIES_DELETE => 'Kategori',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -44,6 +44,10 @@ public function permissions(): array
|
||||
Permission::EMPLOYEES_DELETE,
|
||||
Permission::EMPLOYEES_RESET_PASSWORD,
|
||||
Permission::EMPLOYEES_TOGGLE_STATUS,
|
||||
Permission::CATEGORIES_VIEW,
|
||||
Permission::CATEGORIES_CREATE,
|
||||
Permission::CATEGORIES_UPDATE,
|
||||
Permission::CATEGORIES_DELETE,
|
||||
],
|
||||
self::ADMIN_TOKO => [
|
||||
Permission::DASHBOARD_VIEW,
|
||||
@ -52,14 +56,23 @@ public function permissions(): array
|
||||
Permission::EMPLOYEES_UPDATE,
|
||||
Permission::EMPLOYEES_RESET_PASSWORD,
|
||||
Permission::EMPLOYEES_TOGGLE_STATUS,
|
||||
Permission::CATEGORIES_VIEW,
|
||||
Permission::CATEGORIES_CREATE,
|
||||
Permission::CATEGORIES_UPDATE,
|
||||
Permission::CATEGORIES_DELETE,
|
||||
],
|
||||
self::ADMIN_BAHAN_BAKU => [
|
||||
Permission::DASHBOARD_VIEW,
|
||||
Permission::EMPLOYEES_VIEW,
|
||||
Permission::CATEGORIES_VIEW,
|
||||
Permission::CATEGORIES_CREATE,
|
||||
Permission::CATEGORIES_UPDATE,
|
||||
Permission::CATEGORIES_DELETE,
|
||||
],
|
||||
self::MARKETING => [
|
||||
Permission::DASHBOARD_VIEW,
|
||||
Permission::EMPLOYEES_VIEW,
|
||||
Permission::CATEGORIES_VIEW,
|
||||
],
|
||||
self::NON_OPERATOR => [
|
||||
Permission::DASHBOARD_VIEW,
|
||||
|
||||
97
app/Http/Controllers/Admin/CategoryController.php
Normal file
97
app/Http/Controllers/Admin/CategoryController.php
Normal file
@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Concerns\ParsesDataTableQuery;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\CategoryRequest;
|
||||
use App\Models\Category;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class CategoryController extends Controller
|
||||
{
|
||||
use ParsesDataTableQuery;
|
||||
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$tableQuery = $this->parseDataTableQuery($request);
|
||||
$search = $tableQuery['search'];
|
||||
$sort = $tableQuery['sort'];
|
||||
$direction = $tableQuery['direction'];
|
||||
|
||||
$query = Category::query()
|
||||
->when($search !== '', function (Builder $query) use ($search): void {
|
||||
$query->where(function (Builder $query) use ($search): void {
|
||||
$query->where('name', 'like', "%{$search}%");
|
||||
});
|
||||
});
|
||||
|
||||
$this->applySorting($query, $sort, $direction);
|
||||
|
||||
$categories = $query
|
||||
->paginate(10)
|
||||
->withQueryString()
|
||||
->through(fn (Category $category) => $this->transformCategory($category));
|
||||
|
||||
return Inertia::render('admin/categories/Index', [
|
||||
'categories' => $categories,
|
||||
'filters' => $this->dataTableFilters($tableQuery),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(CategoryRequest $request): RedirectResponse
|
||||
{
|
||||
Category::create($request->validated());
|
||||
|
||||
Inertia::flash('success', 'Kategori berhasil ditambahkan.');
|
||||
|
||||
return redirect()->route('admin.master.categories.index');
|
||||
}
|
||||
|
||||
public function update(CategoryRequest $request, Category $category): RedirectResponse
|
||||
{
|
||||
$validated = $request->validated();
|
||||
$category->name = $validated['name'];
|
||||
$category->save();
|
||||
|
||||
Inertia::flash('success', 'Kategori berhasil diperbarui.');
|
||||
|
||||
return redirect()->route('admin.master.categories.index');
|
||||
}
|
||||
|
||||
public function destroy(Category $category): RedirectResponse
|
||||
{
|
||||
$category->delete();
|
||||
|
||||
Inertia::flash('success', 'Kategori berhasil dihapus.');
|
||||
|
||||
return redirect()->route('admin.master.categories.index');
|
||||
}
|
||||
|
||||
private function applySorting(Builder $query, string $sort, string $direction): void
|
||||
{
|
||||
if (in_array($sort, ['name', 'slug'], true)) {
|
||||
$query->orderBy($sort, $direction);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$query->latest();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function transformCategory(Category $category): array
|
||||
{
|
||||
return [
|
||||
'id' => $category->id,
|
||||
'name' => $category->name,
|
||||
'slug' => $category->slug,
|
||||
];
|
||||
}
|
||||
}
|
||||
28
app/Http/Requests/Admin/CategoryRequest.php
Normal file
28
app/Http/Requests/Admin/CategoryRequest.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin;
|
||||
|
||||
use App\Enums\Permission;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class CategoryRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
$permission = $this->isMethod('POST')
|
||||
? Permission::CATEGORIES_CREATE
|
||||
: Permission::CATEGORIES_UPDATE;
|
||||
|
||||
return $this->user()?->can($permission->value) ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:50'],
|
||||
];
|
||||
}
|
||||
}
|
||||
15
app/Models/Category.php
Normal file
15
app/Models/Category.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Spatie\Sluggable\Attributes\Sluggable;
|
||||
|
||||
#[Guarded(['id'])]
|
||||
#[Sluggable(from: 'name', to: 'slug')]
|
||||
class Category extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
}
|
||||
@ -14,7 +14,8 @@
|
||||
"laravel/framework": "^13.7",
|
||||
"laravel/tinker": "^3.0",
|
||||
"laravel/wayfinder": "^0.1.14",
|
||||
"spatie/laravel-permission": "^8.0"
|
||||
"spatie/laravel-permission": "^8.0",
|
||||
"spatie/laravel-sluggable": "^4.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.24",
|
||||
|
||||
80
composer.lock
generated
80
composer.lock
generated
@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "214f5e51f5d046a46a01d066adf96929",
|
||||
"content-hash": "dd4192a3bb321ccbee15386b55196fc5",
|
||||
"packages": [
|
||||
{
|
||||
"name": "brick/math",
|
||||
@ -3636,6 +3636,84 @@
|
||||
],
|
||||
"time": "2026-05-30T19:30:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "spatie/laravel-sluggable",
|
||||
"version": "4.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/spatie/laravel-sluggable.git",
|
||||
"reference": "82a69be1ef661ce2ff38242b271457ef0b9611dd"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/spatie/laravel-sluggable/zipball/82a69be1ef661ce2ff38242b271457ef0b9611dd",
|
||||
"reference": "82a69be1ef661ce2ff38242b271457ef0b9611dd",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"illuminate/database": "^12.0|^13.0",
|
||||
"illuminate/support": "^12.0|^13.0",
|
||||
"php": "^8.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"larastan/larastan": "^3.0",
|
||||
"laravel/pint": "^1.24",
|
||||
"orchestra/testbench": "^10.0|^11.0",
|
||||
"pestphp/pest": "^4.0",
|
||||
"spatie/laravel-translatable": "^6.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"aliases": {
|
||||
"SelfHealing": "Spatie\\Sluggable\\Facades\\SelfHealing"
|
||||
},
|
||||
"providers": [
|
||||
"Spatie\\Sluggable\\SluggableServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Spatie\\Sluggable\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Freek Van der Herten",
|
||||
"email": "freek@spatie.be",
|
||||
"homepage": "https://spatie.be",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Generate slugs when saving Eloquent models",
|
||||
"homepage": "https://github.com/spatie/laravel-sluggable",
|
||||
"keywords": [
|
||||
"eloquent",
|
||||
"laravel",
|
||||
"laravel-sluggable",
|
||||
"self-healing",
|
||||
"slug",
|
||||
"slugs",
|
||||
"spatie",
|
||||
"translatable"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/spatie/laravel-sluggable/issues",
|
||||
"source": "https://github.com/spatie/laravel-sluggable/tree/4.0.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/spatie",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-04-30T17:28:09+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/clock",
|
||||
"version": "v8.1.0",
|
||||
|
||||
@ -0,0 +1,24 @@
|
||||
<?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('categories', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name', 50);
|
||||
$table->string('slug', 50)->unique();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('categories');
|
||||
}
|
||||
};
|
||||
@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { Link, usePage } from '@inertiajs/vue3';
|
||||
import { LayoutDashboard, Users } from '@lucide/vue';
|
||||
import { FolderTree, LayoutDashboard, Users } from '@lucide/vue';
|
||||
import { computed } from 'vue';
|
||||
import {
|
||||
Sidebar,
|
||||
@ -21,6 +21,7 @@ const { can } = useCan();
|
||||
|
||||
const isDashboardActive = computed(() => page.url.startsWith('/admin/dashboard'));
|
||||
const isEmployeesActive = computed(() => page.url.startsWith('/admin/hr/employees'));
|
||||
const isCategoriesActive = computed(() => page.url.startsWith('/admin/master/categories'));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -54,6 +55,21 @@ const isEmployeesActive = computed(() => page.url.startsWith('/admin/hr/employee
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
<SidebarGroup v-if="can('categories.view')">
|
||||
<SidebarGroupLabel>Master</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton as-child tooltip="Kategori" :is-active="isCategoriesActive">
|
||||
<Link href="/admin/master/categories">
|
||||
<FolderTree />
|
||||
<span>Kategori</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
<SidebarGroup v-if="can('employees.view')">
|
||||
<SidebarGroupLabel>HR</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
|
||||
116
resources/js/components/categories/CategoryFormModal.vue
Normal file
116
resources/js/components/categories/CategoryFormModal.vue
Normal file
@ -0,0 +1,116 @@
|
||||
<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 { Input } from '@/components/ui/input';
|
||||
import type { CategoryFormData, CategoryListItem } from '@/types/category';
|
||||
|
||||
const open = defineModel<boolean>('open', { default: false });
|
||||
|
||||
const props = defineProps<{
|
||||
category?: CategoryListItem | null;
|
||||
}>();
|
||||
|
||||
const isEditing = computed(() => props.category != null);
|
||||
|
||||
const form = useForm<CategoryFormData>({
|
||||
name: '',
|
||||
});
|
||||
|
||||
function resetForm() {
|
||||
form.reset();
|
||||
form.clearErrors();
|
||||
}
|
||||
|
||||
function populateForm(category: CategoryListItem | null | undefined) {
|
||||
resetForm();
|
||||
|
||||
if (!category) {
|
||||
return;
|
||||
}
|
||||
|
||||
form.name = category.name;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.category,
|
||||
(category) => {
|
||||
populateForm(category);
|
||||
},
|
||||
);
|
||||
|
||||
watch(open, (isOpen) => {
|
||||
if (isOpen) {
|
||||
populateForm(props.category);
|
||||
} else {
|
||||
resetForm();
|
||||
}
|
||||
});
|
||||
|
||||
function submit() {
|
||||
const options = {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
open.value = false;
|
||||
},
|
||||
onError: () => {
|
||||
toast.error('Gagal menyimpan data. Periksa kembali formulir.');
|
||||
},
|
||||
};
|
||||
|
||||
if (isEditing.value && props.category) {
|
||||
form.put(`/admin/master/categories/${props.category.id}`, options);
|
||||
} else {
|
||||
form.post('/admin/master/categories', options);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog v-model:open="open">
|
||||
<DialogContent class="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ isEditing ? 'Ubah Kategori' : 'Tambah Kategori' }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<form @submit.prevent="submit">
|
||||
<FieldGroup>
|
||||
<FieldSet class="grid gap-4">
|
||||
<Field>
|
||||
<FieldLabel for="category-name" required>Nama</FieldLabel>
|
||||
<Input id="category-name" v-model="form.name" type="text" placeholder="Nama kategori"
|
||||
autofocus />
|
||||
<FieldError :errors="form.errors.name ? [form.errors.name] : []" />
|
||||
</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>
|
||||
24
resources/js/components/categories/columns.ts
Normal file
24
resources/js/components/categories/columns.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import type { ColumnDef } from '@tanstack/vue-table';
|
||||
import { h } from 'vue';
|
||||
import DataTableActions from '@/components/categories/data-table-actions.vue';
|
||||
import { DataTableColumnHeader } from '@/components/data-table';
|
||||
import type { CategoryListItem } from '@/types/category';
|
||||
|
||||
export function createColumns(onEdit: (category: CategoryListItem) => void): ColumnDef<CategoryListItem>[] {
|
||||
return [
|
||||
{
|
||||
accessorKey: 'name',
|
||||
enableSorting: true,
|
||||
header: () => h(DataTableColumnHeader, { title: 'Nama', column: 'name' }),
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
cell: ({ row }) => h(DataTableActions, {
|
||||
category: row.original,
|
||||
onEdit: () => onEdit(row.original),
|
||||
}),
|
||||
},
|
||||
];
|
||||
}
|
||||
108
resources/js/pages/admin/categories/Index.vue
Normal file
108
resources/js/pages/admin/categories/Index.vue
Normal 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 CategoryFormModal from '@/components/categories/CategoryFormModal.vue';
|
||||
import { createColumns } from '@/components/categories/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 { CategoryListItem, PaginatedCategories } from '@/types/category';
|
||||
import type { DataTableSort } from '@/types/data-table';
|
||||
|
||||
const props = defineProps<{
|
||||
categories: PaginatedCategories;
|
||||
filters: {
|
||||
search: string;
|
||||
sort?: string;
|
||||
direction?: 'asc' | 'desc';
|
||||
};
|
||||
}>();
|
||||
|
||||
const { can } = useCan();
|
||||
const search = ref(props.filters.search ?? '');
|
||||
const formModalOpen = ref(false);
|
||||
const editingCategory = ref<CategoryListItem | null>(null);
|
||||
|
||||
const { query, setSearch, setSort, resetFilters, syncFromServer } = useDataTableQuery({
|
||||
url: '/admin/mastercategories',
|
||||
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.categories.current_page,
|
||||
perPage: props.categories.per_page,
|
||||
lastPage: props.categories.last_page,
|
||||
total: props.categories.total,
|
||||
}));
|
||||
|
||||
function openCreateModal() {
|
||||
editingCategory.value = null;
|
||||
formModalOpen.value = true;
|
||||
}
|
||||
|
||||
function openEditModal(category: CategoryListItem) {
|
||||
editingCategory.value = category;
|
||||
formModalOpen.value = true;
|
||||
}
|
||||
|
||||
watch(search, (value) => {
|
||||
setSearch(value);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.filters.search,
|
||||
(value) => {
|
||||
search.value = value ?? '';
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<Head title="Kategori" />
|
||||
|
||||
<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">
|
||||
Kategori
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<Button v-if="can('categories.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="categories.data" :pagination="pagination"
|
||||
:pagination-links="categories.links" :sort="currentSort" @sort-change="setSort"
|
||||
@filters-reset="resetFilters" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<CategoryFormModal v-if="can('categories.create') || can('categories.update')" v-model:open="formModalOpen"
|
||||
:category="editingCategory" />
|
||||
</AdminLayout>
|
||||
</template>
|
||||
29
resources/js/types/category.ts
Normal file
29
resources/js/types/category.ts
Normal file
@ -0,0 +1,29 @@
|
||||
export type CategoryListItem = {
|
||||
id: number;
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
|
||||
export type CategoryFormData = {
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
|
||||
export type CategoryFilters = {
|
||||
search: string;
|
||||
sort?: string;
|
||||
direction?: 'asc' | 'desc' | null;
|
||||
};
|
||||
|
||||
export type PaginatedCategories = {
|
||||
data: CategoryListItem[];
|
||||
current_page: number;
|
||||
last_page: number;
|
||||
per_page: number;
|
||||
total: number;
|
||||
links: Array<{
|
||||
url: string | null;
|
||||
label: string;
|
||||
active: boolean;
|
||||
}>;
|
||||
};
|
||||
@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\Permission;
|
||||
use App\Http\Controllers\Admin\CategoryController;
|
||||
use App\Http\Controllers\Admin\DashboardController;
|
||||
use App\Http\Controllers\Admin\Hr\EmployeeController;
|
||||
use App\Http\Controllers\Auth\LoginController;
|
||||
@ -21,29 +22,62 @@
|
||||
Route::get('/dashboard', [DashboardController::class, 'index'])
|
||||
->name('dashboard');
|
||||
|
||||
Route::prefix('master')->name('master.')
|
||||
->middleware('permission:'.Permission::CATEGORIES_VIEW->value)
|
||||
->group(function () {
|
||||
Route::prefix('categories')->name('categories.')
|
||||
->middleware('permission:'.Permission::CATEGORIES_VIEW->value)
|
||||
->group(function () {
|
||||
Route::get('/', [CategoryController::class, 'index'])->name('index');
|
||||
|
||||
Route::post('/', [CategoryController::class, 'store'])
|
||||
->middleware('permission:'.Permission::CATEGORIES_CREATE->value)
|
||||
->name('store');
|
||||
|
||||
Route::put('{category}', [CategoryController::class, 'update'])
|
||||
->middleware('permission:'.Permission::CATEGORIES_UPDATE->value)
|
||||
->name('update');
|
||||
|
||||
Route::delete('{category}', [CategoryController::class, 'destroy'])
|
||||
->middleware('permission:'.Permission::CATEGORIES_DELETE->value)
|
||||
->name('destroy');
|
||||
});
|
||||
});
|
||||
|
||||
Route::prefix('hr')->name('hr.')->middleware('permission:'.Permission::EMPLOYEES_VIEW->value)->group(function () {
|
||||
Route::post('employees/{user}/reset-password', [EmployeeController::class, 'resetPassword'])
|
||||
->middleware('permission:'.Permission::EMPLOYEES_RESET_PASSWORD->value)
|
||||
->name('employees.reset-password');
|
||||
Route::patch('employees/{user}/toggle-status', [EmployeeController::class, 'toggleStatus'])
|
||||
->middleware('permission:'.Permission::EMPLOYEES_TOGGLE_STATUS->value)
|
||||
->name('employees.toggle-status');
|
||||
Route::get('employees/create', [EmployeeController::class, 'create'])
|
||||
->middleware('permission:'.Permission::EMPLOYEES_CREATE->value)
|
||||
->name('employees.create');
|
||||
Route::post('employees', [EmployeeController::class, 'store'])
|
||||
->middleware('permission:'.Permission::EMPLOYEES_CREATE->value)
|
||||
->name('employees.store');
|
||||
Route::get('employees/{user}/edit', [EmployeeController::class, 'edit'])
|
||||
->middleware('permission:'.Permission::EMPLOYEES_UPDATE->value)
|
||||
->name('employees.edit');
|
||||
Route::put('employees/{user}', [EmployeeController::class, 'update'])
|
||||
->middleware('permission:'.Permission::EMPLOYEES_UPDATE->value)
|
||||
->name('employees.update');
|
||||
Route::delete('employees/{user}', [EmployeeController::class, 'destroy'])
|
||||
->middleware('permission:'.Permission::EMPLOYEES_DELETE->value)
|
||||
->name('employees.destroy');
|
||||
Route::get('employees', [EmployeeController::class, 'index'])->name('employees.index');
|
||||
Route::prefix('employees')->name('employees.')
|
||||
->middleware('permission:'.Permission::EMPLOYEES_VIEW->value)
|
||||
->group(function () {
|
||||
Route::post('{user}/reset-password', [EmployeeController::class, 'resetPassword'])
|
||||
->middleware('permission:'.Permission::EMPLOYEES_RESET_PASSWORD->value)
|
||||
->name('employees.reset-password');
|
||||
|
||||
Route::patch('{user}/toggle-status', [EmployeeController::class, 'toggleStatus'])
|
||||
->middleware('permission:'.Permission::EMPLOYEES_TOGGLE_STATUS->value)
|
||||
->name('employees.toggle-status');
|
||||
|
||||
Route::get('create', [EmployeeController::class, 'create'])
|
||||
->middleware('permission:'.Permission::EMPLOYEES_CREATE->value)
|
||||
->name('employees.create');
|
||||
|
||||
Route::post('employees', [EmployeeController::class, 'store'])
|
||||
->middleware('permission:'.Permission::EMPLOYEES_CREATE->value)
|
||||
->name('employees.store');
|
||||
|
||||
Route::get('{user}/edit', [EmployeeController::class, 'edit'])
|
||||
->middleware('permission:'.Permission::EMPLOYEES_UPDATE->value)
|
||||
->name('employees.edit');
|
||||
|
||||
Route::put('{user}', [EmployeeController::class, 'update'])
|
||||
->middleware('permission:'.Permission::EMPLOYEES_UPDATE->value)
|
||||
->name('employees.update');
|
||||
|
||||
Route::delete('{user}', [EmployeeController::class, 'destroy'])
|
||||
->middleware('permission:'.Permission::EMPLOYEES_DELETE->value)
|
||||
->name('employees.destroy');
|
||||
|
||||
Route::get('/', [EmployeeController::class, 'index'])->name('employees.index');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user