From 5ab014281a0299bae82fce80ef1ab4a70b0acd87 Mon Sep 17 00:00:00 2001 From: nico Date: Mon, 27 Apr 2026 17:37:16 +0800 Subject: [PATCH] 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 --- package.json | 2 +- .../(dashboard)/_state/ekonomi/umkm/umkm.ts | 113 ++++++++++++- .../ekonomi/umkm/_lib/layoutTabs.tsx | 8 +- .../umkm/kategori-produk/[id]/edit/page.tsx | 113 +++++++++++++ .../umkm/kategori-produk/create/page.tsx | 82 +++++++++ .../ekonomi/umkm/kategori-produk/page.tsx | 144 ++++++++++++++++ .../api/[[...slugs]]/_lib/ekonomi/index.ts | 2 +- .../_lib/ekonomi/kategori-produk.ts | 31 ---- .../umkm/kategori-produk/kategori-produk.ts | 158 ++++++++++++++++++ 9 files changed, 614 insertions(+), 39 deletions(-) create mode 100644 src/app/admin/(dashboard)/ekonomi/umkm/kategori-produk/[id]/edit/page.tsx create mode 100644 src/app/admin/(dashboard)/ekonomi/umkm/kategori-produk/create/page.tsx create mode 100644 src/app/admin/(dashboard)/ekonomi/umkm/kategori-produk/page.tsx delete mode 100644 src/app/api/[[...slugs]]/_lib/ekonomi/kategori-produk.ts create mode 100644 src/app/api/[[...slugs]]/_lib/ekonomi/umkm/kategori-produk/kategori-produk.ts diff --git a/package.json b/package.json index aa52c445..8c65ce06 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "desa-darmasaba", - "version": "0.1.31", + "version": "0.1.32", "private": true, "scripts": { "dev": "next dev", diff --git a/src/app/admin/(dashboard)/_state/ekonomi/umkm/umkm.ts b/src/app/admin/(dashboard)/_state/ekonomi/umkm/umkm.ts index bd760250..2f8f23a1 100644 --- a/src/app/admin/(dashboard)/_state/ekonomi/umkm/umkm.ts +++ b/src/app/admin/(dashboard)/_state/ekonomi/umkm/umkm.ts @@ -63,6 +63,15 @@ const defaultPenjualanForm = { isActive: true, }; +// Kategori Produk Form Validation +const kategoriProdukFormSchema = z.object({ + nama: z.string().min(1, "Nama kategori wajib diisi"), +}); + +const defaultKategoriProdukForm = { + nama: "", +}; + export const umkmState = proxy({ // UMKM Module umkm: { @@ -101,7 +110,7 @@ export const umkmState = proxy({ loading: false, async submit() { const cek = umkmFormSchema.safeParse(this.form); - if (!cek.success) return toast.error("Cek kembali form anda"); + if (!cek.success) { toast.error("Cek kembali form anda"); return false; } this.loading = true; try { const res = await fetch("/api/ekonomi/umkm/create", { @@ -129,7 +138,7 @@ export const umkmState = proxy({ loading: false, async submit(id: string) { const cek = umkmFormSchema.safeParse(this.form); - if (!cek.success) return toast.error("Cek kembali form anda"); + if (!cek.success) { toast.error("Cek kembali form anda"); return false; } this.loading = true; try { const res = await fetch(`/api/ekonomi/umkm/${id}`, { @@ -237,7 +246,7 @@ export const umkmState = proxy({ loading: false, async submit() { const cek = produkFormSchema.safeParse(this.form); - if (!cek.success) return toast.error("Cek kembali form anda"); + if (!cek.success) { toast.error("Cek kembali form anda"); return false; } this.loading = true; try { const res = await fetch("/api/ekonomi/umkm/produk/create", { @@ -260,7 +269,7 @@ export const umkmState = proxy({ loading: false, async submit(id: string) { const cek = produkFormSchema.safeParse(this.form); - if (!cek.success) return toast.error("Cek kembali form anda"); + if (!cek.success) { toast.error("Cek kembali form anda"); return false; } this.loading = true; try { const res = await fetch(`/api/ekonomi/umkm/produk/${id}`, { @@ -323,7 +332,7 @@ export const umkmState = proxy({ loading: false, async submit() { const cek = penjualanFormSchema.safeParse(this.form); - if (!cek.success) return toast.error("Cek kembali form anda"); + if (!cek.success) { toast.error("Cek kembali form anda"); return false; } this.loading = true; try { const res = await fetch("/api/ekonomi/umkm/penjualan/create", { @@ -368,6 +377,31 @@ export const umkmState = proxy({ // Kategori Produk (Share with Pasar Desa) kategoriProduk: { + findMany: { + data: [] as any[], + page: 1, + totalPages: 1, + loading: false, + search: "", + async load(page = 1, limit = 10, search = "") { + this.loading = true; + this.page = page; + this.search = search; + try { + const params = new URLSearchParams({ + page: page.toString(), + limit: limit.toString(), + search + }); + const res = await fetch(`/api/ekonomi/kategoriproduk/find-many?${params}`); + const result = await res.json(); + if (result.success) { + this.data = result.data; + this.totalPages = result.totalPages; + } + } catch (e) { console.error(e); } finally { this.loading = false; } + } + }, findManyAll: { data: [] as any[], loading: false, @@ -381,6 +415,75 @@ export const umkmState = proxy({ } } catch (e) { console.error(e); } finally { this.loading = false; } } + }, + create: { + form: { ...defaultKategoriProdukForm }, + loading: false, + async submit() { + const cek = kategoriProdukFormSchema.safeParse(this.form); + if (!cek.success) { toast.error("Nama kategori wajib diisi"); return false; } + this.loading = true; + try { + const res = await fetch("/api/ekonomi/kategoriproduk/create", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(this.form) + }); + const result = await res.json(); + if (result.success) { + toast.success("Kategori berhasil dibuat"); + umkmState.kategoriProduk.findMany.load(); + umkmState.kategoriProduk.findManyAll.load(); + return true; + } + toast.error(result.message || "Gagal membuat kategori"); + } catch (e) { toast.error("Gagal membuat kategori"); } finally { this.loading = false; } + return false; + } + }, + update: { + form: { ...defaultKategoriProdukForm }, + loading: false, + async submit(id: string) { + const cek = kategoriProdukFormSchema.safeParse(this.form); + if (!cek.success) { toast.error("Nama kategori wajib diisi"); return false; } + this.loading = true; + try { + const res = await fetch(`/api/ekonomi/kategoriproduk/${id}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(this.form) + }); + const result = await res.json(); + if (result.success) { + toast.success("Kategori berhasil diperbarui"); + umkmState.kategoriProduk.findMany.load(); + umkmState.kategoriProduk.findManyAll.load(); + return true; + } + toast.error(result.message || "Gagal memperbarui kategori"); + } catch (e) { toast.error("Gagal memperbarui kategori"); } finally { this.loading = false; } + return false; + } + }, + del: { + loading: false, + async submit(id: string) { + this.loading = true; + try { + const res = await fetch(`/api/ekonomi/kategoriproduk/del/${id}`, { + method: "DELETE" + }); + const result = await res.json(); + if (result.success) { + toast.success("Kategori berhasil dihapus"); + umkmState.kategoriProduk.findMany.load(); + umkmState.kategoriProduk.findManyAll.load(); + return true; + } + } catch (e) { toast.error("Gagal menghapus kategori"); } finally { this.loading = false; } + return false; + } } }, diff --git a/src/app/admin/(dashboard)/ekonomi/umkm/_lib/layoutTabs.tsx b/src/app/admin/(dashboard)/ekonomi/umkm/_lib/layoutTabs.tsx index 26a79fe9..54b4493f 100644 --- a/src/app/admin/(dashboard)/ekonomi/umkm/_lib/layoutTabs.tsx +++ b/src/app/admin/(dashboard)/ekonomi/umkm/_lib/layoutTabs.tsx @@ -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: }, + { + label: "Kategori Produk", + value: "kategori-produk", + href: "/admin/ekonomi/umkm/kategori-produk", + icon: + }, ]; const currentTab = tabs.find((tab) => pathname.startsWith(tab.href)); diff --git a/src/app/admin/(dashboard)/ekonomi/umkm/kategori-produk/[id]/edit/page.tsx b/src/app/admin/(dashboard)/ekonomi/umkm/kategori-produk/[id]/edit/page.tsx new file mode 100644 index 00000000..234472d6 --- /dev/null +++ b/src/app/admin/(dashboard)/ekonomi/umkm/kategori-produk/[id]/edit/page.tsx @@ -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 ( +
+ +
+ ); + } + + return ( + + + + Edit Kategori Produk + + + + + setNama(e.target.value)} + /> + + + + + + + + + ); +} diff --git a/src/app/admin/(dashboard)/ekonomi/umkm/kategori-produk/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/umkm/kategori-produk/create/page.tsx new file mode 100644 index 00000000..92853203 --- /dev/null +++ b/src/app/admin/(dashboard)/ekonomi/umkm/kategori-produk/create/page.tsx @@ -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 ( + + + + Tambah Kategori Produk Baru + + + + + (state.form.nama = e.target.value)} + /> + + + + + + + + + ); +} diff --git a/src/app/admin/(dashboard)/ekonomi/umkm/kategori-produk/page.tsx b/src/app/admin/(dashboard)/ekonomi/umkm/kategori-produk/page.tsx new file mode 100644 index 00000000..1c09ab3a --- /dev/null +++ b/src/app/admin/(dashboard)/ekonomi/umkm/kategori-produk/page.tsx @@ -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(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 ( + + + Kategori Produk + + + + + } + value={search} + onChange={(e) => setSearch(e.currentTarget.value)} + mb="md" + /> + + {state.loading ? ( + + ) : ( + + + + + Nama Kategori + Status + Aksi + + + + {state.data.map((item) => ( + + {item.nama} + + + {item.isActive ? "Aktif" : "Nonaktif"} + + + + + + + + + + ))} + +
+
+ )} + +
+ state.load(p, 10, debouncedSearch)} + /> +
+
+ + setModalHapus(false)} + onConfirm={handleHapus} + text="Apakah Anda yakin ingin menghapus kategori ini? Kategori yang dihapus tidak akan muncul di pilihan kategori produk baru." + /> +
+ ); +} + +export default KategoriProdukPage; diff --git a/src/app/api/[[...slugs]]/_lib/ekonomi/index.ts b/src/app/api/[[...slugs]]/_lib/ekonomi/index.ts index 42daa93f..7a7e6670 100644 --- a/src/app/api/[[...slugs]]/_lib/ekonomi/index.ts +++ b/src/app/api/[[...slugs]]/_lib/ekonomi/index.ts @@ -9,7 +9,7 @@ import DemografiPekerjaan from "./demografi-pekerjaan"; import JumlahPengangguran from "./jumlah-pengangguran"; import PendapatanAsliDesa from "./pendapatan-asli-desa"; import StrukturOrganisasi from "./struktur-bumdes"; -import KategoriProduk from "./kategori-produk"; +import KategoriProduk from "./umkm/kategori-produk/kategori-produk"; import Umkm from "./umkm"; import ProdukUmkm from "./umkm/produk"; import PenjualanProduk from "./umkm/penjualan"; diff --git a/src/app/api/[[...slugs]]/_lib/ekonomi/kategori-produk.ts b/src/app/api/[[...slugs]]/_lib/ekonomi/kategori-produk.ts deleted file mode 100644 index 3fd7fb26..00000000 --- a/src/app/api/[[...slugs]]/_lib/ekonomi/kategori-produk.ts +++ /dev/null @@ -1,31 +0,0 @@ -import prisma from "@/lib/prisma"; -import Elysia from "elysia"; - -const KategoriProduk = new Elysia({ - prefix: "/kategoriproduk", -}) - .get("/find-many-all", async () => { - try { - const data = await prisma.kategoriProduk.findMany({ - where: { - isActive: true, - deletedAt: null, - }, - orderBy: { nama: 'asc' }, - }); - - return { - success: true, - message: "Berhasil mengambil semua kategori produk", - data, - }; - } catch (e) { - console.error("Error di KategoriProduk find-many-all:", e); - return { - success: false, - message: "Gagal mengambil data kategori produk", - }; - } - }); - -export default KategoriProduk; diff --git a/src/app/api/[[...slugs]]/_lib/ekonomi/umkm/kategori-produk/kategori-produk.ts b/src/app/api/[[...slugs]]/_lib/ekonomi/umkm/kategori-produk/kategori-produk.ts new file mode 100644 index 00000000..4d9393b0 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/ekonomi/umkm/kategori-produk/kategori-produk.ts @@ -0,0 +1,158 @@ +import prisma from "@/lib/prisma"; +import Elysia, { t } from "elysia"; + +const KategoriProduk = new Elysia({ + prefix: "/kategoriproduk", +}) + .get("/find-many-all", async () => { + try { + const data = await prisma.kategoriProduk.findMany({ + where: { + isActive: true, + deletedAt: null, + }, + orderBy: { nama: 'asc' }, + }); + + return { + success: true, + message: "Berhasil mengambil semua kategori produk", + data, + }; + } catch (e) { + console.error("Error di KategoriProduk find-many-all:", e); + return { + success: false, + message: "Gagal mengambil data kategori produk", + }; + } + }) + .get("/find-many", async ({ query }) => { + try { + const { page = 1, limit = 10, search = "" } = query; + const skip = (Number(page) - 1) * Number(limit); + const take = Number(limit); + + const where = { + isActive: true, + deletedAt: null, + nama: { contains: search, mode: 'insensitive' as const }, + }; + + const [data, total] = await Promise.all([ + prisma.kategoriProduk.findMany({ + where, + skip, + take, + orderBy: { createdAt: 'desc' }, + }), + prisma.kategoriProduk.count({ where }), + ]); + + return { + success: true, + message: "Berhasil mengambil data kategori produk", + data, + total, + page: Number(page), + limit: Number(limit), + totalPages: Math.ceil(total / take), + }; + } catch (e) { + console.error("Error di KategoriProduk find-many:", e); + return { + success: false, + message: "Gagal mengambil data kategori produk", + }; + } + }, { + query: t.Object({ + page: t.Optional(t.String()), + limit: t.Optional(t.String()), + search: t.Optional(t.String()), + }) + }) + .post("/create", async ({ body }) => { + try { + const data = await prisma.kategoriProduk.create({ + data: { + nama: body.nama, + isActive: true, + }, + }); + + return { + success: true, + message: "Berhasil membuat kategori produk", + data, + }; + } catch (e) { + console.error("Error di KategoriProduk create:", e); + return { + success: false, + message: "Gagal membuat kategori produk", + }; + } + }, { + body: t.Object({ + nama: t.String(), + }) + }) + .put("/:id", async ({ params, body }) => { + try { + const data = await prisma.kategoriProduk.update({ + where: { id: params.id }, + data: { + nama: body.nama, + }, + }); + + return { + success: true, + message: "Berhasil memperbarui kategori produk", + data, + }; + } catch (e) { + console.error("Error di KategoriProduk update:", e); + return { + success: false, + message: "Gagal memperbarui kategori produk", + }; + } + }, { + params: t.Object({ + id: t.String(), + }), + body: t.Object({ + nama: t.String(), + }) + }) + .delete("/del/:id", async ({ params }) => { + try { + const data = await prisma.kategoriProduk.update({ + where: { id: params.id }, + data: { + isActive: false, + deletedAt: new Date(), + }, + }); + + return { + success: true, + message: "Berhasil menghapus kategori produk", + data, + }; + } catch (e) { + console.error("Error di KategoriProduk delete:", e); + return { + success: false, + message: "Gagal menghapus kategori produk", + }; + } + }, { + params: t.Object({ + id: t.String(), + }) + }); + +export default KategoriProduk;