tr]:last:border-b-0",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
+ return (
+
+ )
+}
+
+function TableHead({ className, ...props }: React.ComponentProps<"th">) {
+ return (
+ [role=checkbox]]:translate-y-[2px]",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function TableCell({ className, ...props }: React.ComponentProps<"td">) {
+ return (
+ | [role=checkbox]]:translate-y-[2px]",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function TableCaption({
+ className,
+ ...props
+}: React.ComponentProps<"caption">) {
+ return (
+
+ )
+}
+
+export {
+ Table,
+ TableHeader,
+ TableBody,
+ TableFooter,
+ TableHead,
+ TableRow,
+ TableCell,
+ TableCaption,
+}
diff --git a/resources/js/pages/admin/master/category/columns.tsx b/resources/js/pages/admin/master/category/columns.tsx
new file mode 100644
index 0000000..11b0229
--- /dev/null
+++ b/resources/js/pages/admin/master/category/columns.tsx
@@ -0,0 +1,114 @@
+import type { ColumnDef } from '@tanstack/react-table';
+import { ArrowUpDown, Pencil, Trash2 } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from '@/components/ui/tooltip';
+
+export type Category = {
+ id: number;
+ name: string;
+};
+
+type CreateColumnsParams = {
+ handleEdit: (category: Category) => void;
+ handleDeleteClick: (category: Category) => void;
+};
+
+export function createCategoryColumns(
+ params: CreateColumnsParams,
+): ColumnDef[] {
+ const { handleEdit, handleDeleteClick } = params;
+
+ return [
+ {
+ id: 'no',
+ header: () => No,
+ cell: ({ row }) => (
+
+ {row.index + 1}
+
+ ),
+ meta: {
+ className: 'w-[50px] text-center',
+ headerClassName: 'w-[50px] text-center',
+ },
+ },
+ {
+ accessorKey: 'name',
+ header: ({ column }) => (
+
+ ),
+ cell: ({ row }) => (
+
+ {row.getValue('name') as string}
+
+ ),
+ },
+ {
+ id: 'actions',
+ header: () => Aksi,
+ meta: {
+ className: 'w-[100px] text-center',
+ headerClassName: 'w-[100px] text-center',
+ },
+ cell: ({ row }) => {
+ const category = row.original;
+
+ return (
+
+
+
+
+
+
+
+ Edit
+
+
+
+
+
+
+
+
+ Hapus
+
+
+
+
+ );
+ },
+ },
+ ];
+}
diff --git a/resources/js/pages/admin/master/category/index.tsx b/resources/js/pages/admin/master/category/index.tsx
new file mode 100644
index 0000000..925331a
--- /dev/null
+++ b/resources/js/pages/admin/master/category/index.tsx
@@ -0,0 +1,206 @@
+import { Form, Head, router } from '@inertiajs/react';
+import { Plus } from 'lucide-react';
+import { useState } from 'react';
+import InputError from '@/components/input-error';
+import { Button } from '@/components/ui/button';
+import {
+ Dialog,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { destroy, index as categoryIndex, store, update } from '@/routes/admin/master/categories';
+import { createCategoryColumns } from './columns';
+import type { Category } from './columns';
+import { ConfirmDialog } from '@/components/confirm-dialog';
+import { DataTable } from '@/components/data-table';
+
+type Props = {
+ categories: Category[];
+};
+
+export default function CategoryIndex({ categories }: Props) {
+ const [createOpen, setCreateOpen] = useState(false);
+ const [editing, setEditing] = useState(null);
+ const [deleting, setDeleting] = useState(null);
+
+ function handleDelete() {
+ if (!deleting) {
+ return;
+ }
+
+ router.delete(destroy(deleting.id), {
+ onSuccess: () => setDeleting(null),
+ });
+ }
+
+ const columns = createCategoryColumns({
+ handleEdit: (category) => setEditing(category),
+ handleDeleteClick: (category) => setDeleting(category),
+ });
+
+ return (
+ <>
+
+
+
+
+
+
+ Kategori
+
+
+
+
+
+
+
+
+
+ {
+ if (!open) {
+ setDeleting(null);
+ }
+ }}
+ title="Hapus Kategori"
+ description={`Apakah Anda yakin ingin menghapus kategori "${deleting?.name}"? Tindakan ini tidak dapat dibatalkan.`}
+ confirmLabel="Hapus"
+ onConfirm={handleDelete}
+ />
+
+ >
+ );
+}
+
+CategoryIndex.layout = {
+ breadcrumbs: [
+ {
+ title: 'Master',
+ href: categoryIndex(),
+ },
+ {
+ title: 'Kategori',
+ href: categoryIndex(),
+ },
+ ],
+};
diff --git a/routes/web.php b/routes/web.php
index 2085b21..6d9aff6 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -1,11 +1,16 @@
name('home');
Route::middleware(['auth', 'verified'])->group(function () {
Route::inertia('dashboard', 'dashboard')->name('dashboard');
+
+ Route::prefix('admin/master')->name('admin.master.')->group(function () {
+ Route::resource('categories', CategoryController::class)->except(['show', 'create', 'edit']);
+ });
});
require __DIR__.'/settings.php';
diff --git a/tests/Feature/Admin/Master/CategoryTest.php b/tests/Feature/Admin/Master/CategoryTest.php
new file mode 100644
index 0000000..f4d7d7f
--- /dev/null
+++ b/tests/Feature/Admin/Master/CategoryTest.php
@@ -0,0 +1,160 @@
+get(route('admin.master.categories.index'));
+ $response->assertRedirect(route('login'));
+});
+
+test('authenticated users can visit the category index page', function () {
+ $user = User::factory()->create();
+ $this->actingAs($user);
+
+ $response = $this->get(route('admin.master.categories.index'));
+ $response->assertOk();
+});
+
+test('category index page displays categories', function () {
+ $user = User::factory()->create();
+ $this->actingAs($user);
+
+ $categories = Category::factory()->count(3)->create();
+
+ $response = $this->get(route('admin.master.categories.index'));
+ $response->assertOk();
+ $response->assertInertia(fn (Assert $page) => $page
+ ->component('admin/master/category/index')
+ ->has('categories', 3)
+ );
+});
+
+test('category can be created', function () {
+ $user = User::factory()->create();
+ $this->actingAs($user);
+
+ $response = $this->post(route('admin.master.categories.store'), [
+ 'name' => 'Kategori Test',
+ ]);
+
+ $response
+ ->assertSessionHasNoErrors()
+ ->assertRedirect(route('admin.master.categories.index'));
+
+ $this->assertDatabaseHas('categories', [
+ 'name' => 'Kategori Test',
+ 'slug' => 'kategori-test',
+ ]);
+});
+
+test('category name is required', function () {
+ $user = User::factory()->create();
+ $this->actingAs($user);
+
+ $response = $this->post(route('admin.master.categories.store'), [
+ 'name' => '',
+ ]);
+
+ $response->assertSessionHasErrors('name');
+});
+
+test('category name must not exceed 100 characters', function () {
+ $user = User::factory()->create();
+ $this->actingAs($user);
+
+ $response = $this->post(route('admin.master.categories.store'), [
+ 'name' => str_repeat('a', 101),
+ ]);
+
+ $response->assertSessionHasErrors('name');
+});
+
+test('category name must be unique', function () {
+ $user = User::factory()->create();
+ $this->actingAs($user);
+
+ Category::factory()->create(['name' => 'Existing Category']);
+
+ $response = $this->post(route('admin.master.categories.store'), [
+ 'name' => 'Existing Category',
+ ]);
+
+ $response->assertSessionHasErrors('name');
+});
+
+test('category slug is automatically generated', function () {
+ $user = User::factory()->create();
+ $this->actingAs($user);
+
+ $this->post(route('admin.master.categories.store'), [
+ 'name' => 'Kategori Otomatis',
+ ]);
+
+ $this->assertDatabaseHas('categories', [
+ 'name' => 'Kategori Otomatis',
+ 'slug' => 'kategori-otomatis',
+ ]);
+});
+
+test('category can be updated', function () {
+ $user = User::factory()->create();
+ $this->actingAs($user);
+
+ $category = Category::factory()->create();
+
+ $response = $this->put(route('admin.master.categories.update', $category), [
+ 'name' => 'Kategori Updated',
+ ]);
+
+ $response
+ ->assertSessionHasNoErrors()
+ ->assertRedirect(route('admin.master.categories.index'));
+
+ $category->refresh();
+ expect($category->name)->toBe('Kategori Updated');
+ expect($category->slug)->toBe('kategori-updated');
+});
+
+test('category name can be updated to itself', function () {
+ $user = User::factory()->create();
+ $this->actingAs($user);
+
+ $category = Category::factory()->create(['name' => 'My Category']);
+
+ $response = $this->put(route('admin.master.categories.update', $category), [
+ 'name' => 'My Category',
+ ]);
+
+ $response->assertSessionHasNoErrors();
+});
+
+test('category update name must be unique excluding itself', function () {
+ $user = User::factory()->create();
+ $this->actingAs($user);
+
+ $category = Category::factory()->create(['name' => 'First']);
+ Category::factory()->create(['name' => 'Second']);
+
+ $response = $this->put(route('admin.master.categories.update', $category), [
+ 'name' => 'Second',
+ ]);
+
+ $response->assertSessionHasErrors('name');
+});
+
+test('category can be deleted', function () {
+ $user = User::factory()->create();
+ $this->actingAs($user);
+
+ $category = Category::factory()->create();
+
+ $response = $this->delete(route('admin.master.categories.destroy', $category));
+
+ $response
+ ->assertSessionHasNoErrors()
+ ->assertRedirect(route('admin.master.categories.index'));
+
+ $this->assertSoftDeleted('categories', ['id' => $category->id]);
+});
|