refactor(ekonomi): consolidate Pasar Desa into UMKM module

- Remove "Pasar Desa" as a separate entity; products are now strictly linked to UMKM.
- Delete redundant Pasar Desa API endpoints and state management.
- Update Admin UI: remove "Pasar Desa" menu and unified product management under UMKM.
- Update Public UI: replace "Pasar Desa" with "UMKM" in navbar and unified hub at /darmasaba/ekonomi/umkm.
- Implement mandatory umkmId in PasarDesa model and update seeders accordingly.
- Fix UI bugs, missing imports, and invalid API filters for mandatory umkmId.
- Increment version to 0.1.18.
This commit is contained in:
2026-04-21 17:52:08 +08:00
parent e286cb4f2b
commit 1a48c15c87
45 changed files with 341 additions and 3409 deletions

View File

@@ -1,8 +1,6 @@
import Elysia from "elysia";
import PasarDesa from "./pasar-desa";
import LowonganKerja from "./lowongan-kerja";
import ProgramKemiskinan from "./program-kemiskinan";
import KategoriProduk from "./pasar-desa/kategori-produk";
import GrafikUsiaKerjaYangMenganggur from "./usia-kerja-yang-menganggur";
import GrafikMenganggurBerdasarkanPendidikan from "./usia-kerja-yang-menganggur/pengangguran-berdasrkan-pendidikan";
import JumlahPendudukMiskin from "./jumlah-penduduk-miskin";
@@ -20,8 +18,6 @@ const Ekonomi = new Elysia({
prefix: "/ekonomi",
tags: ["Ekonomi"],
})
.use(PasarDesa)
.use(KategoriProduk)
.use(LowonganKerja)
.use(ProgramKemiskinan)
.use(StrukturOrganisasi)

View File

@@ -1,73 +0,0 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type FormCreate = {
nama: string;
harga: number;
alamatUsaha: string;
imageId: string;
rating: number;
kategoriId: string[];
kontak: string;
deskripsi: string;
// Array of KategoriProduk IDs
};
export default async function pasarDesaCreate(context: Context) {
const body = context.body as FormCreate;
if (!body.kategoriId || body.kategoriId.length === 0) {
throw new Error("At least one kategoriId is required");
}
try {
// Start a transaction to ensure data consistency
const result = await prisma.$transaction(async (prisma) => {
// 1. Create PasarDesa with the first kategoriId as the main category
const pasarDesa = await prisma.pasarDesa.create({
data: {
nama: body.nama,
harga: Number(body.harga),
alamatUsaha: body.alamatUsaha,
imageId: body.imageId,
rating: Number(body.rating),
kategoriProdukId: body.kategoriId[0],
kontak: body.kontak,
deskripsi: body.deskripsi,
// Use the first category as the main one
},
});
// 2. Create category relationships in KategoriToPasar for all categories
await prisma.kategoriToPasar.createMany({
data: body.kategoriId.map((kategoriId) => ({
pasarDesaId: pasarDesa.id,
kategoriId: kategoriId, // Note: The field is 'kategoriId' in the schema, not 'kategoriProdukId'
})),
});
// 3. Get the complete data with relationships
return await prisma.pasarDesa.findUnique({
where: { id: pasarDesa.id },
include: {
image: true,
kategoriProduk: true,
KategoriToPasar: {
include: {
kategori: true,
},
},
},
});
});
return {
success: true,
message: "Sukses menambahkan pasar desa",
data: result,
};
} catch (error) {
console.error("Error creating PasarDesa:", error);
throw new Error("Failed to create PasarDesa: " + (error as Error).message);
}
}

View File

@@ -1,27 +0,0 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function pasarDesaDelete(context: Context) {
const { params } = context;
const id = params?.id as string;
if (!id) {
throw new Error("ID tidak ditemukan dalam parameter");
}
// 1. Hapus relasi dari pivot
await prisma.kategoriToPasar.deleteMany({
where: { pasarDesaId: id },
});
// 2. Hapus pasar desa utama
const deleted = await prisma.pasarDesa.delete({
where: { id },
});
return {
success: true,
message: "Berhasil menghapus pasar desa",
data: deleted,
};
}

View File

@@ -1,88 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// /api/berita/findManyPaginated.ts
import prisma from "@/lib/prisma";
import { Context } from "elysia";
async function pasarDesaFindMany(context: Context) {
// Ambil parameter dari query
const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || '';
const categoryId = context.query.categoryId as string | undefined;
const skip = (page - 1) * limit;
// Buat where clause: Tampilkan hanya yang TIDAK punya umkmId (Produk Pasar Murni)
const where: any = {
isActive: true,
deletedAt: null,
umkmId: null
};
// Tambahkan filter kategori (jika ada)
if (categoryId) {
where.KategoriToPasar = {
some: {
kategoriId: categoryId
}
};
}
// Tambahkan pencarian (jika ada)
if (search) {
where.AND = where.AND || [];
where.AND.push({
OR: [
{ nama: { contains: search, mode: 'insensitive' } },
{ alamatUsaha: { contains: search, mode: 'insensitive' } },
{
KategoriToPasar: {
some: {
kategori: {
nama: { contains: search, mode: 'insensitive' }
}
}
}
}
]
});
}
try {
// Ambil data dan total count secara paralel
const [data, total] = await Promise.all([
prisma.pasarDesa.findMany({
where,
include: {
image: true,
KategoriToPasar: {
include: {
kategori: true
}
}
},
skip,
take: limit,
orderBy: { createdAt: 'desc' },
}),
prisma.pasarDesa.count({ where }),
]);
return {
success: true,
message: "Berhasil ambil pasar desa dengan pagination (Non-UMKM)",
data,
page,
limit,
total,
totalPages: Math.ceil(total / limit),
};
} catch (e) {
console.error("Error di findMany paginated:", e);
return {
success: false,
message: "Gagal mengambil data pasar desa",
};
}
}
export default pasarDesaFindMany;

View File

@@ -1,33 +0,0 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function pasarDesaFindUnique(context: Context) {
const { params } = context;
const id = params?.id as string;
if (!id) {
throw new Error("ID tidak ditemukan dalam parameter");
}
const data = await prisma.pasarDesa.findUnique({
where: { id },
include: {
image: true,
KategoriToPasar: {
include: {
kategori: true,
},
},
},
});
if (!data) {
throw new Error("Pasar desa tidak ditemukan");
}
return {
success: true,
message: "Data pasar desa ditemukan",
data,
};
}

View File

@@ -1,90 +0,0 @@
import Elysia, { t } from "elysia";
import pasarDesaCreate from "./create";
import pasarDesaDelete from "./del";
import pasarDesaFindMany from "./findMany";
import pasarDesaUpdate from "./updt";
import pasarDesaFindUnique from "./findUnique";
const PasarDesa = new Elysia({
prefix: "/pasardesa",
tags: ["Ekonomi/Pasar Desa"],
})
// GET all
.get("/find-many", pasarDesaFindMany)
// GET by ID
.get(
"/:id",
async (context) => {
return await pasarDesaFindUnique(context);
},
{
params: t.Object({
id: t.String(),
}),
}
)
// POST create
.post(
"/create",
pasarDesaCreate,
{
body: t.Object({
nama: t.String(),
harga: t.Number(),
alamatUsaha: t.String(),
imageId: t.String(),
rating: t.Number(),
kategoriId: t.Array(t.String()),
kontak: t.String(),
deskripsi: t.String(),
}),
}
)
// DELETE
.delete(
"/del/:id",
pasarDesaDelete,
{
params: t.Object({
id: t.String(),
}),
}
)
// PUT update
.put(
"/:id",
async (context) => {
const body = context.body;
const id = context.params.id;
// Gabungkan id ke body
return await pasarDesaUpdate({
...context,
body: {
...body,
id,
},
});
},
{
params: t.Object({
id: t.String(),
}),
body: t.Object({
nama: t.String(),
harga: t.Number(),
alamatUsaha: t.String(),
imageId: t.String(),
rating: t.Number(),
kategoriId: t.Array(t.String()),
kontak: t.String(),
deskripsi: t.String(),
}),
}
);
export default PasarDesa;

View File

@@ -1,25 +0,0 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function kategoriProdukCreate(context: Context) {
const body = context.body as {nama: string};
if (!body.nama) {
return {
success: false,
message: "Nama is required",
};
}
const kategoriProduk = await prisma.kategoriProduk.create({
data: {
nama: body.nama,
},
});
return {
success: true,
message: "Sukses menambahkan kategori produk",
data: kategoriProduk
};
}

View File

@@ -1,33 +0,0 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
const kategoriProdukDelete = async (context: Context) => {
const id = context.params.id;
if (!id) {
return {
success: false,
message: "ID is required",
}
}
const kategoriProduk = await prisma.kategoriProduk.delete({
where: {
id: id,
},
})
if(!kategoriProduk) {
return {
success: false,
message: "Kategori Produk tidak ditemukan",
}
}
return {
success: true,
message: "Sukses Menghapus kategori produk",
data: kategoriProduk,
}
}
export default kategoriProdukDelete

View File

@@ -1,60 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// /api/berita/findManyPaginated.ts
import prisma from "@/lib/prisma";
import { Context } from "elysia";
async function kategoriProdukFindMany(context: Context) {
// Ambil parameter dari query
const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || '';
const skip = (page - 1) * limit;
// Buat where clause
const where: any = { isActive: true };
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ nama: { contains: search, mode: 'insensitive' } },
{KategoriToPasar : {
some: {
kategori: {
nama: { contains: search, mode: 'insensitive' }
}
}
}}
];
}
try {
// Ambil data dan total count secara paralel
const [data, total] = await Promise.all([
prisma.kategoriProduk.findMany({
where,
skip,
take: limit,
orderBy: { createdAt: 'desc' },
}),
prisma.kategoriProduk.count({ where }),
]);
return {
success: true,
message: "Berhasil ambil kategori produk dengan pagination",
data,
page,
limit,
total,
totalPages: Math.ceil(total / limit),
};
} catch (e) {
console.error("Error di findMany paginated:", e);
return {
success: false,
message: "Gagal mengambil data kategori produk",
};
}
}
export default kategoriProdukFindMany;

View File

@@ -1,49 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// /api/berita/findManyAll.ts
import prisma from "@/lib/prisma";
import { Context } from "elysia";
async function kategoriProdukFindManyAll(context: Context) {
// Ambil query search (opsional)
const search = (context.query.search as string) || "";
// Buat where clause
const where: any = { isActive: true };
if (search) {
where.OR = [
{ nama: { contains: search, mode: "insensitive" } },
{
KategoriToPasar: {
some: {
kategori: {
nama: { contains: search, mode: "insensitive" },
},
},
},
},
];
}
try {
const data = await prisma.kategoriProduk.findMany({
where,
orderBy: { createdAt: "desc" },
});
return {
success: true,
message: "Berhasil ambil semua kategori produk",
data,
total: data.length,
};
} catch (e) {
console.error("Error di findManyAll:", e);
return {
success: false,
message: "Gagal mengambil data kategori produk",
};
}
}
export default kategoriProdukFindManyAll;

View File

@@ -1,47 +0,0 @@
import { Context } from "elysia";
import prisma from "@/lib/prisma";
export default async function kategoriProdukFindUnique(context: Context) {
const url = new URL(context.request.url);
const pathSegments = url.pathname.split('/');
const id = pathSegments[pathSegments.length - 1];
if (!id) {
return {
success: false,
message: "ID is required",
}
}
try {
if (typeof id !== 'string') {
return {
success: false,
message: "ID is required",
}
}
const data = await prisma.kategoriProduk.findUnique({
where: { id },
});
if (!data) {
return {
success: false,
message: "Kategori makanan tidak ditemukan",
}
}
return {
success: true,
message: "Success find kategori makanan",
data,
}
} catch (error) {
console.error("Find by ID error:", error);
return {
success: false,
message: "Gagal mengambil kategori makanan: " + (error instanceof Error ? error.message : 'Unknown error'),
}
}
}

View File

@@ -1,32 +0,0 @@
import Elysia from "elysia";
import kategoriProdukFindMany from "./findMany";
import kategoriProdukFindUnique from "./findUnique";
import kategoriProdukDelete from "./del";
import kategoriProdukCreate from "./create";
import kategoriProdukUpdate from "./updt";
import { t } from "elysia";
import kategoriProdukFindManyAll from "./findManyAll";
const KategoriProduk = new Elysia({
prefix: "/kategoriproduk",
tags: ["Ekonomi/Kategori Produk"],
})
.get("/find-many", kategoriProdukFindMany)
.get("/find-many-all", kategoriProdukFindManyAll)
.get("/:id", async (context) => {
const response = await kategoriProdukFindUnique(context);
return response;
})
.delete("/del/:id", kategoriProdukDelete)
.post("/create", kategoriProdukCreate, {
body: t.Object({
nama: t.String(),
}),
})
.put("/:id", kategoriProdukUpdate, {
body: t.Object({
nama: t.String(),
}),
});
export default KategoriProduk;

View File

@@ -1,44 +0,0 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function kategoriProdukUpdate(context: Context) {
const body = context.body as { nama: string };
const id = context.params?.id as string;
// Validasi ID dan nama
if (!id) {
return {
success: false,
message: "ID is required",
};
}
if (!body.nama) {
return {
success: false,
message: "Nama is required",
};
}
try {
const kategoriProduk = await prisma.kategoriProduk.update({
where: { id },
data: {
nama: body.nama,
},
});
return {
success: true,
message: "Success update kategori produk",
data: kategoriProduk,
};
} catch (error) {
console.error("Update error:", error);
return {
success: false,
message: "Gagal update kategori produk",
error: error instanceof Error ? error.message : String(error),
};
}
}

View File

@@ -1,72 +0,0 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type FormUpdate = {
id: string;
nama: string;
harga: number;
alamatUsaha: string;
imageId: string;
rating: number;
kategoriId: string[]; // Array of KategoriProduk IDs
kontak: string;
deskripsi: string;
};
export default async function pasarDesaUpdate(context: Context) {
const body = context.body as FormUpdate;
if (!body.id) {
throw new Error("ID pasar desa tidak boleh kosong");
}
if (!body.kategoriId || body.kategoriId.length === 0) {
throw new Error("Minimal 1 kategori harus dipilih");
}
// 1. Update data utama pasar desa
await prisma.pasarDesa.update({
where: { id: body.id },
data: {
nama: body.nama,
harga: Number(body.harga),
alamatUsaha: body.alamatUsaha,
imageId: body.imageId,
rating: Number(body.rating),
kontak: body.kontak,
deskripsi: body.deskripsi
},
});
// 2. Hapus semua relasi kategori lama
await prisma.kategoriToPasar.deleteMany({
where: { pasarDesaId: body.id },
});
// 3. Tambah relasi kategori yang baru
await prisma.kategoriToPasar.createMany({
data: body.kategoriId.map((kategoriProdukId) => ({
pasarDesaId: body.id,
kategoriId: kategoriProdukId,
})),
});
// 4. Ambil data lengkap setelah update
const updated = await prisma.pasarDesa.findUnique({
where: { id: body.id },
include: {
image: true,
KategoriToPasar: {
include: {
kategori: true,
},
},
},
});
return {
success: true,
message: "Success update pasar desa",
data: updated,
};
}

View File

@@ -22,9 +22,9 @@ async function umkmDashboardDetailPenjualan(context: Context) {
where: { periode: periodeLalu, deletedAt: null },
_sum: { totalNilai: true }
}),
// Use PasarDesa with umkmId filter
// Use PasarDesa
prisma.pasarDesa.findMany({
where: { deletedAt: null, umkmId: { not: null } },
where: { deletedAt: null },
select: { id: true, nama: true, stok: true }
})
]);

View File

@@ -20,9 +20,9 @@ async function umkmDashboardRingSummary(context: Context) {
where: { periode: periodeLalu, deletedAt: null },
_sum: { totalNilai: true }
}),
// Count from PasarDesa with umkmId filter
prisma.pasarDesa.count({
where: { isActive: true, deletedAt: null, umkmId: { not: null } }
// Count from PasarDesa
prisma.pasarDesa.count({
where: { isActive: true, deletedAt: null }
}),
prisma.penjualanProduk.count({ where: { periode, deletedAt: null } })
]);

View File

@@ -10,13 +10,11 @@ async function produkUmkmFindMany(context: Context) {
const kategoriId = context.query.kategoriId as string | undefined;
const skip = (page - 1) * limit;
// Filter: ONLY products that belong to an UMKM
const where: any = {
// Filter: ONLY active products
const where: any = {
deletedAt: null,
isActive: true,
umkmId: { not: null }
};
if (umkmId) {
where.umkmId = umkmId;
}

View File

@@ -20,6 +20,7 @@ const ProdukUmkm = new Elysia({
nama: t.String(),
harga: t.Number(),
umkmId: t.String(),
kategoriId: t.String(), // Added validation
stok: t.Optional(t.Number()),
deskripsi: t.Optional(t.String()),
imageId: t.Optional(t.String()),
@@ -34,6 +35,7 @@ const ProdukUmkm = new Elysia({
nama: t.String(),
harga: t.Number(),
umkmId: t.String(),
kategoriId: t.String(), // Added validation
stok: t.Number(),
deskripsi: t.Optional(t.String()),
imageId: t.Optional(t.String()),