diff --git a/MIND/PLAN/umkm-module.md b/MIND/PLAN/umkm-module.md
index 0afb2f03..8b89ce95 100644
--- a/MIND/PLAN/umkm-module.md
+++ b/MIND/PLAN/umkm-module.md
@@ -28,3 +28,7 @@ Implement UMKM, ProdukUmkm, and PenjualanProduk module with CRUD API and Dashboa
- [x] Step 12: Implement Produk UI Page
- [x] Step 13: Implement Penjualan UI Page
- [x] Step 14: Register UI pages in Admin Menu
+- [x] Step 15: Implement Public UMKM Directory Page
+- [x] Step 16: Implement Public UMKM Detail Page
+- [x] Step 17: Implement Public Product Catalog Page
+- [x] Step 18: Register public pages in Navbar
diff --git a/MIND/SUMMARY/umkm-module-summary.md b/MIND/SUMMARY/umkm-module-summary.md
index 41e56f9e..0f72b5dd 100644
--- a/MIND/SUMMARY/umkm-module-summary.md
+++ b/MIND/SUMMARY/umkm-module-summary.md
@@ -8,6 +8,9 @@
- Implemented the Admin UI with a modern tab-based layout.
- Created four main admin pages: Dashboard, Data UMKM, Produk, and Penjualan.
- Registered the new UMKM module in the Admin Navigation Menu for all roles.
+- Implemented the Public UI for citizens to browse local businesses.
+- Created three public pages: Direktori UMKM, UMKM Detail, and Katalog Produk.
+- Registered the public UMKM pages in the main Website Navbar under the Ekonomi section.
- Verified the implementation with `tsc` and `bun run build`.
## Files Created/Modified
diff --git a/package.json b/package.json
index 93277391..746d8fce 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "desa-darmasaba",
- "version": "0.1.13",
+ "version": "0.1.14",
"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 764f5dde..025fee92 100644
--- a/src/app/admin/(dashboard)/_state/ekonomi/umkm/umkm.ts
+++ b/src/app/admin/(dashboard)/_state/ekonomi/umkm/umkm.ts
@@ -122,6 +122,20 @@ export const umkmState = proxy({
}
return false;
}
+ },
+ findUnique: {
+ data: null as any,
+ loading: false,
+ async load(id: string) {
+ this.loading = true;
+ try {
+ const res = await fetch(`/api/ekonomi/umkm/${id}`);
+ const result = await res.json();
+ if (result.success) {
+ this.data = result.data;
+ }
+ } catch (e) { console.error(e); } finally { this.loading = false; }
+ }
}
},
@@ -132,10 +146,17 @@ export const umkmState = proxy({
page: 1,
totalPages: 1,
loading: false,
- async load(page = 1, limit = 10, search = "", umkmId = "") {
+ async load(page = 1, limit = 10, search = "", umkmId = "", kategoriId = "") {
this.loading = true;
+ this.page = page;
try {
- const params = new URLSearchParams({ page: page.toString(), limit: limit.toString(), search, umkmId });
+ const params = new URLSearchParams({
+ page: page.toString(),
+ limit: limit.toString(),
+ search,
+ umkmId,
+ kategoriId
+ });
const res = await fetch(`/api/ekonomi/umkm/produk/find-many?${params}`);
const result = await res.json();
if (result.success) {
@@ -215,6 +236,24 @@ export const umkmState = proxy({
}
},
+ // Kategori Produk (Share with Pasar Desa)
+ kategoriProduk: {
+ findManyAll: {
+ data: [] as any[],
+ loading: false,
+ async load() {
+ this.loading = true;
+ try {
+ const res = await fetch("/api/ekonomi/pasar-desa/kategori-produk/find-many-all");
+ const result = await res.json();
+ if (result.success) {
+ this.data = result.data;
+ }
+ } catch (e) { console.error(e); } finally { this.loading = false; }
+ }
+ }
+ },
+
// Dashboard Module
dashboard: {
kpi: { data: null as any, loading: false },
diff --git a/src/app/darmasaba/(pages)/ekonomi/umkm/[id]/page.tsx b/src/app/darmasaba/(pages)/ekonomi/umkm/[id]/page.tsx
new file mode 100644
index 00000000..41f64836
--- /dev/null
+++ b/src/app/darmasaba/(pages)/ekonomi/umkm/[id]/page.tsx
@@ -0,0 +1,120 @@
+'use client'
+import umkmState from '@/app/admin/(dashboard)/_state/ekonomi/umkm/umkm';
+import colors from '@/con/colors';
+import { Box, Card, Flex, Grid, GridCol, Image, Paper, Skeleton, Stack, Text, Title, Badge, SimpleGrid, Group, Divider, Button } from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconBrandWhatsapp, IconMapPinFilled, IconPackage, IconUser } from '@tabler/icons-react';
+import { useParams } from 'next/navigation';
+import { useProxy } from 'valtio/utils';
+import BackButton from '../../../desa/layanan/_com/BackButto';
+
+function Page() {
+ const params = useParams();
+ const id = params.id as string;
+ const state = useProxy(umkmState.umkm.findUnique);
+
+ useShallowEffect(() => {
+ if (id) state.load(id);
+ }, [id]);
+
+ if (state.loading || !state.data) {
+ return (
+
+
+
+ );
+ }
+
+ const u = state.data;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ {u.kategori?.nama}
+ {u.nama}
+
+
+
+ {u.pemilik}
+
+
+
+ {u.alamat || 'Desa Darmasaba'}
+
+
+
+ {u.deskripsi || 'Tidak ada deskripsi tersedia.'}
+ {u.kontak && (
+ }
+ color="green"
+ radius="md"
+ component="a"
+ href={`https://wa.me/${u.kontak}`}
+ target="_blank"
+ w="fit-content"
+ >
+ Hubungi Pemilik
+
+ )}
+
+
+
+
+
+
+ Katalog Produk
+
+ {u.produk?.map((p: any, k: number) => (
+
+
+
+
+
+ {p.nama}
+ 0 ? 'green' : 'red'}>
+ {p.stok > 0 ? 'Tersedia' : 'Habis'}
+
+
+
+ Rp {p.harga.toLocaleString('id-ID')}
+
+
+ {p.deskripsi || 'Kualitas terbaik dari UMKM lokal.'}
+
+
+ ))}
+
+ {(!u.produk || u.produk.length === 0) && (
+
+ Belum ada produk dari UMKM ini
+
+ )}
+
+
+ );
+}
+
+export default Page;
diff --git a/src/app/darmasaba/(pages)/ekonomi/umkm/page.tsx b/src/app/darmasaba/(pages)/ekonomi/umkm/page.tsx
new file mode 100644
index 00000000..65c55697
--- /dev/null
+++ b/src/app/darmasaba/(pages)/ekonomi/umkm/page.tsx
@@ -0,0 +1,153 @@
+'use client'
+import umkmState from '@/app/admin/(dashboard)/_state/ekonomi/umkm/umkm';
+import colors from '@/con/colors';
+import { Box, Center, Flex, Grid, GridCol, Image, Pagination, Paper, Select, SimpleGrid, Skeleton, Stack, Text, TextInput, Title, Badge } from '@mantine/core';
+import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
+import { IconMapPinFilled, IconSearch, IconUser } from '@tabler/icons-react';
+import { motion } from 'motion/react';
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+import BackButton from '../../desa/layanan/_com/BackButto';
+
+function Page() {
+ const router = useRouter()
+ const state = useProxy(umkmState.umkm)
+ const [search, setSearch] = useState('');
+ const [debouncedSearch] = useDebouncedValue(search, 1000);
+ const [selectedCategory, setSelectedCategory] = useState(null);
+
+ const { data, page, loading, totalPages, load } = state.findMany
+
+ useShallowEffect(() => {
+ umkmState.kategoriProduk.findManyAll.load()
+ }, [])
+
+ useShallowEffect(() => {
+ load(page, 8, debouncedSearch, selectedCategory || undefined)
+ }, [page, debouncedSearch, selectedCategory])
+
+ if (loading || !data) {
+ return (
+
+
+
+ )
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+ Direktori UMKM Desa Darmasaba
+
+
+
+ setSearch(e.target.value)}
+ leftSection={}
+ w={"100%"}
+ />
+
+
+
+
+ Daftar Usaha Mikro, Kecil, dan Menengah yang ada di wilayah Desa Darmasaba.
+ Mendukung pertumbuhan ekonomi lokal melalui pemberdayaan pengusaha desa.
+
+
+
+
+
+
+
+
+
+
+
+ {data?.map((v, k) => (
+ router.push(`/darmasaba/ekonomi/umkm/${v.id}`)}
+ whileHover={{ scale: 1.03 }}
+ whileTap={{ scale: 0.98 }}
+ >
+
+
+
+ {v.kategori?.nama}
+
+
+ {v.nama}
+
+
+
+ {v.pemilik}
+
+
+
+ {v.alamat || 'Darmasaba'}
+
+
+
+ ))}
+
+
+ {data.length === 0 && (
+
+ Tidak ada UMKM ditemukan
+
+ )}
+
+
+ load(newPage)}
+ total={totalPages}
+ my="md"
+ />
+
+
+
+
+ );
+}
+
+export default Page;
diff --git a/src/app/darmasaba/(pages)/ekonomi/umkm/produk/page.tsx b/src/app/darmasaba/(pages)/ekonomi/umkm/produk/page.tsx
new file mode 100644
index 00000000..c406e3db
--- /dev/null
+++ b/src/app/darmasaba/(pages)/ekonomi/umkm/produk/page.tsx
@@ -0,0 +1,190 @@
+'use client'
+import umkmState from '@/app/admin/(dashboard)/_state/ekonomi/umkm/umkm';
+import colors from '@/con/colors';
+import { Box, Center, Flex, Grid, GridCol, Image, Pagination, Paper, Select, SimpleGrid, Skeleton, Stack, Text, TextInput, Title, Badge, Card, Group } from '@mantine/core';
+import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
+import { IconBuildingStore, IconSearch, IconTag } from '@tabler/icons-react';
+import { motion } from 'motion/react';
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+import BackButton from '../../../desa/layanan/_com/BackButto';
+
+function Page() {
+ const router = useRouter()
+ const state = useProxy(umkmState.produk)
+ const [search, setSearch] = useState('');
+ const [debouncedSearch] = useDebouncedValue(search, 1000);
+ const [selectedCategory, setSelectedCategory] = useState(null);
+ const [selectedUmkm, setSelectedUmkm] = useState(null);
+
+ const { data, page, loading, totalPages, load } = state.findMany
+
+ useShallowEffect(() => {
+ umkmState.kategoriProduk.findManyAll.load()
+ // Load all UMKM for filter
+ fetch('/api/ekonomi/umkm/find-many-all')
+ .then(r => r.json())
+ .then(res => {
+ if (res.success) {
+ (umkmState.umkm as any).allList = res.data;
+ }
+ })
+ }, [])
+
+ const allUmkm = useProxy(umkmState.umkm as any).allList || [];
+
+ useShallowEffect(() => {
+ load(page, 12, debouncedSearch, selectedUmkm || undefined, selectedCategory || undefined)
+ }, [page, debouncedSearch, selectedUmkm, selectedCategory])
+
+ if (loading || !data) {
+ return (
+
+
+
+ )
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+ Katalog Produk UMKM
+
+
+
+ setSearch(e.currentTarget.value)}
+ leftSection={}
+ w={"100%"}
+ />
+
+
+
+
+ Temukan berbagai produk unggulan dari pelaku UMKM di Desa Darmasaba.
+ Mulai dari kuliner, kerajinan tangan, hingga jasa tersedia untuk memenuhi kebutuhan Anda.
+
+
+
+
+
+
+
+
+
+ {data?.map((v, k) => (
+
+
+
+
+
+
+
+
+
+ 0 ? 'teal' : 'red'} variant="light">
+ {v.stok > 0 ? 'Tersedia' : 'Habis'}
+
+ Stok: {v.stok}
+
+
+ {v.nama}
+
+
+
+ router.push(`/darmasaba/ekonomi/umkm/${v.umkmId}`)}>
+ {v.umkm?.nama}
+
+
+
+
+
+ {v.umkm?.kategori?.nama}
+
+
+
+
+
+ Rp {v.harga.toLocaleString('id-ID')}
+
+
+
+
+
+ ))}
+
+
+ {data.length === 0 && (
+
+
+ Produk tidak ditemukan
+ Coba gunakan kata kunci atau filter lain
+
+
+ )}
+
+
+ load(newPage)}
+ total={totalPages}
+ my="md"
+ color="blue"
+ radius="md"
+ />
+
+
+
+
+ );
+}
+
+export default Page;
diff --git a/src/con/navbar-list-menu.ts b/src/con/navbar-list-menu.ts
index fb14726a..6c063aa1 100644
--- a/src/con/navbar-list-menu.ts
+++ b/src/con/navbar-list-menu.ts
@@ -182,41 +182,51 @@ const navbarListMenu = [
},
{
id: "5.3",
+ name: "Direktori UMKM",
+ href: "/darmasaba/ekonomi/umkm"
+ },
+ {
+ id: "5.4",
+ name: "Produk UMKM",
+ href: "/darmasaba/ekonomi/umkm/produk"
+ },
+ {
+ id: "5.5",
name: "Struktur Organisasi dan SK Pengurus BUMDesa",
href: "/darmasaba/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes"
},
{
- id: "5.4",
+ id: "5.6",
name: "PADesa (Pendapatan Asli Desa)",
href: "/darmasaba/ekonomi/PADesa-pendapatan-asli-desa"
},
{
- id: "5.5",
+ id: "5.7",
name: "Jumlah Pengangguran",
href: "/darmasaba/ekonomi/jumlah-pengangguran"
},
{
- id: "5.6",
+ id: "5.8",
name: "Jumlah penduduk usia kerja yang menganggur",
href: "/darmasaba/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur"
},
{
- id: "5.7",
+ id: "5.9",
name: "Jumlah Penduduk Miskin",
href: "/darmasaba/ekonomi/jumlah-penduduk-miskin"
},
{
- id: "5.8",
+ id: "5.10",
name: "Program Kemiskinan",
href: "/darmasaba/ekonomi/program-kemiskinan"
},
{
- id: "5.9",
+ id: "5.11",
name: "Sektor Unggulan Desa",
href: "/darmasaba/ekonomi/sektor-unggulan-desa"
},
{
- id: "5.10",
+ id: "5.12",
name: "Demografi Pekerjaan",
href: "/darmasaba/ekonomi/demografi-pekerjaan"
}