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
+ }
+ color="blue"
+ onClick={() => router.push('/admin/ekonomi/umkm/kategori-produk/create')}
+ >
+ Tambah Kategori
+
+
+
+
+ }
+ 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;