feat(ekonomi): unify UMKM and Pasar Desa models, add business profile and product forms
This commit is contained in:
26
MIND/PLAN/refactor-umkm-pasar-desa.md
Normal file
26
MIND/PLAN/refactor-umkm-pasar-desa.md
Normal file
@@ -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.
|
||||||
10
MIND/PLAN/task-refactor-umkm-pasar-desa.md
Normal file
10
MIND/PLAN/task-refactor-umkm-pasar-desa.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Task: Refactor UMKM and Pasar Desa Model
|
||||||
|
|
||||||
|
- [x] Refactor `prisma/schema.prisma` and run `db push` <!-- id: 0 -->
|
||||||
|
- [x] Update Pasar Desa `findMany` API with `umkmId: null` filter <!-- id: 1 -->
|
||||||
|
- [x] Update UMKM Produk APIs (CRUD) to use `PasarDesa` model <!-- id: 2 -->
|
||||||
|
- [x] Update UMKM Dashboard analytics and Penjualan logic <!-- id: 3 -->
|
||||||
|
- [x] Create Admin Form for "Data UMKM" (Business Profile) <!-- id: 4 -->
|
||||||
|
- [x] Create Admin Form for "Produk UMKM" (Product) <!-- id: 5 -->
|
||||||
|
- [x] Link list views to new forms and update state <!-- id: 6 -->
|
||||||
|
- [ ] Run build and verify <!-- id: 7 -->
|
||||||
20
MIND/SUMMARY/refactor-umkm-pasar-desa-summary.md
Normal file
20
MIND/SUMMARY/refactor-umkm-pasar-desa-summary.md
Normal file
@@ -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.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "desa-darmasaba",
|
"name": "desa-darmasaba",
|
||||||
"version": "0.1.16",
|
"version": "0.1.17",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
|
|||||||
@@ -107,7 +107,6 @@ model FileStorage {
|
|||||||
MusikDesaAudio MusikDesa[] @relation("MusikAudioFile")
|
MusikDesaAudio MusikDesa[] @relation("MusikAudioFile")
|
||||||
MusikDesaCover MusikDesa[] @relation("MusikCoverImage")
|
MusikDesaCover MusikDesa[] @relation("MusikCoverImage")
|
||||||
UmkmImage Umkm[] @relation("UmkmImage")
|
UmkmImage Umkm[] @relation("UmkmImage")
|
||||||
ProdukUmkmImage ProdukUmkm[] @relation("ProdukUmkmImage")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//========================================= MENU LANDING PAGE ========================================= //
|
//========================================= MENU LANDING PAGE ========================================= //
|
||||||
@@ -1428,14 +1427,24 @@ model PasarDesa {
|
|||||||
image FileStorage? @relation(fields: [imageId], references: [id])
|
image FileStorage? @relation(fields: [imageId], references: [id])
|
||||||
imageId String?
|
imageId String?
|
||||||
harga Int
|
harga Int
|
||||||
rating Float
|
rating Float @default(0)
|
||||||
alamatUsaha String
|
alamatUsaha String? // Opsional, bisa ambil dari UMKM
|
||||||
kontak String
|
kontak String? // Opsional, bisa ambil dari UMKM
|
||||||
deskripsi String?
|
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())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
|
|
||||||
kategoriProduk KategoriProduk @relation(fields: [kategoriProdukId], references: [id])
|
kategoriProduk KategoriProduk @relation(fields: [kategoriProdukId], references: [id])
|
||||||
kategoriProdukId String
|
kategoriProdukId String
|
||||||
KategoriToPasar KategoriToPasar[]
|
KategoriToPasar KategoriToPasar[]
|
||||||
@@ -1446,7 +1455,7 @@ model KategoriProduk {
|
|||||||
nama String
|
nama String
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
KategoriToPasar KategoriToPasar[]
|
KategoriToPasar KategoriToPasar[]
|
||||||
PasarDesa PasarDesa[]
|
PasarDesa PasarDesa[]
|
||||||
@@ -2429,39 +2438,22 @@ model Umkm {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime?
|
deletedAt DateTime?
|
||||||
produk ProdukUmkm[]
|
produk PasarDesa[]
|
||||||
}
|
|
||||||
|
|
||||||
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[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model PenjualanProduk {
|
model PenjualanProduk {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
produk ProdukUmkm @relation(fields: [produkId], references: [id])
|
produk PasarDesa @relation(fields: [produkId], references: [id])
|
||||||
produkId String
|
produkId String
|
||||||
jumlah Int
|
jumlah Int
|
||||||
hargaSatuan Int // snapshot harga saat transaksi, agar histori tetap akurat
|
hargaSatuan Int // snapshot harga saat transaksi, agar histori tetap akurat
|
||||||
totalNilai Int // hargaSatuan * jumlah
|
totalNilai Int // hargaSatuan * jumlah
|
||||||
tanggal DateTime @default(now())
|
tanggal DateTime @default(now())
|
||||||
periode String // format "YYYY-MM" untuk grouping bulanan
|
periode String // format "YYYY-MM" untuk grouping bulanan
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime?
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
|
|
||||||
@@index([periode])
|
@@index([periode])
|
||||||
@@index([produkId])
|
@@index([produkId])
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const defaultUmkmForm = {
|
|||||||
isActive: true,
|
isActive: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Produk Form Validation
|
// Produk Form Validation (Now using PasarDesa model)
|
||||||
const produkFormSchema = z.object({
|
const produkFormSchema = z.object({
|
||||||
nama: z.string().min(1, "Nama produk minimal 1 karakter"),
|
nama: z.string().min(1, "Nama produk minimal 1 karakter"),
|
||||||
harga: z.number().min(0, "Harga tidak boleh negatif"),
|
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"),
|
umkmId: z.string().min(1, "UMKM wajib dipilih"),
|
||||||
deskripsi: z.string().optional(),
|
deskripsi: z.string().optional(),
|
||||||
imageId: z.string().optional(),
|
imageId: z.string().optional(),
|
||||||
|
kategoriId: z.string().min(1, "Kategori wajib dipilih"), // PasarDesa needs category
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultProdukForm = {
|
const defaultProdukForm = {
|
||||||
@@ -42,6 +43,7 @@ const defaultProdukForm = {
|
|||||||
umkmId: "",
|
umkmId: "",
|
||||||
deskripsi: "",
|
deskripsi: "",
|
||||||
imageId: "",
|
imageId: "",
|
||||||
|
kategoriId: "",
|
||||||
isActive: true,
|
isActive: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
224
src/app/admin/(dashboard)/ekonomi/umkm/data-umkm/create/page.tsx
Normal file
224
src/app/admin/(dashboard)/ekonomi/umkm/data-umkm/create/page.tsx
Normal file
@@ -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<string | null>(null);
|
||||||
|
const [file, setFile] = useState<File | null>(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 (
|
||||||
|
<Box>
|
||||||
|
<Group mb="lg">
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
leftSection={<IconArrowBack size={20} />}
|
||||||
|
>
|
||||||
|
Kembali
|
||||||
|
</Button>
|
||||||
|
<Title order={3}>Daftarkan UMKM Baru</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Paper withBorder p="xl" radius="md" shadow="sm">
|
||||||
|
<Stack gap="lg">
|
||||||
|
{/* Logo / Image UMKM */}
|
||||||
|
<Box>
|
||||||
|
<Text fw={500} size="sm" mb={4}>Logo / Foto UMKM</Text>
|
||||||
|
{!previewImage ? (
|
||||||
|
<Dropzone
|
||||||
|
onDrop={(files) => {
|
||||||
|
const file = files[0];
|
||||||
|
setFile(file);
|
||||||
|
setPreviewImage(URL.createObjectURL(file));
|
||||||
|
}}
|
||||||
|
maxSize={3 * 1024 ** 2}
|
||||||
|
accept={IMAGE_MIME_TYPE}
|
||||||
|
radius="md"
|
||||||
|
>
|
||||||
|
<Group justify="center" gap="xl" mih={120} style={{ pointerEvents: 'none' }}>
|
||||||
|
<Dropzone.Accept>
|
||||||
|
<IconUpload size={42} stroke={1.5} />
|
||||||
|
</Dropzone.Accept>
|
||||||
|
<Dropzone.Reject>
|
||||||
|
<IconX size={42} stroke={1.5} />
|
||||||
|
</Dropzone.Reject>
|
||||||
|
<Dropzone.Idle>
|
||||||
|
<IconPhoto size={42} stroke={1.5} />
|
||||||
|
</Dropzone.Idle>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text size="xl" inline>
|
||||||
|
Klik atau tarik gambar di sini
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c="dimmed" inline mt={7}>
|
||||||
|
Maksimal 3MB
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Group>
|
||||||
|
</Dropzone>
|
||||||
|
) : (
|
||||||
|
<Box pos="relative" w="fit-content">
|
||||||
|
<Image src={previewImage} h={200} radius="md" alt="Preview" />
|
||||||
|
<ActionIcon
|
||||||
|
color="red"
|
||||||
|
variant="filled"
|
||||||
|
pos="absolute"
|
||||||
|
top={5}
|
||||||
|
right={5}
|
||||||
|
onClick={() => {
|
||||||
|
setPreviewImage(null);
|
||||||
|
setFile(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconX size={14} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Group grow>
|
||||||
|
<TextInput
|
||||||
|
label="Nama UMKM / Bisnis"
|
||||||
|
placeholder="Contoh: Warung Sate Bu Komang"
|
||||||
|
required
|
||||||
|
value={state.create.form.nama}
|
||||||
|
onChange={(e) => (state.create.form.nama = e.target.value)}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label="Nama Pemilik"
|
||||||
|
placeholder="Masukkan nama lengkap pemilik"
|
||||||
|
required
|
||||||
|
value={state.create.form.pemilik}
|
||||||
|
onChange={(e) => (state.create.form.pemilik = e.target.value)}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Group grow>
|
||||||
|
<Select
|
||||||
|
label="Kategori Bisnis"
|
||||||
|
placeholder="Pilih kategori"
|
||||||
|
required
|
||||||
|
data={umkmState.kategoriProduk.findManyAll.data?.map(v => ({
|
||||||
|
value: v.id, label: v.nama
|
||||||
|
})) || []}
|
||||||
|
value={state.create.form.kategoriId}
|
||||||
|
onChange={(val) => (state.create.form.kategoriId = val || "")}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label="Nomor WA / Kontak"
|
||||||
|
placeholder="Contoh: 08123456789"
|
||||||
|
value={state.create.form.kontak}
|
||||||
|
onChange={(e) => (state.create.form.kontak = e.target.value)}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
label="Alamat Lengkap"
|
||||||
|
placeholder="Masukkan alamat fisik usaha"
|
||||||
|
value={state.create.form.alamat}
|
||||||
|
onChange={(e) => (state.create.form.alamat = e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fw={500} size="sm" mb={4}>Deskripsi UMKM</Text>
|
||||||
|
<CreateEditor
|
||||||
|
value={state.create.form.deskripsi || ""}
|
||||||
|
onChange={(val) => (state.create.form.deskripsi = val)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Group justify="flex-end" mt="xl">
|
||||||
|
<Button variant="outline" color="gray" onClick={handleResetForm}>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="blue"
|
||||||
|
onClick={handleCreate}
|
||||||
|
loading={isSubmitting}
|
||||||
|
>
|
||||||
|
Daftarkan UMKM
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -21,11 +21,13 @@ import {
|
|||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconPlus, IconSearch, IconEdit, IconTrash } from '@tabler/icons-react';
|
import { IconPlus, IconSearch, IconEdit, IconTrash } from '@tabler/icons-react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import umkmState from '../../../_state/ekonomi/umkm/umkm';
|
import umkmState from '../../../_state/ekonomi/umkm/umkm';
|
||||||
|
|
||||||
function DataUmkm() {
|
function DataUmkm() {
|
||||||
|
const router = useRouter();
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const state = useProxy(umkmState.umkm.findMany);
|
const state = useProxy(umkmState.umkm.findMany);
|
||||||
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||||
@@ -38,7 +40,11 @@ function DataUmkm() {
|
|||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
<Group justify="space-between">
|
<Group justify="space-between">
|
||||||
<Title order={3}>Data UMKM</Title>
|
<Title order={3}>Data UMKM</Title>
|
||||||
<Button leftSection={<IconPlus size={18} />} color="blue">
|
<Button
|
||||||
|
leftSection={<IconPlus size={18} />}
|
||||||
|
color="blue"
|
||||||
|
onClick={() => router.push('/admin/ekonomi/umkm/data-umkm/create')}
|
||||||
|
>
|
||||||
Tambah UMKM
|
Tambah UMKM
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
203
src/app/admin/(dashboard)/ekonomi/umkm/produk/create/page.tsx
Normal file
203
src/app/admin/(dashboard)/ekonomi/umkm/produk/create/page.tsx
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
'use client';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Text,
|
||||||
|
Select,
|
||||||
|
ActionIcon,
|
||||||
|
Image,
|
||||||
|
NumberInput
|
||||||
|
} 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 CreateProdukUmkm() {
|
||||||
|
const router = useRouter();
|
||||||
|
const state = useProxy(umkmState.produk);
|
||||||
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||||
|
const [file, setFile] = useState<File | null>(null);
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Load UMKM list for selection and Categories
|
||||||
|
umkmState.umkm.findMany.load(1, 100);
|
||||||
|
umkmState.kategoriProduk.findManyAll.load();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleResetForm = () => {
|
||||||
|
state.create.form = {
|
||||||
|
nama: "",
|
||||||
|
harga: 0,
|
||||||
|
stok: 0,
|
||||||
|
umkmId: "",
|
||||||
|
deskripsi: "",
|
||||||
|
imageId: "",
|
||||||
|
kategoriId: "",
|
||||||
|
isActive: true,
|
||||||
|
};
|
||||||
|
setPreviewImage(null);
|
||||||
|
setFile(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreate = async () => {
|
||||||
|
setIsSubmitting(true);
|
||||||
|
try {
|
||||||
|
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 foto produk");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.create.form.imageId = uploadedImageId;
|
||||||
|
const success = await state.create.submit();
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
handleResetForm();
|
||||||
|
router.push('/admin/ekonomi/umkm/produk');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
toast.error("Terjadi kesalahan sistem");
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Group mb="lg">
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
leftSection={<IconArrowBack size={20} />}
|
||||||
|
>
|
||||||
|
Kembali
|
||||||
|
</Button>
|
||||||
|
<Title order={3}>Tambah Produk UMKM</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Paper withBorder p="xl" radius="md" shadow="sm">
|
||||||
|
<Stack gap="lg">
|
||||||
|
<Box>
|
||||||
|
<Text fw={500} size="sm" mb={4}>Foto Produk</Text>
|
||||||
|
{!previewImage ? (
|
||||||
|
<Dropzone
|
||||||
|
onDrop={(files) => {
|
||||||
|
const file = files[0];
|
||||||
|
setFile(file);
|
||||||
|
setPreviewImage(URL.createObjectURL(file));
|
||||||
|
}}
|
||||||
|
maxSize={3 * 1024 ** 2}
|
||||||
|
accept={IMAGE_MIME_TYPE}
|
||||||
|
radius="md"
|
||||||
|
>
|
||||||
|
<Group justify="center" gap="xl" mih={120} style={{ pointerEvents: 'none' }}>
|
||||||
|
<Dropzone.Idle>
|
||||||
|
<IconPhoto size={42} stroke={1.5} />
|
||||||
|
</Dropzone.Idle>
|
||||||
|
<Box>
|
||||||
|
<Text size="xl" inline>Pilih gambar produk</Text>
|
||||||
|
<Text size="sm" c="dimmed" inline mt={7}>Maksimal 3MB</Text>
|
||||||
|
</Box>
|
||||||
|
</Group>
|
||||||
|
</Dropzone>
|
||||||
|
) : (
|
||||||
|
<Box pos="relative" w="fit-content">
|
||||||
|
<Image src={previewImage} h={200} radius="md" alt="Preview" />
|
||||||
|
<ActionIcon color="red" variant="filled" pos="absolute" top={5} right={5} onClick={() => { setPreviewImage(null); setFile(null); }}>
|
||||||
|
<IconX size={14} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label="Pilih UMKM Pemilik"
|
||||||
|
placeholder="Siapa pemilik produk ini?"
|
||||||
|
required
|
||||||
|
searchable
|
||||||
|
data={umkmState.umkm.findMany.data?.map(v => ({
|
||||||
|
value: v.id, label: v.nama
|
||||||
|
})) || []}
|
||||||
|
value={state.create.form.umkmId}
|
||||||
|
onChange={(val) => (state.create.form.umkmId = val || "")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
label="Nama Produk"
|
||||||
|
placeholder="Contoh: Kripik Singkong Pedas"
|
||||||
|
required
|
||||||
|
value={state.create.form.nama}
|
||||||
|
onChange={(e) => (state.create.form.nama = e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Group grow>
|
||||||
|
<NumberInput
|
||||||
|
label="Harga Produk (Rp)"
|
||||||
|
placeholder="0"
|
||||||
|
required
|
||||||
|
min={0}
|
||||||
|
thousandSeparator="."
|
||||||
|
decimalSeparator=","
|
||||||
|
value={state.create.form.harga}
|
||||||
|
onChange={(val) => (state.create.form.harga = Number(val))}
|
||||||
|
/>
|
||||||
|
<NumberInput
|
||||||
|
label="Stok Awal"
|
||||||
|
placeholder="0"
|
||||||
|
required
|
||||||
|
min={0}
|
||||||
|
value={state.create.form.stok}
|
||||||
|
onChange={(val) => (state.create.form.stok = Number(val))}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label="Kategori Produk"
|
||||||
|
placeholder="Pilih kategori produk"
|
||||||
|
required
|
||||||
|
data={umkmState.kategoriProduk.findManyAll.data?.map(v => ({
|
||||||
|
value: v.id, label: v.nama
|
||||||
|
})) || []}
|
||||||
|
value={state.create.form.kategoriId}
|
||||||
|
onChange={(val) => (state.create.form.kategoriId = val || "")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fw={500} size="sm" mb={4}>Deskripsi Produk</Text>
|
||||||
|
<CreateEditor
|
||||||
|
value={state.create.form.deskripsi || ""}
|
||||||
|
onChange={(val) => (state.create.form.deskripsi = val)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Group justify="flex-end" mt="xl">
|
||||||
|
<Button variant="outline" color="gray" onClick={handleResetForm}>Reset</Button>
|
||||||
|
<Button color="blue" onClick={handleCreate} loading={isSubmitting}>Simpan Produk</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -21,11 +21,13 @@ import {
|
|||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconPlus, IconSearch, IconEdit, IconTrash } from '@tabler/icons-react';
|
import { IconPlus, IconSearch, IconEdit, IconTrash } from '@tabler/icons-react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import umkmState from '../../../_state/ekonomi/umkm/umkm';
|
import umkmState from '../../../_state/ekonomi/umkm/umkm';
|
||||||
|
|
||||||
function ProdukUmkm() {
|
function ProdukUmkm() {
|
||||||
|
const router = useRouter();
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const state = useProxy(umkmState.produk.findMany);
|
const state = useProxy(umkmState.produk.findMany);
|
||||||
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||||
@@ -38,7 +40,11 @@ function ProdukUmkm() {
|
|||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
<Group justify="space-between">
|
<Group justify="space-between">
|
||||||
<Title order={3}>Daftar Produk UMKM</Title>
|
<Title order={3}>Daftar Produk UMKM</Title>
|
||||||
<Button leftSection={<IconPlus size={18} />} color="blue">
|
<Button
|
||||||
|
leftSection={<IconPlus size={18} />}
|
||||||
|
color="blue"
|
||||||
|
onClick={() => router.push('/admin/ekonomi/umkm/produk/create')}
|
||||||
|
>
|
||||||
Tambah Produk
|
Tambah Produk
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
@@ -11,8 +11,12 @@ async function pasarDesaFindMany(context: Context) {
|
|||||||
const categoryId = context.query.categoryId as string | undefined;
|
const categoryId = context.query.categoryId as string | undefined;
|
||||||
const skip = (page - 1) * limit;
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
// Buat where clause
|
// Buat where clause: Tampilkan hanya yang TIDAK punya umkmId (Produk Pasar Murni)
|
||||||
const where: any = { isActive: true };
|
const where: any = {
|
||||||
|
isActive: true,
|
||||||
|
deletedAt: null,
|
||||||
|
umkmId: null
|
||||||
|
};
|
||||||
|
|
||||||
// Tambahkan filter kategori (jika ada)
|
// Tambahkan filter kategori (jika ada)
|
||||||
if (categoryId) {
|
if (categoryId) {
|
||||||
@@ -65,7 +69,7 @@ async function pasarDesaFindMany(context: Context) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Berhasil ambil pasar desa dengan pagination",
|
message: "Berhasil ambil pasar desa dengan pagination (Non-UMKM)",
|
||||||
data,
|
data,
|
||||||
page,
|
page,
|
||||||
limit,
|
limit,
|
||||||
|
|||||||
@@ -22,8 +22,9 @@ async function umkmDashboardDetailPenjualan(context: Context) {
|
|||||||
where: { periode: periodeLalu, deletedAt: null },
|
where: { periode: periodeLalu, deletedAt: null },
|
||||||
_sum: { totalNilai: true }
|
_sum: { totalNilai: true }
|
||||||
}),
|
}),
|
||||||
prisma.produkUmkm.findMany({
|
// Use PasarDesa with umkmId filter
|
||||||
where: { deletedAt: null },
|
prisma.pasarDesa.findMany({
|
||||||
|
where: { deletedAt: null, umkmId: { not: null } },
|
||||||
select: { id: true, nama: true, stok: true }
|
select: { id: true, nama: true, stok: true }
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
@@ -33,11 +34,11 @@ async function umkmDashboardDetailPenjualan(context: Context) {
|
|||||||
const laluRaw = produkLalu.find(l => l.produkId === p.id)?._sum || { totalNilai: 0 };
|
const laluRaw = produkLalu.find(l => l.produkId === p.id)?._sum || { totalNilai: 0 };
|
||||||
|
|
||||||
const skrg = {
|
const skrg = {
|
||||||
totalNilai: skrgRaw.totalNilai || 0,
|
totalNilai: (skrgRaw as any).totalNilai || 0,
|
||||||
jumlah: skrgRaw.jumlah || 0
|
jumlah: (skrgRaw as any).jumlah || 0
|
||||||
};
|
};
|
||||||
const lalu = {
|
const lalu = {
|
||||||
totalNilai: laluRaw.totalNilai || 0
|
totalNilai: (laluRaw as any).totalNilai || 0
|
||||||
};
|
};
|
||||||
|
|
||||||
let trend = "stable";
|
let trend = "stable";
|
||||||
|
|||||||
@@ -20,7 +20,10 @@ async function umkmDashboardRingSummary(context: Context) {
|
|||||||
where: { periode: periodeLalu, deletedAt: null },
|
where: { periode: periodeLalu, deletedAt: null },
|
||||||
_sum: { totalNilai: true }
|
_sum: { totalNilai: true }
|
||||||
}),
|
}),
|
||||||
prisma.produkUmkm.count({ where: { isActive: true, deletedAt: null } }),
|
// Count from PasarDesa with umkmId filter
|
||||||
|
prisma.pasarDesa.count({
|
||||||
|
where: { isActive: true, deletedAt: null, umkmId: { not: null } }
|
||||||
|
}),
|
||||||
prisma.penjualanProduk.count({ where: { periode, deletedAt: null } })
|
prisma.penjualanProduk.count({ where: { periode, deletedAt: null } })
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -15,17 +15,18 @@ async function umkmDashboardTopProduk(context: Context) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const data = await Promise.all(topPenjualan.map(async (item) => {
|
const data = await Promise.all(topPenjualan.map(async (item) => {
|
||||||
const produk = await prisma.produkUmkm.findUnique({
|
// Find from PasarDesa now
|
||||||
|
const produk = await prisma.pasarDesa.findUnique({
|
||||||
where: { id: item.produkId },
|
where: { id: item.produkId },
|
||||||
include: { umkm: true }
|
include: { umkm: true }
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
namaProduk: produk?.nama || "Unknown",
|
namaProduk: produk?.nama || "Unknown",
|
||||||
namaUmkm: produk?.umkm.nama || "Unknown",
|
namaUmkm: produk?.umkm?.nama || "Unknown",
|
||||||
totalPenjualan: item._sum.totalNilai || 0,
|
totalPenjualan: item._sum.totalNilai || 0,
|
||||||
jumlahTerjual: item._sum.jumlah || 0,
|
jumlahTerjual: item._sum.jumlah || 0,
|
||||||
growth: 0 // logic growth bisa ditambah jika diperlukan
|
growth: 0
|
||||||
};
|
};
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ async function penjualanProdukCreate(context: Context) {
|
|||||||
const totalNilai = body.jumlah * body.hargaSatuan;
|
const totalNilai = body.jumlah * body.hargaSatuan;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Gunakan transaction untuk update stok produk
|
// Gunakan transaction untuk update stok produk (PasarDesa)
|
||||||
const result = await prisma.$transaction(async (tx) => {
|
const result = await prisma.$transaction(async (tx) => {
|
||||||
// 1. Catat penjualan
|
// 1. Catat penjualan (relasi ke PasarDesa)
|
||||||
const penjualan = await tx.penjualanProduk.create({
|
const penjualan = await tx.penjualanProduk.create({
|
||||||
data: {
|
data: {
|
||||||
produkId: body.produkId,
|
produkId: body.produkId,
|
||||||
@@ -26,8 +26,8 @@ async function penjualanProdukCreate(context: Context) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. Update stok produk
|
// 2. Update stok di model PasarDesa
|
||||||
await tx.produkUmkm.update({
|
await tx.pasarDesa.update({
|
||||||
where: { id: body.produkId },
|
where: { id: body.produkId },
|
||||||
data: {
|
data: {
|
||||||
stok: {
|
stok: {
|
||||||
@@ -41,7 +41,7 @@ async function penjualanProdukCreate(context: Context) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Berhasil mencatat penjualan produk",
|
message: "Berhasil mencatat penjualan produk (PasarDesa)",
|
||||||
data: result,
|
data: result,
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ async function penjualanProdukDelete(context: Context) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 3. Kembalikan stok produk
|
// 3. Kembalikan stok produk ke PasarDesa
|
||||||
await tx.produkUmkm.update({
|
await tx.pasarDesa.update({
|
||||||
where: { id: data.produkId },
|
where: { id: data.produkId },
|
||||||
data: {
|
data: {
|
||||||
stok: {
|
stok: {
|
||||||
@@ -38,7 +38,7 @@ async function penjualanProdukDelete(context: Context) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Berhasil menghapus data penjualan dan mengembalikan stok",
|
message: "Berhasil menghapus data penjualan dan mengembalikan stok (PasarDesa)",
|
||||||
data: result,
|
data: result,
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -32,10 +32,10 @@ async function penjualanProdukUpdate(context: Context) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 3. Update stok jika produk sama, sesuaikan selisih
|
// 3. Update stok di PasarDesa jika produk sama, sesuaikan selisih
|
||||||
if (oldData.produkId === body.produkId) {
|
if (oldData.produkId === body.produkId) {
|
||||||
const diff = body.jumlah - oldData.jumlah;
|
const diff = body.jumlah - oldData.jumlah;
|
||||||
await tx.produkUmkm.update({
|
await tx.pasarDesa.update({
|
||||||
where: { id: body.produkId },
|
where: { id: body.produkId },
|
||||||
data: {
|
data: {
|
||||||
stok: {
|
stok: {
|
||||||
@@ -44,8 +44,8 @@ async function penjualanProdukUpdate(context: Context) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Jika produk berubah, kembalikan stok lama dan kurangi stok baru
|
// Jika produk berubah, kembalikan stok lama dan kurangi stok baru di PasarDesa
|
||||||
await tx.produkUmkm.update({
|
await tx.pasarDesa.update({
|
||||||
where: { id: oldData.produkId },
|
where: { id: oldData.produkId },
|
||||||
data: {
|
data: {
|
||||||
stok: {
|
stok: {
|
||||||
@@ -53,7 +53,7 @@ async function penjualanProdukUpdate(context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
await tx.produkUmkm.update({
|
await tx.pasarDesa.update({
|
||||||
where: { id: body.produkId },
|
where: { id: body.produkId },
|
||||||
data: {
|
data: {
|
||||||
stok: {
|
stok: {
|
||||||
@@ -68,7 +68,7 @@ async function penjualanProdukUpdate(context: Context) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Berhasil memperbarui data penjualan",
|
message: "Berhasil memperbarui data penjualan (PasarDesa)",
|
||||||
data: result,
|
data: result,
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ async function produkUmkmCreate(context: Context) {
|
|||||||
const body = context.body as any;
|
const body = context.body as any;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await prisma.produkUmkm.create({
|
const data = await prisma.pasarDesa.create({
|
||||||
data: {
|
data: {
|
||||||
nama: body.nama,
|
nama: body.nama,
|
||||||
harga: body.harga,
|
harga: body.harga,
|
||||||
@@ -14,12 +14,14 @@ async function produkUmkmCreate(context: Context) {
|
|||||||
umkmId: body.umkmId,
|
umkmId: body.umkmId,
|
||||||
imageId: body.imageId,
|
imageId: body.imageId,
|
||||||
isActive: body.isActive ?? true,
|
isActive: body.isActive ?? true,
|
||||||
|
rating: 0, // Default for UMKM products
|
||||||
|
kategoriProdukId: body.kategoriId, // Now required via PasarDesa
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Berhasil membuat produk UMKM baru",
|
message: "Berhasil membuat produk UMKM baru (PasarDesa)",
|
||||||
data,
|
data,
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ async function produkUmkmDelete(context: Context) {
|
|||||||
const id = context.params.id;
|
const id = context.params.id;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Soft delete
|
// Soft delete on PasarDesa
|
||||||
const data = await prisma.produkUmkm.update({
|
const data = await prisma.pasarDesa.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: {
|
data: {
|
||||||
deletedAt: new Date(),
|
deletedAt: new Date(),
|
||||||
@@ -16,7 +16,7 @@ async function produkUmkmDelete(context: Context) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Berhasil menghapus produk UMKM",
|
message: "Berhasil menghapus produk UMKM (PasarDesa)",
|
||||||
data,
|
data,
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -10,16 +10,19 @@ async function produkUmkmFindMany(context: Context) {
|
|||||||
const kategoriId = context.query.kategoriId as string | undefined;
|
const kategoriId = context.query.kategoriId as string | undefined;
|
||||||
const skip = (page - 1) * limit;
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
const where: any = { deletedAt: null };
|
// Filter: ONLY products that belong to an UMKM
|
||||||
|
const where: any = {
|
||||||
|
deletedAt: null,
|
||||||
|
isActive: true,
|
||||||
|
umkmId: { not: null }
|
||||||
|
};
|
||||||
|
|
||||||
if (umkmId) {
|
if (umkmId) {
|
||||||
where.umkmId = umkmId;
|
where.umkmId = umkmId;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (kategoriId) {
|
if (kategoriId) {
|
||||||
where.umkm = {
|
where.kategoriProdukId = kategoriId;
|
||||||
kategoriId: kategoriId
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
@@ -28,19 +31,20 @@ async function produkUmkmFindMany(context: Context) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const [data, total] = await Promise.all([
|
const [data, total] = await Promise.all([
|
||||||
prisma.produkUmkm.findMany({
|
prisma.pasarDesa.findMany({
|
||||||
where,
|
where,
|
||||||
include: {
|
include: {
|
||||||
image: true,
|
image: true,
|
||||||
umkm: {
|
umkm: {
|
||||||
include: { kategori: true }
|
include: { kategori: true }
|
||||||
}
|
},
|
||||||
|
kategoriProduk: true
|
||||||
},
|
},
|
||||||
skip,
|
skip,
|
||||||
take: limit,
|
take: limit,
|
||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { createdAt: 'desc' },
|
||||||
}),
|
}),
|
||||||
prisma.produkUmkm.count({ where }),
|
prisma.pasarDesa.count({ where }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ async function produkUmkmFindUnique(context: Context) {
|
|||||||
const id = context.params.id;
|
const id = context.params.id;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await prisma.produkUmkm.findUnique({
|
const data = await prisma.pasarDesa.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
include: {
|
include: {
|
||||||
image: true,
|
image: true,
|
||||||
@@ -14,7 +14,8 @@ async function produkUmkmFindUnique(context: Context) {
|
|||||||
where: { deletedAt: null },
|
where: { deletedAt: null },
|
||||||
orderBy: { tanggal: 'desc' },
|
orderBy: { tanggal: 'desc' },
|
||||||
take: 10
|
take: 10
|
||||||
}
|
},
|
||||||
|
kategoriProduk: true
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -27,7 +28,7 @@ async function produkUmkmFindUnique(context: Context) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Berhasil mengambil detail produk UMKM",
|
message: "Berhasil mengambil detail produk UMKM (PasarDesa)",
|
||||||
data,
|
data,
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ async function produkUmkmUpdate(context: Context) {
|
|||||||
const id = context.params.id;
|
const id = context.params.id;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await prisma.produkUmkm.update({
|
const data = await prisma.pasarDesa.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: {
|
data: {
|
||||||
nama: body.nama,
|
nama: body.nama,
|
||||||
@@ -16,12 +16,13 @@ async function produkUmkmUpdate(context: Context) {
|
|||||||
umkmId: body.umkmId,
|
umkmId: body.umkmId,
|
||||||
imageId: body.imageId,
|
imageId: body.imageId,
|
||||||
isActive: body.isActive,
|
isActive: body.isActive,
|
||||||
|
kategoriProdukId: body.kategoriId, // Now editable via PasarDesa
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Berhasil memperbarui produk UMKM",
|
message: "Berhasil memperbarui produk UMKM (PasarDesa)",
|
||||||
data,
|
data,
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
Reference in New Issue
Block a user