feat(admin-ui): implement UMKM admin dashboard and CRUD pages
This commit is contained in:
@@ -22,3 +22,9 @@ Implement UMKM, ProdukUmkm, and PenjualanProduk module with CRUD API and Dashboa
|
|||||||
- [x] Step 6: Implement Dashboard API
|
- [x] Step 6: Implement Dashboard API
|
||||||
- [x] Step 7: Register routers
|
- [x] Step 7: Register routers
|
||||||
- [x] Step 8: Verify changes
|
- [x] Step 8: Verify changes
|
||||||
|
- [x] Step 9: Implement Admin UI Layout and Tabs
|
||||||
|
- [x] Step 10: Implement Dashboard UI Page
|
||||||
|
- [x] Step 11: Implement Data UMKM UI Page
|
||||||
|
- [x] Step 12: Implement Produk UI Page
|
||||||
|
- [x] Step 13: Implement Penjualan UI Page
|
||||||
|
- [x] Step 14: Register UI pages in Admin Menu
|
||||||
|
|||||||
@@ -5,18 +5,24 @@
|
|||||||
- Implemented a complete set of CRUD API endpoints for UMKM, Products, and Sales.
|
- Implemented a complete set of CRUD API endpoints for UMKM, Products, and Sales.
|
||||||
- Developed a comprehensive Dashboard API providing KPIs, sales summaries, top products, and detailed stock analytics.
|
- Developed a comprehensive Dashboard API providing KPIs, sales summaries, top products, and detailed stock analytics.
|
||||||
- Integrated the new module into the existing `ekonomi` router.
|
- Integrated the new module into the existing `ekonomi` router.
|
||||||
- Verified the implementation with `tsc` to ensure type safety.
|
- 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.
|
||||||
|
- Verified the implementation with `tsc` and `bun run build`.
|
||||||
|
|
||||||
## Files Created/Modified
|
## Files Created/Modified
|
||||||
### Modified
|
### Modified
|
||||||
- `prisma/schema.prisma`: Added relations and models.
|
- `prisma/schema.prisma`: Added relations and models.
|
||||||
- `src/app/api/[[...slugs]]/_lib/ekonomi/index.ts`: Registered new routers.
|
- `src/app/api/[[...slugs]]/_lib/ekonomi/index.ts`: Registered new routers.
|
||||||
|
- `src/app/admin/_com/list_PageAdmin.tsx`: Registered new UI pages in menu.
|
||||||
|
|
||||||
### Created
|
### Created
|
||||||
- `src/app/api/[[...slugs]]/_lib/ekonomi/umkm/`: CRUD for UMKM.
|
- `src/app/api/[[...slugs]]/_lib/ekonomi/umkm/`: CRUD for UMKM.
|
||||||
- `src/app/api/[[...slugs]]/_lib/ekonomi/umkm/produk/`: CRUD for Products.
|
- `src/app/api/[[...slugs]]/_lib/ekonomi/umkm/produk/`: CRUD for Products.
|
||||||
- `src/app/api/[[...slugs]]/_lib/ekonomi/umkm/penjualan/`: CRUD for Sales with stock management.
|
- `src/app/api/[[...slugs]]/_lib/ekonomi/umkm/penjualan/`: CRUD for Sales with stock management.
|
||||||
- `src/app/api/[[...slugs]]/_lib/ekonomi/umkm/dashboard/`: Analytics endpoints.
|
- `src/app/api/[[...slugs]]/_lib/ekonomi/umkm/dashboard/`: Analytics endpoints.
|
||||||
|
- `src/app/admin/(dashboard)/ekonomi/umkm/`: Admin UI pages and layouts.
|
||||||
|
- `src/app/admin/(dashboard)/_state/ekonomi/umkm/umkm.ts`: Valtio state for the UMKM module.
|
||||||
|
|
||||||
## Stock Management Logic
|
## Stock Management Logic
|
||||||
- Creating a sale decrements product stock.
|
- Creating a sale decrements product stock.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "desa-darmasaba",
|
"name": "desa-darmasaba",
|
||||||
"version": "0.1.12",
|
"version": "0.1.13",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
|
|||||||
251
src/app/admin/(dashboard)/_state/ekonomi/umkm/umkm.ts
Normal file
251
src/app/admin/(dashboard)/_state/ekonomi/umkm/umkm.ts
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import { proxy } from "valtio";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
// UMKM Form Validation
|
||||||
|
const umkmFormSchema = z.object({
|
||||||
|
nama: z.string().min(1, "Nama minimal 1 karakter"),
|
||||||
|
pemilik: z.string().min(1, "Nama pemilik wajib diisi"),
|
||||||
|
kategoriId: z.string().min(1, "Kategori wajib dipilih"),
|
||||||
|
deskripsi: z.string().optional(),
|
||||||
|
alamat: z.string().optional(),
|
||||||
|
kontak: z.string().optional(),
|
||||||
|
imageId: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultUmkmForm = {
|
||||||
|
nama: "",
|
||||||
|
pemilik: "",
|
||||||
|
kategoriId: "",
|
||||||
|
deskripsi: "",
|
||||||
|
alamat: "",
|
||||||
|
kontak: "",
|
||||||
|
imageId: "",
|
||||||
|
isActive: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Produk Form Validation
|
||||||
|
const produkFormSchema = z.object({
|
||||||
|
nama: z.string().min(1, "Nama produk minimal 1 karakter"),
|
||||||
|
harga: z.number().min(0, "Harga tidak boleh negatif"),
|
||||||
|
stok: z.number().min(0, "Stok tidak boleh negatif"),
|
||||||
|
umkmId: z.string().min(1, "UMKM wajib dipilih"),
|
||||||
|
deskripsi: z.string().optional(),
|
||||||
|
imageId: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultProdukForm = {
|
||||||
|
nama: "",
|
||||||
|
harga: 0,
|
||||||
|
stok: 0,
|
||||||
|
umkmId: "",
|
||||||
|
deskripsi: "",
|
||||||
|
imageId: "",
|
||||||
|
isActive: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Penjualan Form Validation
|
||||||
|
const penjualanFormSchema = z.object({
|
||||||
|
produkId: z.string().min(1, "Produk wajib dipilih"),
|
||||||
|
jumlah: z.number().min(1, "Jumlah minimal 1"),
|
||||||
|
hargaSatuan: z.number().min(0, "Harga tidak boleh negatif"),
|
||||||
|
tanggal: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultPenjualanForm = {
|
||||||
|
produkId: "",
|
||||||
|
jumlah: 0,
|
||||||
|
hargaSatuan: 0,
|
||||||
|
tanggal: new Date().toISOString().split('T')[0],
|
||||||
|
isActive: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const umkmState = proxy({
|
||||||
|
// UMKM Module
|
||||||
|
umkm: {
|
||||||
|
findMany: {
|
||||||
|
data: [] as any[],
|
||||||
|
page: 1,
|
||||||
|
totalPages: 1,
|
||||||
|
loading: false,
|
||||||
|
search: "",
|
||||||
|
async load(page = 1, limit = 10, search = "", kategoriId = "") {
|
||||||
|
this.loading = true;
|
||||||
|
this.page = page;
|
||||||
|
this.search = search;
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
page: page.toString(),
|
||||||
|
limit: limit.toString(),
|
||||||
|
search,
|
||||||
|
kategoriId
|
||||||
|
});
|
||||||
|
const res = await fetch(`/api/ekonomi/umkm/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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
form: { ...defaultUmkmForm },
|
||||||
|
loading: false,
|
||||||
|
async submit() {
|
||||||
|
const cek = umkmFormSchema.safeParse(this.form);
|
||||||
|
if (!cek.success) return toast.error("Cek kembali form anda");
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/ekonomi/umkm/create", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(this.form)
|
||||||
|
});
|
||||||
|
const result = await res.json();
|
||||||
|
if (result.success) {
|
||||||
|
toast.success("UMKM berhasil dibuat");
|
||||||
|
umkmState.umkm.findMany.load();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
toast.error(result.message);
|
||||||
|
} catch (e) {
|
||||||
|
toast.error("Gagal membuat UMKM");
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Produk Module
|
||||||
|
produk: {
|
||||||
|
findMany: {
|
||||||
|
data: [] as any[],
|
||||||
|
page: 1,
|
||||||
|
totalPages: 1,
|
||||||
|
loading: false,
|
||||||
|
async load(page = 1, limit = 10, search = "", umkmId = "") {
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams({ page: page.toString(), limit: limit.toString(), search, umkmId });
|
||||||
|
const res = await fetch(`/api/ekonomi/umkm/produk/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; }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
form: { ...defaultProdukForm },
|
||||||
|
loading: false,
|
||||||
|
async submit() {
|
||||||
|
const cek = produkFormSchema.safeParse(this.form);
|
||||||
|
if (!cek.success) return toast.error("Cek kembali form anda");
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/ekonomi/umkm/produk/create", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(this.form)
|
||||||
|
});
|
||||||
|
const result = await res.json();
|
||||||
|
if (result.success) {
|
||||||
|
toast.success("Produk berhasil dibuat");
|
||||||
|
umkmState.produk.findMany.load();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (e) { toast.error("Gagal membuat produk"); } finally { this.loading = false; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Penjualan Module
|
||||||
|
penjualan: {
|
||||||
|
findMany: {
|
||||||
|
data: [] as any[],
|
||||||
|
page: 1,
|
||||||
|
totalPages: 1,
|
||||||
|
loading: false,
|
||||||
|
async load(page = 1, limit = 10, produkId = "", periode = "") {
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams({ page: page.toString(), limit: limit.toString(), produkId, periode });
|
||||||
|
const res = await fetch(`/api/ekonomi/umkm/penjualan/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; }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
form: { ...defaultPenjualanForm },
|
||||||
|
loading: false,
|
||||||
|
async submit() {
|
||||||
|
const cek = penjualanFormSchema.safeParse(this.form);
|
||||||
|
if (!cek.success) return toast.error("Cek kembali form anda");
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/ekonomi/umkm/penjualan/create", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(this.form)
|
||||||
|
});
|
||||||
|
const result = await res.json();
|
||||||
|
if (result.success) {
|
||||||
|
toast.success("Penjualan berhasil dicatat");
|
||||||
|
umkmState.penjualan.findMany.load();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (e) { toast.error("Gagal mencatat penjualan"); } finally { this.loading = false; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Dashboard Module
|
||||||
|
dashboard: {
|
||||||
|
kpi: { data: null as any, loading: false },
|
||||||
|
summary: { data: null as any, loading: false },
|
||||||
|
topProduk: { data: [] as any[], loading: false },
|
||||||
|
detail: { data: [] as any[], loading: false },
|
||||||
|
async loadAll(periode = "") {
|
||||||
|
const p = periode || `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`;
|
||||||
|
this.kpi.loading = true;
|
||||||
|
this.summary.loading = true;
|
||||||
|
this.topProduk.loading = true;
|
||||||
|
this.detail.loading = true;
|
||||||
|
try {
|
||||||
|
const [kpi, sum, top, det] = await Promise.all([
|
||||||
|
fetch(`/api/ekonomi/umkm/dashboard/kpi?periode=${p}`).then(r => r.json()),
|
||||||
|
fetch(`/api/ekonomi/umkm/dashboard/ringkasan-penjualan?periode=${p}`).then(r => r.json()),
|
||||||
|
fetch(`/api/ekonomi/umkm/dashboard/top-produk?periode=${p}`).then(r => r.json()),
|
||||||
|
fetch(`/api/ekonomi/umkm/dashboard/detail-penjualan?periode=${p}`).then(r => r.json())
|
||||||
|
]);
|
||||||
|
if (kpi.success) this.kpi.data = kpi.data;
|
||||||
|
if (sum.success) this.summary.data = sum.data;
|
||||||
|
if (top.success) this.topProduk.data = top.data;
|
||||||
|
if (det.success) this.detail.data = det.data;
|
||||||
|
} catch (e) { console.error(e); } finally {
|
||||||
|
this.kpi.loading = false;
|
||||||
|
this.summary.loading = false;
|
||||||
|
this.topProduk.loading = false;
|
||||||
|
this.detail.loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default umkmState;
|
||||||
168
src/app/admin/(dashboard)/ekonomi/umkm/_lib/layoutTabs.tsx
Normal file
168
src/app/admin/(dashboard)/ekonomi/umkm/_lib/layoutTabs.tsx
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
'use client'
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
ScrollArea,
|
||||||
|
Stack,
|
||||||
|
Tabs,
|
||||||
|
TabsList,
|
||||||
|
TabsPanel,
|
||||||
|
TabsTab,
|
||||||
|
Title
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { IconDashboard, IconBuildingStore, IconPackage, IconShoppingCart } from '@tabler/icons-react';
|
||||||
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
function LayoutTabs({ children }: { children: React.ReactNode }) {
|
||||||
|
const router = useRouter();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
label: "Dashboard",
|
||||||
|
value: "dashboard",
|
||||||
|
href: "/admin/ekonomi/umkm/dashboard",
|
||||||
|
icon: <IconDashboard size={18} stroke={1.8} />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Data UMKM",
|
||||||
|
value: "data-umkm",
|
||||||
|
href: "/admin/ekonomi/umkm/data-umkm",
|
||||||
|
icon: <IconBuildingStore size={18} stroke={1.8} />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Produk",
|
||||||
|
value: "produk",
|
||||||
|
href: "/admin/ekonomi/umkm/produk",
|
||||||
|
icon: <IconPackage size={18} stroke={1.8} />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Penjualan",
|
||||||
|
value: "penjualan",
|
||||||
|
href: "/admin/ekonomi/umkm/penjualan",
|
||||||
|
icon: <IconShoppingCart size={18} stroke={1.8} />
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const currentTab = tabs.find((tab) => pathname.startsWith(tab.href));
|
||||||
|
const [activeTab, setActiveTab] = useState<string | null>(
|
||||||
|
currentTab?.value || tabs[0].value
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleTabChange = (value: string | null) => {
|
||||||
|
const tab = tabs.find((t) => t.value === value);
|
||||||
|
if (tab) {
|
||||||
|
router.push(tab.href);
|
||||||
|
}
|
||||||
|
setActiveTab(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const match = tabs.find((tab) => pathname.startsWith(tab.href));
|
||||||
|
if (match) {
|
||||||
|
setActiveTab(match.value);
|
||||||
|
}
|
||||||
|
}, [pathname]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="lg">
|
||||||
|
<Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
|
||||||
|
Manajemen UMKM
|
||||||
|
</Title>
|
||||||
|
|
||||||
|
<Tabs
|
||||||
|
color={colors['blue-button']}
|
||||||
|
variant="pills"
|
||||||
|
value={activeTab}
|
||||||
|
onChange={handleTabChange}
|
||||||
|
radius="lg"
|
||||||
|
keepMounted={false}
|
||||||
|
>
|
||||||
|
<Box visibleFrom='md' pb={10}>
|
||||||
|
<ScrollArea type="auto" offsetScrollbars>
|
||||||
|
<TabsList
|
||||||
|
p="sm"
|
||||||
|
style={{
|
||||||
|
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||||
|
borderRadius: "1rem",
|
||||||
|
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||||
|
display: "flex",
|
||||||
|
flexWrap: "nowrap",
|
||||||
|
gap: "0.5rem",
|
||||||
|
paddingInline: "0.5rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tabs.map((tab, i) => (
|
||||||
|
<TabsTab
|
||||||
|
key={i}
|
||||||
|
value={tab.value}
|
||||||
|
leftSection={tab.icon}
|
||||||
|
style={{
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: "0.9rem",
|
||||||
|
transition: "all 0.2s ease",
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</TabsTab>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
</ScrollArea>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box hiddenFrom='md' pb={10}>
|
||||||
|
<ScrollArea type="auto" offsetScrollbars={false} w="100%">
|
||||||
|
<TabsList
|
||||||
|
p="xs"
|
||||||
|
style={{
|
||||||
|
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||||
|
borderRadius: "1rem",
|
||||||
|
display: "flex",
|
||||||
|
flexWrap: "nowrap",
|
||||||
|
gap: "0.5rem",
|
||||||
|
width: "max-content",
|
||||||
|
maxWidth: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tabs.map((tab, i) => (
|
||||||
|
<TabsTab
|
||||||
|
key={i}
|
||||||
|
value={tab.value}
|
||||||
|
leftSection={tab.icon}
|
||||||
|
style={{
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: "0.9rem",
|
||||||
|
paddingInline: "0.75rem",
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</TabsTab>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
</ScrollArea>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{tabs.map((tab, i) => (
|
||||||
|
<TabsPanel
|
||||||
|
key={i}
|
||||||
|
value={tab.value}
|
||||||
|
style={{
|
||||||
|
padding: "1.5rem",
|
||||||
|
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
|
||||||
|
borderRadius: "1rem",
|
||||||
|
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</TabsPanel>
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LayoutTabs;
|
||||||
140
src/app/admin/(dashboard)/ekonomi/umkm/dashboard/page.tsx
Normal file
140
src/app/admin/(dashboard)/ekonomi/umkm/dashboard/page.tsx
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
'use client'
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Card,
|
||||||
|
Grid,
|
||||||
|
Group,
|
||||||
|
SimpleGrid,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
TableTbody,
|
||||||
|
TableTd,
|
||||||
|
TableTh,
|
||||||
|
TableThead,
|
||||||
|
TableTr,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
Badge
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
|
import { IconArrowUpRight, IconArrowDownRight, IconMinus } from '@tabler/icons-react';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
import umkmState from '../../../_state/ekonomi/umkm/umkm';
|
||||||
|
|
||||||
|
function UmkmDashboard() {
|
||||||
|
const state = useProxy(umkmState.dashboard);
|
||||||
|
|
||||||
|
useShallowEffect(() => {
|
||||||
|
state.loadAll();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (state.kpi.loading || !state.kpi.data) {
|
||||||
|
return <Skeleton height={400} radius="md" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const kpi = state.kpi.data;
|
||||||
|
const summary = state.summary.data;
|
||||||
|
const topProduk = state.topProduk.data;
|
||||||
|
const detail = state.detail.data;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="lg">
|
||||||
|
<SimpleGrid cols={{ base: 1, sm: 2, md: 4 }}>
|
||||||
|
<KpiCard title="UMKM Aktif" value={kpi.umkmAktif} subValue={`Total: ${kpi.totalUmkm}`} />
|
||||||
|
<KpiCard
|
||||||
|
title="Omzet Bulan Ini"
|
||||||
|
value={`Rp ${kpi.omzetBulanan.toLocaleString()}`}
|
||||||
|
trend={summary?.persentasePerubahan}
|
||||||
|
/>
|
||||||
|
<KpiCard title="Produk Aktif" value={summary?.produkAktif || 0} />
|
||||||
|
<KpiCard title="Kategori Populer" value={kpi.kategoriTerbanyak} />
|
||||||
|
</SimpleGrid>
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={{ base: 12, md: 4 }}>
|
||||||
|
<Card withBorder radius="md" p="lg" shadow="sm">
|
||||||
|
<Title order={4} mb="md">Top 3 Produk</Title>
|
||||||
|
<Stack gap="sm">
|
||||||
|
{topProduk.map((item, i) => (
|
||||||
|
<Group key={i} justify="space-between">
|
||||||
|
<Box>
|
||||||
|
<Text fw={500}>{item.namaProduk}</Text>
|
||||||
|
<Text size="xs" c="dimmed">{item.namaUmkm}</Text>
|
||||||
|
</Box>
|
||||||
|
<Text fw={600} c="blue">Rp {item.totalPenjualan.toLocaleString()}</Text>
|
||||||
|
</Group>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={{ base: 12, md: 8 }}>
|
||||||
|
<Card withBorder radius="md" p="lg" shadow="sm">
|
||||||
|
<Title order={4} mb="md">Detail Penjualan & Stok</Title>
|
||||||
|
<Box style={{ overflowX: 'auto' }}>
|
||||||
|
<Table highlightOnHover>
|
||||||
|
<TableThead>
|
||||||
|
<TableTr>
|
||||||
|
<TableTh>Produk</TableTh>
|
||||||
|
<TableTh>Penjualan</TableTh>
|
||||||
|
<TableTh>Trend</TableTh>
|
||||||
|
<TableTh>Stok</TableTh>
|
||||||
|
<TableTh>Status</TableTh>
|
||||||
|
</TableTr>
|
||||||
|
</TableThead>
|
||||||
|
<TableTbody>
|
||||||
|
{detail.map((item, i) => (
|
||||||
|
<TableTr key={i}>
|
||||||
|
<TableTd>{item.namaProduk}</TableTd>
|
||||||
|
<TableTd>Rp {item.penjualanBulanIni.toLocaleString()}</TableTd>
|
||||||
|
<TableTd>{renderTrend(item.trend)}</TableTd>
|
||||||
|
<TableTd>{item.stok}</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Badge color={getStatusColor(item.statusStok)}>
|
||||||
|
{item.statusStok}
|
||||||
|
</Badge>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
))}
|
||||||
|
</TableTbody>
|
||||||
|
</Table>
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function KpiCard({ title, value, subValue, trend }: any) {
|
||||||
|
return (
|
||||||
|
<Card withBorder radius="md" p="lg" shadow="sm">
|
||||||
|
<Text size="xs" c="dimmed" fw={700} tt="uppercase">{title}</Text>
|
||||||
|
<Group align="flex-end" gap="xs" mt="sm">
|
||||||
|
<Text fz="xl" fw={700} lh={1}>{value}</Text>
|
||||||
|
{trend !== undefined && (
|
||||||
|
<Text c={trend >= 0 ? 'teal' : 'red'} fz="sm" fw={500}>
|
||||||
|
{trend >= 0 ? '+' : ''}{trend}%
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
{subValue && <Text size="xs" c="dimmed" mt={4}>{subValue}</Text>}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTrend(trend: string) {
|
||||||
|
if (trend === 'up') return <IconArrowUpRight size={18} color="green" />;
|
||||||
|
if (trend === 'down') return <IconArrowDownRight size={18} color="red" />;
|
||||||
|
return <IconMinus size={18} color="gray" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusColor(status: string) {
|
||||||
|
if (status === 'Aman') return 'green';
|
||||||
|
if (status === 'Menipis') return 'yellow';
|
||||||
|
return 'red';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UmkmDashboard;
|
||||||
105
src/app/admin/(dashboard)/ekonomi/umkm/data-umkm/page.tsx
Normal file
105
src/app/admin/(dashboard)/ekonomi/umkm/data-umkm/page.tsx
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
'use client'
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Group,
|
||||||
|
Pagination,
|
||||||
|
Paper,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
TableTbody,
|
||||||
|
TableTd,
|
||||||
|
TableTh,
|
||||||
|
TableThead,
|
||||||
|
TableTr,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
TextInput
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||||
|
import { IconPlus, IconSearch, IconEdit, IconTrash } from '@tabler/icons-react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
import umkmState from '../../../_state/ekonomi/umkm/umkm';
|
||||||
|
|
||||||
|
function DataUmkm() {
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
const state = useProxy(umkmState.umkm.findMany);
|
||||||
|
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||||
|
|
||||||
|
useShallowEffect(() => {
|
||||||
|
state.load(state.page, 10, debouncedSearch);
|
||||||
|
}, [state.page, debouncedSearch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="lg">
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Title order={3}>Data UMKM</Title>
|
||||||
|
<Button leftSection={<IconPlus size={18} />} color="blue">
|
||||||
|
Tambah UMKM
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Paper withBorder p="md" radius="md">
|
||||||
|
<TextInput
|
||||||
|
placeholder="Cari UMKM atau Pemilik..."
|
||||||
|
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 UMKM</TableTh>
|
||||||
|
<TableTh>Pemilik</TableTh>
|
||||||
|
<TableTh>Kategori</TableTh>
|
||||||
|
<TableTh>Kontak</TableTh>
|
||||||
|
<TableTh>Aksi</TableTh>
|
||||||
|
</TableTr>
|
||||||
|
</TableThead>
|
||||||
|
<TableTbody>
|
||||||
|
{state.data.map((item) => (
|
||||||
|
<TableTr key={item.id}>
|
||||||
|
<TableTd fw={500}>{item.nama}</TableTd>
|
||||||
|
<TableTd>{item.pemilik}</TableTd>
|
||||||
|
<TableTd>{item.kategori?.nama || '-'}</TableTd>
|
||||||
|
<TableTd>{item.kontak || '-'}</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Group gap="xs">
|
||||||
|
<Button variant="subtle" color="blue" size="xs">
|
||||||
|
<IconEdit size={16} />
|
||||||
|
</Button>
|
||||||
|
<Button variant="subtle" color="red" size="xs">
|
||||||
|
<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>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DataUmkm;
|
||||||
28
src/app/admin/(dashboard)/ekonomi/umkm/layout.tsx
Normal file
28
src/app/admin/(dashboard)/ekonomi/umkm/layout.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
import LayoutTabs from "./_lib/layoutTabs"
|
||||||
|
import { Box } from "@mantine/core";
|
||||||
|
|
||||||
|
|
||||||
|
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
const segments = pathname.split('/').filter(Boolean);
|
||||||
|
// Path /admin/ekonomi/umkm/dashboard -> length 4
|
||||||
|
// Path detail usually adds an ID -> length >= 5
|
||||||
|
const isDetailPage = segments.length >= 5;
|
||||||
|
|
||||||
|
if (isDetailPage) {
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<LayoutTabs>
|
||||||
|
{children}
|
||||||
|
</LayoutTabs>
|
||||||
|
)
|
||||||
|
}
|
||||||
89
src/app/admin/(dashboard)/ekonomi/umkm/penjualan/page.tsx
Normal file
89
src/app/admin/(dashboard)/ekonomi/umkm/penjualan/page.tsx
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
'use client'
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Group,
|
||||||
|
Pagination,
|
||||||
|
Paper,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
TableTbody,
|
||||||
|
TableTd,
|
||||||
|
TableTh,
|
||||||
|
TableThead,
|
||||||
|
TableTr,
|
||||||
|
Text,
|
||||||
|
Title
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
|
import { IconPlus, IconTrash } from '@tabler/icons-react';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
import umkmState from '../../../_state/ekonomi/umkm/umkm';
|
||||||
|
|
||||||
|
function PenjualanUmkm() {
|
||||||
|
const state = useProxy(umkmState.penjualan.findMany);
|
||||||
|
|
||||||
|
useShallowEffect(() => {
|
||||||
|
state.load(state.page, 10);
|
||||||
|
}, [state.page]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="lg">
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Title order={3}>Histori Penjualan UMKM</Title>
|
||||||
|
<Button leftSection={<IconPlus size={18} />} color="blue">
|
||||||
|
Catat Penjualan
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Paper withBorder p="md" radius="md">
|
||||||
|
{state.loading ? (
|
||||||
|
<Skeleton height={400} />
|
||||||
|
) : (
|
||||||
|
<Box style={{ overflowX: 'auto' }}>
|
||||||
|
<Table highlightOnHover>
|
||||||
|
<TableThead>
|
||||||
|
<TableTr>
|
||||||
|
<TableTh>Tanggal</TableTh>
|
||||||
|
<TableTh>Produk</TableTh>
|
||||||
|
<TableTh>UMKM</TableTh>
|
||||||
|
<TableTh>Jumlah</TableTh>
|
||||||
|
<TableTh>Total Nilai</TableTh>
|
||||||
|
<TableTh>Aksi</TableTh>
|
||||||
|
</TableTr>
|
||||||
|
</TableThead>
|
||||||
|
<TableTbody>
|
||||||
|
{state.data.map((item) => (
|
||||||
|
<TableTr key={item.id}>
|
||||||
|
<TableTd>{new Date(item.tanggal).toLocaleDateString('id-ID')}</TableTd>
|
||||||
|
<TableTd fw={500}>{item.produk?.nama}</TableTd>
|
||||||
|
<TableTd>{item.produk?.umkm?.nama}</TableTd>
|
||||||
|
<TableTd>{item.jumlah}</TableTd>
|
||||||
|
<TableTd fw={600}>Rp {item.totalNilai.toLocaleString()}</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Button variant="subtle" color="red" size="xs">
|
||||||
|
<IconTrash size={16} />
|
||||||
|
</Button>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
))}
|
||||||
|
</TableTbody>
|
||||||
|
</Table>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Center mt="md">
|
||||||
|
<Pagination
|
||||||
|
total={state.totalPages}
|
||||||
|
value={state.page}
|
||||||
|
onChange={(p) => state.load(p, 10)}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Paper>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PenjualanUmkm;
|
||||||
111
src/app/admin/(dashboard)/ekonomi/umkm/produk/page.tsx
Normal file
111
src/app/admin/(dashboard)/ekonomi/umkm/produk/page.tsx
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
'use client'
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Group,
|
||||||
|
Pagination,
|
||||||
|
Paper,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
TableTbody,
|
||||||
|
TableTd,
|
||||||
|
TableTh,
|
||||||
|
TableThead,
|
||||||
|
TableTr,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
TextInput,
|
||||||
|
Badge
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||||
|
import { IconPlus, IconSearch, IconEdit, IconTrash } from '@tabler/icons-react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
import umkmState from '../../../_state/ekonomi/umkm/umkm';
|
||||||
|
|
||||||
|
function ProdukUmkm() {
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
const state = useProxy(umkmState.produk.findMany);
|
||||||
|
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||||
|
|
||||||
|
useShallowEffect(() => {
|
||||||
|
state.load(state.page, 10, debouncedSearch);
|
||||||
|
}, [state.page, debouncedSearch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="lg">
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Title order={3}>Daftar Produk UMKM</Title>
|
||||||
|
<Button leftSection={<IconPlus size={18} />} color="blue">
|
||||||
|
Tambah Produk
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Paper withBorder p="md" radius="md">
|
||||||
|
<TextInput
|
||||||
|
placeholder="Cari nama produk..."
|
||||||
|
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 Produk</TableTh>
|
||||||
|
<TableTh>UMKM</TableTh>
|
||||||
|
<TableTh>Harga</TableTh>
|
||||||
|
<TableTh>Stok</TableTh>
|
||||||
|
<TableTh>Status Stok</TableTh>
|
||||||
|
<TableTh>Aksi</TableTh>
|
||||||
|
</TableTr>
|
||||||
|
</TableThead>
|
||||||
|
<TableTbody>
|
||||||
|
{state.data.map((item) => (
|
||||||
|
<TableTr key={item.id}>
|
||||||
|
<TableTd fw={500}>{item.nama}</TableTd>
|
||||||
|
<TableTd>{item.umkm?.nama || '-'}</TableTd>
|
||||||
|
<TableTd>Rp {item.harga.toLocaleString()}</TableTd>
|
||||||
|
<TableTd>{item.stok}</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Badge color={item.stok < 5 ? 'red' : item.stok < 20 ? 'yellow' : 'green'}>
|
||||||
|
{item.stok < 5 ? 'Rendah' : item.stok < 20 ? 'Menipis' : 'Aman'}
|
||||||
|
</Badge>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Group gap="xs">
|
||||||
|
<Button variant="subtle" color="blue" size="xs">
|
||||||
|
<IconEdit size={16} />
|
||||||
|
</Button>
|
||||||
|
<Button variant="subtle" color="red" size="xs">
|
||||||
|
<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>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProdukUmkm;
|
||||||
@@ -206,6 +206,26 @@ export const devBar = [
|
|||||||
name: "Ekonomi",
|
name: "Ekonomi",
|
||||||
path: "",
|
path: "",
|
||||||
children: [
|
children: [
|
||||||
|
{
|
||||||
|
id: "Ekonomi_UMKM_1",
|
||||||
|
name: "UMKM - Dashboard",
|
||||||
|
path: "/admin/ekonomi/umkm/dashboard"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Ekonomi_UMKM_2",
|
||||||
|
name: "UMKM - Data UMKM",
|
||||||
|
path: "/admin/ekonomi/umkm/data-umkm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Ekonomi_UMKM_3",
|
||||||
|
name: "UMKM - Produk",
|
||||||
|
path: "/admin/ekonomi/umkm/produk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Ekonomi_UMKM_4",
|
||||||
|
name: "UMKM - Penjualan",
|
||||||
|
path: "/admin/ekonomi/umkm/penjualan"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "Ekonomi_1",
|
id: "Ekonomi_1",
|
||||||
name: "Pasar Desa",
|
name: "Pasar Desa",
|
||||||
@@ -637,6 +657,26 @@ export const navBar = [
|
|||||||
name: "Ekonomi",
|
name: "Ekonomi",
|
||||||
path: "",
|
path: "",
|
||||||
children: [
|
children: [
|
||||||
|
{
|
||||||
|
id: "Ekonomi_UMKM_1",
|
||||||
|
name: "UMKM - Dashboard",
|
||||||
|
path: "/admin/ekonomi/umkm/dashboard"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Ekonomi_UMKM_2",
|
||||||
|
name: "UMKM - Data UMKM",
|
||||||
|
path: "/admin/ekonomi/umkm/data-umkm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Ekonomi_UMKM_3",
|
||||||
|
name: "UMKM - Produk",
|
||||||
|
path: "/admin/ekonomi/umkm/produk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Ekonomi_UMKM_4",
|
||||||
|
name: "UMKM - Penjualan",
|
||||||
|
path: "/admin/ekonomi/umkm/penjualan"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "Ekonomi_1",
|
id: "Ekonomi_1",
|
||||||
name: "Pasar Desa",
|
name: "Pasar Desa",
|
||||||
@@ -1026,6 +1066,26 @@ export const role1 = [
|
|||||||
name: "Ekonomi",
|
name: "Ekonomi",
|
||||||
path: "",
|
path: "",
|
||||||
children: [
|
children: [
|
||||||
|
{
|
||||||
|
id: "Ekonomi_UMKM_1",
|
||||||
|
name: "UMKM - Dashboard",
|
||||||
|
path: "/admin/ekonomi/umkm/dashboard"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Ekonomi_UMKM_2",
|
||||||
|
name: "UMKM - Data UMKM",
|
||||||
|
path: "/admin/ekonomi/umkm/data-umkm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Ekonomi_UMKM_3",
|
||||||
|
name: "UMKM - Produk",
|
||||||
|
path: "/admin/ekonomi/umkm/produk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Ekonomi_UMKM_4",
|
||||||
|
name: "UMKM - Penjualan",
|
||||||
|
path: "/admin/ekonomi/umkm/penjualan"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "Ekonomi_1",
|
id: "Ekonomi_1",
|
||||||
name: "Pasar Desa",
|
name: "Pasar Desa",
|
||||||
|
|||||||
Reference in New Issue
Block a user