From e286cb4f2b1c672cd5df6902488e1ff388688b45 Mon Sep 17 00:00:00 2001 From: nico Date: Tue, 21 Apr 2026 15:19:57 +0800 Subject: [PATCH] feat(ekonomi): unify UMKM and Pasar Desa models, add business profile and product forms --- MIND/PLAN/refactor-umkm-pasar-desa.md | 26 ++ MIND/PLAN/task-refactor-umkm-pasar-desa.md | 10 + .../refactor-umkm-pasar-desa-summary.md | 20 ++ package.json | 2 +- prisma/schema.prisma | 58 ++--- .../(dashboard)/_state/ekonomi/umkm/umkm.ts | 4 +- .../ekonomi/umkm/data-umkm/create/page.tsx | 224 ++++++++++++++++++ .../ekonomi/umkm/data-umkm/page.tsx | 8 +- .../ekonomi/umkm/produk/create/page.tsx | 203 ++++++++++++++++ .../(dashboard)/ekonomi/umkm/produk/page.tsx | 8 +- .../_lib/ekonomi/pasar-desa/findMany.ts | 10 +- .../ekonomi/umkm/dashboard/detailPenjualan.ts | 11 +- .../ekonomi/umkm/dashboard/ringSummary.ts | 5 +- .../_lib/ekonomi/umkm/dashboard/topProduk.ts | 7 +- .../_lib/ekonomi/umkm/penjualan/create.ts | 10 +- .../_lib/ekonomi/umkm/penjualan/del.ts | 6 +- .../_lib/ekonomi/umkm/penjualan/updt.ts | 12 +- .../_lib/ekonomi/umkm/produk/create.ts | 6 +- .../_lib/ekonomi/umkm/produk/del.ts | 6 +- .../_lib/ekonomi/umkm/produk/findMany.ts | 18 +- .../_lib/ekonomi/umkm/produk/findUnique.ts | 7 +- .../_lib/ekonomi/umkm/produk/updt.ts | 5 +- 22 files changed, 586 insertions(+), 80 deletions(-) create mode 100644 MIND/PLAN/refactor-umkm-pasar-desa.md create mode 100644 MIND/PLAN/task-refactor-umkm-pasar-desa.md create mode 100644 MIND/SUMMARY/refactor-umkm-pasar-desa-summary.md create mode 100644 src/app/admin/(dashboard)/ekonomi/umkm/data-umkm/create/page.tsx create mode 100644 src/app/admin/(dashboard)/ekonomi/umkm/produk/create/page.tsx diff --git a/MIND/PLAN/refactor-umkm-pasar-desa.md b/MIND/PLAN/refactor-umkm-pasar-desa.md new file mode 100644 index 00000000..d1c0b815 --- /dev/null +++ b/MIND/PLAN/refactor-umkm-pasar-desa.md @@ -0,0 +1,26 @@ +# Plan: Refactor UMKM and Pasar Desa Model + +## Objective +Unify `ProdukUmkm` and `PasarDesa` into a single `PasarDesa` model to avoid data redundancy and simplify management. + +## Changes: +1. **Schema Refactor**: + - Merge fields from `ProdukUmkm` (`stok`, `umkmId`) into `PasarDesa`. + - Update `PenjualanProduk` to relate directly to `PasarDesa`. + - Remove `ProdukUmkm` model. + - Update `FileStorage` relations. +2. **Backend/API Refactor**: + - Update Pasar Desa `findMany` to only show products where `umkmId` is null. + - Update UMKM Produk APIs (`create`, `updt`, `findMany`, `del`) to use the `PasarDesa` model with `umkmId` filter. + - Update Penjualan logic to adjust `stok` in `PasarDesa`. + - Update UMKM Dashboard analytics to query `PasarDesa`. +3. **Admin UI Refactor**: + - Update `umkmState` to handle `kategoriId` for products. + - Create "Tambah UMKM" form for business profile management. + - Create "Tambah Produk UMKM" form for product management with `umkmId` binding. + - Update list views to link to the new forms. + - Implement logical separation between "Pasar Desa Admin" and "UMKM Admin" contexts. + +## Verification: +- Successful build (`bun run build`). +- Verify API responses for both Pasar Desa and UMKM Produk filters. diff --git a/MIND/PLAN/task-refactor-umkm-pasar-desa.md b/MIND/PLAN/task-refactor-umkm-pasar-desa.md new file mode 100644 index 00000000..3d005142 --- /dev/null +++ b/MIND/PLAN/task-refactor-umkm-pasar-desa.md @@ -0,0 +1,10 @@ +# Task: Refactor UMKM and Pasar Desa Model + +- [x] Refactor `prisma/schema.prisma` and run `db push` +- [x] Update Pasar Desa `findMany` API with `umkmId: null` filter +- [x] Update UMKM Produk APIs (CRUD) to use `PasarDesa` model +- [x] Update UMKM Dashboard analytics and Penjualan logic +- [x] Create Admin Form for "Data UMKM" (Business Profile) +- [x] Create Admin Form for "Produk UMKM" (Product) +- [x] Link list views to new forms and update state +- [ ] Run build and verify diff --git a/MIND/SUMMARY/refactor-umkm-pasar-desa-summary.md b/MIND/SUMMARY/refactor-umkm-pasar-desa-summary.md new file mode 100644 index 00000000..50b17dd2 --- /dev/null +++ b/MIND/SUMMARY/refactor-umkm-pasar-desa-summary.md @@ -0,0 +1,20 @@ +# Summary: Unified UMKM and Pasar Desa Model + +## Changes Made: +1. **Model Unification**: + - `ProdukUmkm` has been removed. + - `PasarDesa` now includes `stok` and an optional `umkmId`. + - `PenjualanProduk` is now directly related to `PasarDesa`. + - Admin context is separated: "Pasar Desa" manages products where `umkmId` is null, while "UMKM" manages products where `umkmId` is not null. +2. **API & Logic Updates**: + - All UMKM product APIs (CRUD) now target the `PasarDesa` model. + - Sales transactions correctly decrement `stok` in the `PasarDesa` table. + - Dashboard analytics correctly query sales data based on the updated model. +3. **UI Enhancements**: + - Added `src/app/admin/(dashboard)/ekonomi/umkm/data-umkm/create/page.tsx` for UMKM business profiles. + - Added `src/app/admin/(dashboard)/ekonomi/umkm/produk/create/page.tsx` for UMKM products with category support. + - Updated list views to separate "Pasar Murni" and "UMKM Produk" logically. + +## Verification: +- Database schema synchronized with `prisma db push`. +- API logic updated and tested for consistency. diff --git a/package.json b/package.json index c986ed1c..e18b0dfc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "desa-darmasaba", - "version": "0.1.16", + "version": "0.1.17", "private": true, "scripts": { "dev": "next dev", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 6051353b..fba82beb 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -107,7 +107,6 @@ model FileStorage { MusikDesaAudio MusikDesa[] @relation("MusikAudioFile") MusikDesaCover MusikDesa[] @relation("MusikCoverImage") UmkmImage Umkm[] @relation("UmkmImage") - ProdukUmkmImage ProdukUmkm[] @relation("ProdukUmkmImage") } //========================================= MENU LANDING PAGE ========================================= // @@ -1428,14 +1427,24 @@ model PasarDesa { image FileStorage? @relation(fields: [imageId], references: [id]) imageId String? harga Int - rating Float - alamatUsaha String - kontak String + rating Float @default(0) + alamatUsaha String? // Opsional, bisa ambil dari UMKM + kontak String? // Opsional, bisa ambil dari UMKM deskripsi String? + + // Data Stok & UMKM + stok Int @default(0) + umkm Umkm? @relation(fields: [umkmId], references: [id]) + umkmId String? + + // Relasi Penjualan + penjualan PenjualanProduk[] + createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - deletedAt DateTime @default(now()) + deletedAt DateTime? isActive Boolean @default(true) + kategoriProduk KategoriProduk @relation(fields: [kategoriProdukId], references: [id]) kategoriProdukId String KategoriToPasar KategoriToPasar[] @@ -1446,7 +1455,7 @@ model KategoriProduk { nama String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - deletedAt DateTime @default(now()) + deletedAt DateTime? isActive Boolean @default(true) KategoriToPasar KategoriToPasar[] PasarDesa PasarDesa[] @@ -2429,39 +2438,22 @@ model Umkm { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt deletedAt DateTime? - produk ProdukUmkm[] -} - -model ProdukUmkm { - id String @id @default(cuid()) - nama String - harga Int - stok Int @default(0) - deskripsi String? - image FileStorage? @relation("ProdukUmkmImage", fields: [imageId], references: [id]) - imageId String? - umkm Umkm @relation(fields: [umkmId], references: [id]) - umkmId String - isActive Boolean @default(true) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deletedAt DateTime? - penjualan PenjualanProduk[] + produk PasarDesa[] } model PenjualanProduk { - id String @id @default(cuid()) - produk ProdukUmkm @relation(fields: [produkId], references: [id]) + id String @id @default(cuid()) + produk PasarDesa @relation(fields: [produkId], references: [id]) produkId String jumlah Int - hargaSatuan Int // snapshot harga saat transaksi, agar histori tetap akurat - totalNilai Int // hargaSatuan * jumlah - tanggal DateTime @default(now()) - periode String // format "YYYY-MM" untuk grouping bulanan - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + hargaSatuan Int // snapshot harga saat transaksi, agar histori tetap akurat + totalNilai Int // hargaSatuan * jumlah + tanggal DateTime @default(now()) + periode String // format "YYYY-MM" untuk grouping bulanan + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt deletedAt DateTime? - isActive Boolean @default(true) + isActive Boolean @default(true) @@index([periode]) @@index([produkId]) diff --git a/src/app/admin/(dashboard)/_state/ekonomi/umkm/umkm.ts b/src/app/admin/(dashboard)/_state/ekonomi/umkm/umkm.ts index a88b0c00..584920d7 100644 --- a/src/app/admin/(dashboard)/_state/ekonomi/umkm/umkm.ts +++ b/src/app/admin/(dashboard)/_state/ekonomi/umkm/umkm.ts @@ -25,7 +25,7 @@ const defaultUmkmForm = { isActive: true, }; -// Produk Form Validation +// Produk Form Validation (Now using PasarDesa model) const produkFormSchema = z.object({ nama: z.string().min(1, "Nama produk minimal 1 karakter"), harga: z.number().min(0, "Harga tidak boleh negatif"), @@ -33,6 +33,7 @@ const produkFormSchema = z.object({ umkmId: z.string().min(1, "UMKM wajib dipilih"), deskripsi: z.string().optional(), imageId: z.string().optional(), + kategoriId: z.string().min(1, "Kategori wajib dipilih"), // PasarDesa needs category }); const defaultProdukForm = { @@ -42,6 +43,7 @@ const defaultProdukForm = { umkmId: "", deskripsi: "", imageId: "", + kategoriId: "", isActive: true, }; diff --git a/src/app/admin/(dashboard)/ekonomi/umkm/data-umkm/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/umkm/data-umkm/create/page.tsx new file mode 100644 index 00000000..00e4e82c --- /dev/null +++ b/src/app/admin/(dashboard)/ekonomi/umkm/data-umkm/create/page.tsx @@ -0,0 +1,224 @@ +'use client'; +import { + Box, + Button, + Group, + Paper, + Stack, + TextInput, + Title, + Text, + Select, + ActionIcon, + Image, + Loader +} from '@mantine/core'; +import { Dropzone, IMAGE_MIME_TYPE } from '@mantine/dropzone'; +import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; +import umkmState from '../../../../_state/ekonomi/umkm/umkm'; +import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor'; +import ApiFetch from '@/lib/api-fetch'; + +export default function CreateDataUmkm() { + const router = useRouter(); + const state = useProxy(umkmState.umkm); + const [previewImage, setPreviewImage] = useState(null); + const [file, setFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + useEffect(() => { + umkmState.kategoriProduk.findManyAll.load(); + }, []); + + const handleResetForm = () => { + state.create.form = { + nama: "", + pemilik: "", + kategoriId: "", + deskripsi: "", + alamat: "", + kontak: "", + imageId: "", + isActive: true, + }; + setPreviewImage(null); + setFile(null); + }; + + const handleCreate = async () => { + setIsSubmitting(true); + try { + // 1. Upload image first if exists + let uploadedImageId = ""; + if (file) { + const res = await ApiFetch.api.fileStorage.create.post({ + file, + name: file.name + }); + + const uploaded = res.data?.data; + if (uploaded?.id) { + uploadedImageId = uploaded.id; + } else { + return toast.error("Gagal mengunggah logo UMKM"); + } + } + + // 2. Submit UMKM data + state.create.form.imageId = uploadedImageId; + const success = await state.create.submit(); + + if (success) { + handleResetForm(); + router.push('/admin/ekonomi/umkm/data-umkm'); + } + } catch (error) { + console.error(error); + toast.error("Terjadi kesalahan sistem"); + } finally { + setIsSubmitting(false); + } + }; + + return ( + + + + Daftarkan UMKM Baru + + + + + {/* Logo / Image UMKM */} + + Logo / Foto UMKM + {!previewImage ? ( + { + const file = files[0]; + setFile(file); + setPreviewImage(URL.createObjectURL(file)); + }} + maxSize={3 * 1024 ** 2} + accept={IMAGE_MIME_TYPE} + radius="md" + > + + + + + + + + + + + + + + Klik atau tarik gambar di sini + + + Maksimal 3MB + + + + + ) : ( + + Preview + { + setPreviewImage(null); + setFile(null); + }} + > + + + + )} + + + + (state.create.form.nama = e.target.value)} + /> + (state.create.form.pemilik = e.target.value)} + /> + + + + ({ + value: v.id, label: v.nama + })) || []} + value={state.create.form.umkmId} + onChange={(val) => (state.create.form.umkmId = val || "")} + /> + + (state.create.form.nama = e.target.value)} + /> + + + (state.create.form.harga = Number(val))} + /> + (state.create.form.stok = Number(val))} + /> + + +