feat(umkm): implement full CRUD for product categories
- added CRUD endpoints for KategoriProduk in Elysia API - updated umkmState with category management logic - added 'Kategori Produk' tab in admin dashboard - created list, create, and edit pages for category management - bumped version to 0.1.32
This commit is contained in:
@@ -11,7 +11,7 @@ import {
|
||||
TabsTab,
|
||||
Title
|
||||
} from '@mantine/core';
|
||||
import { IconDashboard, IconBuildingStore, IconPackage, IconShoppingCart } from '@tabler/icons-react';
|
||||
import { IconDashboard, IconBuildingStore, IconPackage, IconShoppingCart, IconTag } from '@tabler/icons-react';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
@@ -44,6 +44,12 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
||||
href: "/admin/ekonomi/umkm/penjualan",
|
||||
icon: <IconShoppingCart size={18} stroke={1.8} />
|
||||
},
|
||||
{
|
||||
label: "Kategori Produk",
|
||||
value: "kategori-produk",
|
||||
href: "/admin/ekonomi/umkm/kategori-produk",
|
||||
icon: <IconTag size={18} stroke={1.8} />
|
||||
},
|
||||
];
|
||||
|
||||
const currentTab = tabs.find((tab) => pathname.startsWith(tab.href));
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
'use client';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
Paper,
|
||||
Stack,
|
||||
TextInput,
|
||||
Title,
|
||||
Center,
|
||||
Loader
|
||||
} from '@mantine/core';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import umkmState from '../../../../../_state/ekonomi/umkm/umkm';
|
||||
|
||||
export default function EditKategoriProduk() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const id = params.id as string;
|
||||
const state = useProxy(umkmState.kategoriProduk.findMany);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [nama, setNama] = useState("");
|
||||
const [originalNama, setOriginalNama] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
// Find the item from the existing list or load it if not available
|
||||
if (state.data.length === 0) {
|
||||
await state.load();
|
||||
}
|
||||
|
||||
const item = state.data.find((v: any) => v.id === id);
|
||||
if (item) {
|
||||
setNama(item.nama);
|
||||
setOriginalNama(item.nama);
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
init();
|
||||
}, [id, state]);
|
||||
|
||||
const handleResetForm = () => {
|
||||
setNama(originalNama);
|
||||
};
|
||||
|
||||
const handleUpdate = async () => {
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
umkmState.kategoriProduk.update.form.nama = nama;
|
||||
const success = await umkmState.kategoriProduk.update.submit(id);
|
||||
if (success) {
|
||||
router.push('/admin/ekonomi/umkm/kategori-produk');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Center h={400}>
|
||||
<Loader size="lg" />
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Group mb="lg">
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => router.back()}
|
||||
leftSection={<IconArrowBack size={20} />}
|
||||
>
|
||||
Kembali
|
||||
</Button>
|
||||
<Title order={3}>Edit Kategori Produk</Title>
|
||||
</Group>
|
||||
|
||||
<Paper withBorder p="xl" radius="md" shadow="sm" maw={600}>
|
||||
<Stack gap="lg">
|
||||
<TextInput
|
||||
label="Nama Kategori"
|
||||
placeholder="Contoh: Makanan, Minuman, Kerajinan"
|
||||
required
|
||||
value={nama}
|
||||
onChange={(e) => setNama(e.target.value)}
|
||||
/>
|
||||
|
||||
<Group justify="flex-end" mt="xl">
|
||||
<Button variant="outline" color="gray" onClick={handleResetForm}>
|
||||
Reset
|
||||
</Button>
|
||||
<Button
|
||||
color="blue"
|
||||
onClick={handleUpdate}
|
||||
loading={isSubmitting}
|
||||
disabled={!nama.trim()}
|
||||
>
|
||||
Simpan Perubahan
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
'use client';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
Paper,
|
||||
Stack,
|
||||
TextInput,
|
||||
Title
|
||||
} from '@mantine/core';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import umkmState from '../../../../_state/ekonomi/umkm/umkm';
|
||||
|
||||
export default function CreateKategoriProduk() {
|
||||
const router = useRouter();
|
||||
const state = useProxy(umkmState.kategoriProduk.create);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const handleResetForm = () => {
|
||||
state.form = {
|
||||
nama: "",
|
||||
};
|
||||
};
|
||||
|
||||
const handleCreate = async () => {
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const success = await umkmState.kategoriProduk.create.submit();
|
||||
if (success) {
|
||||
handleResetForm();
|
||||
router.push('/admin/ekonomi/umkm/kategori-produk');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Group mb="lg">
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => router.back()}
|
||||
leftSection={<IconArrowBack size={20} />}
|
||||
>
|
||||
Kembali
|
||||
</Button>
|
||||
<Title order={3}>Tambah Kategori Produk Baru</Title>
|
||||
</Group>
|
||||
|
||||
<Paper withBorder p="xl" radius="md" shadow="sm" maw={600}>
|
||||
<Stack gap="lg">
|
||||
<TextInput
|
||||
label="Nama Kategori"
|
||||
placeholder="Contoh: Makanan, Minuman, Kerajinan"
|
||||
required
|
||||
value={state.form.nama}
|
||||
onChange={(e) => (state.form.nama = e.target.value)}
|
||||
/>
|
||||
|
||||
<Group justify="flex-end" mt="xl">
|
||||
<Button variant="outline" color="gray" onClick={handleResetForm}>
|
||||
Reset
|
||||
</Button>
|
||||
<Button
|
||||
color="blue"
|
||||
onClick={handleCreate}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
Simpan Kategori
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
144
src/app/admin/(dashboard)/ekonomi/umkm/kategori-produk/page.tsx
Normal file
144
src/app/admin/(dashboard)/ekonomi/umkm/kategori-produk/page.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
'use client'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
Group,
|
||||
Pagination,
|
||||
Paper,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Table,
|
||||
TableTbody,
|
||||
TableTd,
|
||||
TableTh,
|
||||
TableThead,
|
||||
TableTr,
|
||||
Title,
|
||||
TextInput,
|
||||
Badge
|
||||
} from '@mantine/core';
|
||||
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||
import { IconPlus, IconSearch, IconEdit, IconTrash } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import umkmState from '../../../_state/ekonomi/umkm/umkm';
|
||||
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||
|
||||
function KategoriProdukPage() {
|
||||
const router = useRouter();
|
||||
const [search, setSearch] = useState("");
|
||||
const state = useProxy(umkmState.kategoriProduk.findMany);
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||
|
||||
const [modalHapus, setModalHapus] = useState(false);
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
|
||||
useShallowEffect(() => {
|
||||
state.load(state.page, 10, debouncedSearch);
|
||||
}, [state.page, debouncedSearch]);
|
||||
|
||||
const handleHapus = async () => {
|
||||
if (selectedId) {
|
||||
const success = await umkmState.kategoriProduk.del.submit(selectedId);
|
||||
if (success) {
|
||||
setModalHapus(false);
|
||||
setSelectedId(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<Group justify="space-between">
|
||||
<Title order={3}>Kategori Produk</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
onClick={() => router.push('/admin/ekonomi/umkm/kategori-produk/create')}
|
||||
>
|
||||
Tambah Kategori
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<TextInput
|
||||
placeholder="Cari kategori..."
|
||||
leftSection={<IconSearch size={18} />}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
mb="md"
|
||||
/>
|
||||
|
||||
{state.loading ? (
|
||||
<Skeleton height={400} />
|
||||
) : (
|
||||
<Box style={{ overflowX: 'auto' }}>
|
||||
<Table highlightOnHover>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>Nama Kategori</TableTh>
|
||||
<TableTh>Status</TableTh>
|
||||
<TableTh>Aksi</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{state.data.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd fw={500}>{item.nama}</TableTd>
|
||||
<TableTd>
|
||||
<Badge color={item.isActive ? "green" : "red"}>
|
||||
{item.isActive ? "Aktif" : "Nonaktif"}
|
||||
</Badge>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Group gap="xs">
|
||||
<Button
|
||||
variant="subtle"
|
||||
color="blue"
|
||||
size="xs"
|
||||
onClick={() => router.push(`/admin/ekonomi/umkm/kategori-produk/${item.id}/edit`)}
|
||||
>
|
||||
<IconEdit size={16} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="subtle"
|
||||
color="red"
|
||||
size="xs"
|
||||
onClick={() => {
|
||||
setSelectedId(item.id);
|
||||
setModalHapus(true);
|
||||
}}
|
||||
>
|
||||
<IconTrash size={16} />
|
||||
</Button>
|
||||
</Group>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Center mt="md">
|
||||
<Pagination
|
||||
total={state.totalPages}
|
||||
value={state.page}
|
||||
onChange={(p) => state.load(p, 10, debouncedSearch)}
|
||||
/>
|
||||
</Center>
|
||||
</Paper>
|
||||
|
||||
<ModalKonfirmasiHapus
|
||||
opened={modalHapus}
|
||||
onClose={() => setModalHapus(false)}
|
||||
onConfirm={handleHapus}
|
||||
text="Apakah Anda yakin ingin menghapus kategori ini? Kategori yang dihapus tidak akan muncul di pilihan kategori produk baru."
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default KategoriProdukPage;
|
||||
Reference in New Issue
Block a user