Compare commits

...

4 Commits

Author SHA1 Message Date
187e3a2115 feat(admin): refactor UMKM edit pages to match berita pattern with interfaces 2026-04-24 14:34:02 +08:00
7f5588f69e feat(admin): refactor UMKM edit pages to match berita pattern 2026-04-24 14:20:40 +08:00
30fbed73c9 fix(admin): resolve 404 on kategoriProduk API and correct Valtio state endpoint mismatches
- Created missing API endpoint
- Corrected UMKM and Produk update/delete routes in Valtio state to match Elysia API:
  - UMKM Update:
  - UMKM Delete:
  - Produk Update:
  - Produk Delete:
2026-04-24 12:19:24 +08:00
67c51302fe docs: add plan, task, and summary for admin-umkm-edit 2026-04-24 11:49:15 +08:00
12 changed files with 623 additions and 190 deletions

View File

@@ -0,0 +1,24 @@
# Plan - Admin UMKM & Produk Edit Pages
## Problem
Admin UMKM module list pages have "Edit" buttons that are not functional, and there are no edit pages or update state logic implemented.
## Strategy
1. Update Valtio state in `src/app/admin/(dashboard)/_state/ekonomi/umkm/umkm.ts` with `update` modules for UMKM and Produk.
2. Delete Valtio state in `src/app/admin/(dashboard)/_state/ekonomi/umkm/umkm.ts` with `del` modules for UMKM and Produk.
3. Add `onClick` handlers to "Edit" buttons in list pages.
4. Create new edit pages.
5. Use `ModalKonfirmasiHapus` component for delete actions.
6. Verify changes with a successful build.
7. Follow deployment workflow.
## Progress
- [x] Update Valtio state with update modules
- [x] Update Valtio state with delete modules
- [x] Wire edit and delete buttons in list pages
- [x] Create UMKM edit page
- [x] Create Produk edit page
- [x] Build and fix errors
- [x] Update version in package.json
- [x] Commit and push to branch
- [x] Merge to stg and push to remotes

View File

@@ -0,0 +1,22 @@
# Plan - Refactor UMKM Edit Pages Pattern
## Problem
The edit pages for UMKM (Data UMKM and Produk) use an older UI pattern. The user wants to align them with the newer pattern used in the Berita edit page.
## Strategy
1. Analyze the pattern in `src/app/admin/(dashboard)/desa/berita/list-berita/[id]/edit/page.tsx`.
2. Refactor `src/app/admin/(dashboard)/ekonomi/umkm/produk/[id]/edit/page.tsx` to match the pattern.
3. Refactor `src/app/admin/(dashboard)/ekonomi/umkm/data-umkm/[id]/edit/page.tsx` to match the pattern.
4. Add "Batal" (Reset) functionality to both pages.
5. Standardize UI components (Header, Paper, Dropzone, Action buttons).
6. Verify with a production build.
7. Follow the versioning and deployment workflow.
## Progress
- [x] Analyze Berita edit page pattern
- [x] Refactor UMKM Produk edit page (with interfaces)
- [x] Refactor Data UMKM edit page (with interfaces)
- [x] Run build and fix any errors
- [ ] Update version in package.json
- [ ] Commit and push to task branch
- [ ] Merge to stg branch

View File

@@ -0,0 +1,14 @@
# Task - Admin UMKM & Produk Edit functionality
## Description
Implement Edit and Delete functionality for UMKM and Produk modules in the admin dashboard.
## Tasks
- [x] Update Valtio state with update/delete modules for UMKM and Produk
- [x] Wire edit/delete buttons in `src/app/admin/(dashboard)/ekonomi/umkm/data-umkm/page.tsx`
- [x] Wire edit/delete buttons in `src/app/admin/(dashboard)/ekonomi/umkm/produk/page.tsx`
- [x] Create edit page for UMKM at `src/app/admin/(dashboard)/ekonomi/umkm/data-umkm/[id]/edit/page.tsx`
- [x] Create edit page for Produk at `src/app/admin/(dashboard)/ekonomi/umkm/produk/[id]/edit/page.tsx`
- [x] Ensure `ModalKonfirmasiHapus` is used correctly
- [x] Run `bun run build` and fix errors
- [x] Push to task branch and merge to stg

View File

@@ -0,0 +1,12 @@
# Task - Refactor UMKM Edit Pages Pattern
Refactor Data UMKM and Produk edit pages to match the Berita edit page UI pattern and logic.
## Steps
1. [x] Analyze `berita/list-berita/[id]/edit/page.tsx` for the desired pattern.
2. [x] Implement the pattern in `ekonomi/umkm/produk/[id]/edit/page.tsx` (using interfaces).
3. [x] Implement the pattern in `ekonomi/umkm/data-umkm/[id]/edit/page.tsx` (using interfaces).
4. [x] Run `bun run build` to verify.
5. [x] Update `package.json` version.
6. [x] Commit with message: "feat(admin): refactor UMKM edit pages to match berita pattern with interfaces".
7. [ ] Create summary in `MIND/SUMMARY/refactor-umkm-edit-pages-pattern-summary.md`.

View File

@@ -0,0 +1,15 @@
# Summary - Admin UMKM & Produk Edit functionality
## Changes
- **Valtio State**: Added `update` and `del` methods for UMKM and Produk modules in `src/app/admin/(dashboard)/_state/ekonomi/umkm/umkm.ts`.
- **List Pages**: Updated `data-umkm/page.tsx` and `produk/page.tsx` to handle edit (navigation) and delete (confirmation modal + state action).
- **Edit Pages**:
- Created `src/app/admin/(dashboard)/ekonomi/umkm/data-umkm/[id]/edit/page.tsx`
- Created `src/app/admin/(dashboard)/ekonomi/umkm/produk/[id]/edit/page.tsx`
- **Component**: Integrated `ModalKonfirmasiHapus` with named import.
- **Version**: Bumped to `0.1.21`.
## Verification
- Successfully ran `bun run build`.
- Pushed to `tasks/admin-umkm-edit/implement-edit-delete/2026-04-24-11-44`.
- Merged to `stg` and pushed to `origin` and `deploy` remotes.

View File

@@ -0,0 +1,14 @@
# Summary - Refactor UMKM Edit Pages Pattern
## Changes
1. **UMKM Produk Edit Page**: Refactored `src/app/admin/(dashboard)/ekonomi/umkm/produk/[id]/edit/page.tsx` to match the "Berita" edit page pattern. Added explicit `ProdukData` and `ProdukForm` interfaces. Added Reset ("Batal") functionality, standardized header, paper, and dropzone styling, and used `EditEditor`.
2. **Data UMKM Edit Page**: Refactored `src/app/admin/(dashboard)/ekonomi/umkm/data-umkm/[id]/edit/page.tsx` with the same pattern and interfaces (`UmkmData`, `UmkmForm`).
3. **Type Safety**: Improved type safety by using explicit interfaces for data fetching and form state management.
4. **UI Consistency**: Standardized colors and component usage across UMKM edit pages.
5. **UX Improvement**: Added a "Batal" button that resets the form to its original data state.
6. **Build Verification**: Confirmed that the project builds successfully with `bun run build`.
## Verification Results
- `bun run build`: Success.
- Pattern Match: Both pages now follow the consistent layout and logic of the Berita edit page.
- Reset Functionality: Implemented and verified via logic review.

View File

@@ -1,6 +1,6 @@
{
"name": "desa-darmasaba",
"version": "0.1.21",
"version": "0.1.23",
"private": true,
"scripts": {
"dev": "next dev",

View File

@@ -132,7 +132,7 @@ export const umkmState = proxy({
if (!cek.success) return toast.error("Cek kembali form anda");
this.loading = true;
try {
const res = await fetch(`/api/ekonomi/umkm/update/${id}`, {
const res = await fetch(`/api/ekonomi/umkm/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(this.form)
@@ -157,7 +157,7 @@ export const umkmState = proxy({
async submit(id: string) {
this.loading = true;
try {
const res = await fetch(`/api/ekonomi/umkm/delete/${id}`, {
const res = await fetch(`/api/ekonomi/umkm/del/${id}`, {
method: "DELETE"
});
const result = await res.json();
@@ -263,7 +263,7 @@ export const umkmState = proxy({
if (!cek.success) return toast.error("Cek kembali form anda");
this.loading = true;
try {
const res = await fetch(`/api/ekonomi/umkm/produk/update/${id}`, {
const res = await fetch(`/api/ekonomi/umkm/produk/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(this.form)
@@ -283,7 +283,7 @@ export const umkmState = proxy({
async submit(id: string) {
this.loading = true;
try {
const res = await fetch(`/api/ekonomi/umkm/produk/delete/${id}`, {
const res = await fetch(`/api/ekonomi/umkm/produk/del/${id}`, {
method: "DELETE"
});
const result = await res.json();

View File

@@ -1,50 +1,108 @@
'use client';
/* eslint-disable react-hooks/exhaustive-deps */
"use client";
import EditEditor from "@/app/admin/(dashboard)/_com/editEditor";
import colors from "@/con/colors";
import ApiFetch from "@/lib/api-fetch";
import {
ActionIcon,
Box,
Button,
Group,
Image,
Paper,
Select,
Stack,
Text,
TextInput,
Title,
Text,
Select,
ActionIcon,
Image,
Loader,
Center
} from '@mantine/core';
import { Dropzone, IMAGE_MIME_TYPE } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter, useParams } 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';
Center,
} from "@mantine/core";
import { Dropzone, IMAGE_MIME_TYPE } from "@mantine/dropzone";
import {
IconArrowBack,
IconPhoto,
IconUpload,
IconX,
} from "@tabler/icons-react";
import { useParams, 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";
export default function EditDataUmkm() {
interface UmkmData {
id: string;
nama: string;
pemilik: string;
kategoriId: string | null;
deskripsi: string | null;
alamat: string | null;
kontak: string | null;
imageId: string | null;
image?: { url: string } | null;
}
interface UmkmForm {
nama: string;
pemilik: string;
kategoriId: string;
deskripsi: string;
alamat: string;
kontak: string;
imageId: string;
}
function EditDataUmkm() {
const router = useRouter();
const params = useParams();
const id = params.id as string;
const state = useProxy(umkmState.umkm);
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isInitialLoading, setIsInitialLoading] = useState(true);
const [formData, setFormData] = useState<UmkmForm>({
nama: "",
pemilik: "",
kategoriId: "",
deskripsi: "",
alamat: "",
kontak: "",
imageId: "",
});
const [originalData, setOriginalData] = useState<UmkmForm & { imageUrl: string }>({
nama: "",
pemilik: "",
kategoriId: "",
deskripsi: "",
alamat: "",
kontak: "",
imageId: "",
imageUrl: ""
});
const isFormValid = () => {
return (
formData.nama?.trim() !== '' &&
formData.pemilik?.trim() !== '' &&
formData.kategoriId !== ''
);
};
useEffect(() => {
const init = async () => {
await Promise.all([
umkmState.kategoriProduk.findManyAll.load(),
state.findUnique.load(id)
umkmState.umkm.findUnique.load(id)
]);
if (state.findUnique.data) {
const data = state.findUnique.data;
state.update.form = {
const data = umkmState.umkm.findUnique.data as UmkmData | null;
if (data) {
const initialForm: UmkmForm = {
nama: data.nama || "",
pemilik: data.pemilik || "",
kategoriId: data.kategoriId || "",
@@ -52,9 +110,14 @@ export default function EditDataUmkm() {
alamat: data.alamat || "",
kontak: data.kontak || "",
imageId: data.imageId || "",
isActive: data.isActive ?? true,
};
setFormData(initialForm);
setOriginalData({
...initialForm,
imageUrl: data.image?.url || ""
});
if (data.image?.url) {
setPreviewImage(data.image.url);
}
@@ -62,13 +125,20 @@ export default function EditDataUmkm() {
setIsInitialLoading(false);
};
init();
}, [id, state.findUnique, state.update]);
}, [id]);
const handleChange = (field: keyof UmkmForm, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
const handleUpdate = async () => {
if (!formData.nama?.trim()) return toast.error("Nama UMKM wajib diisi");
if (!formData.pemilik?.trim()) return toast.error("Nama pemilik wajib diisi");
if (!formData.kategoriId) return toast.error("Kategori wajib dipilih");
setIsSubmitting(true);
try {
// 1. Upload image if new file selected
let uploadedImageId = state.update.form.imageId;
let uploadedImageId = formData.imageId;
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({
file,
@@ -84,10 +154,14 @@ export default function EditDataUmkm() {
}
}
// 2. Submit UMKM data
state.update.form.imageId = uploadedImageId;
const success = await state.update.submit(id);
// Update proxy state
umkmState.umkm.update.form = {
...umkmState.umkm.update.form,
...formData,
imageId: uploadedImageId
};
const success = await umkmState.umkm.update.submit(id);
if (success) {
router.push('/admin/ekonomi/umkm/data-umkm');
}
@@ -99,6 +173,21 @@ export default function EditDataUmkm() {
}
};
const handleResetForm = () => {
setFormData({
nama: originalData.nama,
pemilik: originalData.pemilik,
kategoriId: originalData.kategoriId,
deskripsi: originalData.deskripsi,
alamat: originalData.alamat,
kontak: originalData.kontak,
imageId: originalData.imageId,
});
setPreviewImage(originalData.imageUrl || null);
setFile(null);
toast.info("Form dikembalikan ke data awal");
};
if (isInitialLoading) {
return (
<Center h={400}>
@@ -108,69 +197,92 @@ export default function EditDataUmkm() {
}
return (
<Box>
<Group mb="lg">
<Box px={{ base: 0, md: 'lg' }} py="xs">
{/* Header */}
<Group mb="md">
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={20} />}
p="xs"
radius="md"
>
Kembali
<IconArrowBack color={colors["blue-button"]} size={24} />
</Button>
<Title order={3}>Edit Data UMKM</Title>
<Title order={4} ml="sm" c="dark">
Edit Data UMKM
</Title>
</Group>
<Paper withBorder p="xl" radius="md" shadow="sm">
<Stack gap="lg">
{/* Logo / Image UMKM */}
{/* Form */}
<Paper
w={{ base: "100%", md: "50%" }}
bg={colors["white-1"]}
p="lg"
radius="md"
shadow="sm"
style={{ border: "1px solid #e0e0e0" }}
>
<Stack gap="md">
{/* Logo / Foto 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>
<Text fw="bold" fz="sm" mb={6}>
Logo / Foto UMKM
</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0];
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile));
}
}}
onReject={() =>
toast.error("File tidak valid, gunakan format gambar")
}
maxSize={5 * 1024 ** 2}
accept={IMAGE_MIME_TYPE}
radius="md"
p="xl"
>
<Group justify="center" gap="xl" mih={140}>
<Dropzone.Accept>
<IconUpload size={48} color={colors["blue-button"]} stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={48} color="red" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={48} color="#868e96" stroke={1.5} />
</Dropzone.Idle>
</Group>
</Dropzone>
<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" />
{previewImage && (
<Box mt="sm" pos="relative" style={{ textAlign: 'center' }}>
<Image
src={previewImage}
alt="Preview Logo"
radius="md"
style={{
maxHeight: 220,
objectFit: "contain",
border: `1px solid ${colors["blue-button"]}`,
}}
loading="lazy"
/>
<ActionIcon
color="red"
variant="filled"
color="red"
radius="xl"
size="sm"
pos="absolute"
top={5}
right={5}
onClick={() => {
setPreviewImage(null);
setFile(null);
state.update.form.imageId = "";
}}
style={{ boxShadow: '0 2px 6px rgba(0,0,0,0.15)' }}
>
<IconX size={14} />
</ActionIcon>
@@ -183,15 +295,15 @@ export default function EditDataUmkm() {
label="Nama UMKM / Bisnis"
placeholder="Contoh: Warung Sate Bu Komang"
required
value={state.update.form.nama}
onChange={(e) => (state.update.form.nama = e.target.value)}
value={formData.nama}
onChange={(e) => handleChange("nama", e.target.value)}
/>
<TextInput
label="Nama Pemilik"
placeholder="Masukkan nama lengkap pemilik"
required
value={state.update.form.pemilik}
onChange={(e) => (state.update.form.pemilik = e.target.value)}
value={formData.pemilik}
onChange={(e) => handleChange("pemilik", e.target.value)}
/>
</Group>
@@ -200,42 +312,64 @@ export default function EditDataUmkm() {
label="Kategori Bisnis"
placeholder="Pilih kategori"
required
data={umkmState.kategoriProduk.findManyAll.data?.map(v => ({
data={umkmState.kategoriProduk.findManyAll.data?.map((v: any) => ({
value: v.id, label: v.nama
})) || []}
value={state.update.form.kategoriId}
onChange={(val) => (state.update.form.kategoriId = val || "")}
value={formData.kategoriId}
onChange={(val) => handleChange("kategoriId", val || "")}
/>
<TextInput
label="Nomor WA / Kontak"
placeholder="Contoh: 08123456789"
value={state.update.form.kontak}
onChange={(e) => (state.update.form.kontak = e.target.value)}
value={formData.kontak}
onChange={(e) => handleChange("kontak", e.target.value)}
/>
</Group>
<TextInput
label="Alamat Lengkap"
placeholder="Masukkan alamat fisik usaha"
value={state.update.form.alamat}
onChange={(e) => (state.update.form.alamat = e.target.value)}
value={formData.alamat}
onChange={(e) => handleChange("alamat", e.target.value)}
/>
<Box>
<Text fw={500} size="sm" mb={4}>Deskripsi UMKM</Text>
<CreateEditor
value={state.update.form.deskripsi || ""}
onChange={(val) => (state.update.form.deskripsi = val)}
<Text fw="bold" fz="sm" mb={4}>
Deskripsi UMKM
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) =>
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }))
}
/>
</Box>
<Group justify="flex-end" mt="xl">
<Button
color="blue"
onClick={handleUpdate}
loading={isSubmitting}
{/* Action Buttons */}
<Group justify="right" mt="md">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Simpan Perubahan
Batal
</Button>
<Button
onClick={handleUpdate}
radius="md"
size="md"
disabled={!isFormValid() || isSubmitting}
style={{
background: !isFormValid() || isSubmitting
? `linear-gradient(135deg, #cccccc, #eeeeee)`
: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group>
</Stack>
@@ -243,3 +377,5 @@ export default function EditDataUmkm() {
</Box>
);
}
export default EditDataUmkm;

View File

@@ -1,52 +1,112 @@
'use client';
/* eslint-disable react-hooks/exhaustive-deps */
"use client";
import EditEditor from "@/app/admin/(dashboard)/_com/editEditor";
import colors from "@/con/colors";
import ApiFetch from "@/lib/api-fetch";
import {
ActionIcon,
Box,
Button,
Group,
Image,
Paper,
Select,
Stack,
Text,
TextInput,
Title,
Text,
Select,
ActionIcon,
Image,
Loader,
NumberInput,
Center,
Loader
} from '@mantine/core';
import { Dropzone, IMAGE_MIME_TYPE } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter, useParams } 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';
} from "@mantine/core";
import { Dropzone, IMAGE_MIME_TYPE } from "@mantine/dropzone";
import {
IconArrowBack,
IconPhoto,
IconUpload,
IconX,
} from "@tabler/icons-react";
import { useParams, 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";
export default function EditProdukUmkm() {
interface ProdukData {
id: string;
nama: string;
harga: number;
stok: number;
umkmId: string | null;
deskripsi: string | null;
imageId: string | null;
kategoriId: string | null;
image?: { url: string } | null;
}
interface ProdukForm {
nama: string;
harga: number;
stok: number;
umkmId: string;
deskripsi: string;
imageId: string;
kategoriId: string;
}
function EditProdukUmkm() {
const router = useRouter();
const params = useParams();
const id = params.id as string;
const state = useProxy(umkmState.produk);
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isInitialLoading, setIsInitialLoading] = useState(true);
const [formData, setFormData] = useState<ProdukForm>({
nama: "",
harga: 0,
stok: 0,
umkmId: "",
deskripsi: "",
imageId: "",
kategoriId: "",
});
const [originalData, setOriginalData] = useState<ProdukForm & { imageUrl: string }>({
nama: "",
harga: 0,
stok: 0,
umkmId: "",
deskripsi: "",
imageId: "",
kategoriId: "",
imageUrl: ""
});
const isFormValid = () => {
return (
formData.nama?.trim() !== '' &&
formData.umkmId !== '' &&
formData.kategoriId !== '' &&
formData.harga >= 0 &&
formData.stok >= 0
);
};
useEffect(() => {
const init = async () => {
await Promise.all([
umkmState.umkm.findMany.load(1, 100),
umkmState.umkm.findMany.load(),
umkmState.kategoriProduk.findManyAll.load(),
state.findUnique.load(id)
umkmState.produk.findUnique.load(id)
]);
if (state.findUnique.data) {
const data = state.findUnique.data;
state.update.form = {
const data = umkmState.produk.findUnique.data as ProdukData | null;
if (data) {
const initialForm: ProdukForm = {
nama: data.nama || "",
harga: data.harga || 0,
stok: data.stok || 0,
@@ -54,9 +114,14 @@ export default function EditProdukUmkm() {
deskripsi: data.deskripsi || "",
imageId: data.imageId || "",
kategoriId: data.kategoriId || "",
isActive: data.isActive ?? true,
};
setFormData(initialForm);
setOriginalData({
...initialForm,
imageUrl: data.image?.url || ""
});
if (data.image?.url) {
setPreviewImage(data.image.url);
}
@@ -64,12 +129,20 @@ export default function EditProdukUmkm() {
setIsInitialLoading(false);
};
init();
}, [id, state.findUnique, state.update]);
}, [id]);
const handleChange = (field: keyof ProdukForm, value: string | number) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
const handleUpdate = async () => {
if (!formData.nama?.trim()) return toast.error("Nama produk wajib diisi");
if (!formData.umkmId) return toast.error("UMKM pemilik wajib dipilih");
if (!formData.kategoriId) return toast.error("Kategori wajib dipilih");
setIsSubmitting(true);
try {
let uploadedImageId = state.update.form.imageId;
let uploadedImageId = formData.imageId;
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({
file,
@@ -85,9 +158,14 @@ export default function EditProdukUmkm() {
}
}
state.update.form.imageId = uploadedImageId;
const success = await state.update.submit(id);
// Update proxy state
umkmState.produk.update.form = {
...umkmState.produk.update.form,
...formData,
imageId: uploadedImageId
};
const success = await umkmState.produk.update.submit(id);
if (success) {
router.push('/admin/ekonomi/umkm/produk');
}
@@ -99,6 +177,21 @@ export default function EditProdukUmkm() {
}
};
const handleResetForm = () => {
setFormData({
nama: originalData.nama,
harga: originalData.harga,
stok: originalData.stok,
umkmId: originalData.umkmId,
deskripsi: originalData.deskripsi,
imageId: originalData.imageId,
kategoriId: originalData.kategoriId,
});
setPreviewImage(originalData.imageUrl || null);
setFile(null);
toast.info("Form dikembalikan ke data awal");
};
if (isInitialLoading) {
return (
<Center h={400}>
@@ -108,57 +201,92 @@ export default function EditProdukUmkm() {
}
return (
<Box>
<Group mb="lg">
<Box px={{ base: 0, md: 'lg' }} py="xs">
{/* Header */}
<Group mb="md">
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={20} />}
p="xs"
radius="md"
>
Kembali
<IconArrowBack color={colors["blue-button"]} size={24} />
</Button>
<Title order={3}>Edit Produk UMKM</Title>
<Title order={4} ml="sm" c="dark">
Edit Produk UMKM
</Title>
</Group>
<Paper withBorder p="xl" radius="md" shadow="sm">
<Stack gap="lg">
{/* Form */}
<Paper
w={{ base: "100%", md: "50%" }}
bg={colors["white-1"]}
p="lg"
radius="md"
shadow="sm"
style={{ border: "1px solid #e0e0e0" }}
>
<Stack gap="md">
{/* Foto Produk */}
<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);
state.update.form.imageId = "";
<Text fw="bold" fz="sm" mb={6}>
Foto Produk
</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0];
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile));
}
}}
onReject={() =>
toast.error("File tidak valid, gunakan format gambar")
}
maxSize={5 * 1024 ** 2}
accept={IMAGE_MIME_TYPE}
radius="md"
p="xl"
>
<Group justify="center" gap="xl" mih={140}>
<Dropzone.Accept>
<IconUpload size={48} color={colors["blue-button"]} stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={48} color="red" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={48} color="#868e96" stroke={1.5} />
</Dropzone.Idle>
</Group>
</Dropzone>
{previewImage && (
<Box mt="sm" pos="relative" style={{ textAlign: 'center' }}>
<Image
src={previewImage}
alt="Preview Produk"
radius="md"
style={{
maxHeight: 220,
objectFit: "contain",
border: `1px solid ${colors["blue-button"]}`,
}}
loading="lazy"
/>
<ActionIcon
variant="filled"
color="red"
radius="xl"
size="sm"
pos="absolute"
top={5}
right={5}
onClick={() => {
setPreviewImage(null);
setFile(null);
}}
style={{ boxShadow: '0 2px 6px rgba(0,0,0,0.15)' }}
>
<IconX size={14} />
</ActionIcon>
@@ -166,26 +294,29 @@ export default function EditProdukUmkm() {
)}
</Box>
{/* UMKM Pemilik */}
<Select
label="Pilih UMKM Pemilik"
placeholder="Siapa pemilik produk ini?"
required
searchable
data={umkmState.umkm.findMany.data?.map(v => ({
data={umkmState.umkm.findMany.data?.map((v: any) => ({
value: v.id, label: v.nama
})) || []}
value={state.update.form.umkmId}
onChange={(val) => (state.update.form.umkmId = val || "")}
value={formData.umkmId}
onChange={(val) => handleChange("umkmId", val || "")}
/>
{/* Nama Produk */}
<TextInput
label="Nama Produk"
placeholder="Contoh: Kripik Singkong Pedas"
placeholder="Masukkan nama produk"
value={formData.nama}
onChange={(e) => handleChange("nama", e.target.value)}
required
value={state.update.form.nama}
onChange={(e) => (state.update.form.nama = e.target.value)}
/>
{/* Harga & Stok */}
<Group grow>
<NumberInput
label="Harga Produk (Rp)"
@@ -194,43 +325,75 @@ export default function EditProdukUmkm() {
min={0}
thousandSeparator="."
decimalSeparator=","
value={state.update.form.harga}
onChange={(val) => (state.update.form.harga = Number(val))}
value={formData.harga}
onChange={(val) => handleChange("harga", Number(val))}
/>
<NumberInput
label="Stok"
placeholder="0"
required
min={0}
value={state.update.form.stok}
onChange={(val) => (state.update.form.stok = Number(val))}
value={formData.stok}
onChange={(val) => handleChange("stok", Number(val))}
/>
</Group>
{/* Kategori */}
<Select
label="Kategori Produk"
placeholder="Pilih kategori produk"
required
data={umkmState.kategoriProduk.findManyAll.data?.map(v => ({
data={umkmState.kategoriProduk.findManyAll.data?.map((v: any) => ({
value: v.id, label: v.nama
})) || []}
value={state.update.form.kategoriId}
onChange={(val) => (state.update.form.kategoriId = val || "")}
value={formData.kategoriId}
onChange={(val) => handleChange("kategoriId", val || "")}
/>
{/* Deskripsi */}
<Box>
<Text fw={500} size="sm" mb={4}>Deskripsi Produk</Text>
<CreateEditor
value={state.update.form.deskripsi || ""}
onChange={(val) => (state.update.form.deskripsi = val)}
<Text fz="sm" fw="bold" mb={4}>
Deskripsi Produk
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) =>
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }))
}
/>
</Box>
<Group justify="flex-end" mt="xl">
<Button color="blue" onClick={handleUpdate} loading={isSubmitting}>Simpan Perubahan</Button>
{/* Action Buttons */}
<Group justify="right" mt="md">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
<Button
onClick={handleUpdate}
radius="md"
size="md"
disabled={!isFormValid() || isSubmitting}
style={{
background: !isFormValid() || isSubmitting
? `linear-gradient(135deg, #cccccc, #eeeeee)`
: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditProdukUmkm;

View File

@@ -9,6 +9,7 @@ import DemografiPekerjaan from "./demografi-pekerjaan";
import JumlahPengangguran from "./jumlah-pengangguran";
import PendapatanAsliDesa from "./pendapatan-asli-desa";
import StrukturOrganisasi from "./struktur-bumdes";
import KategoriProduk from "./kategori-produk";
import Umkm from "./umkm";
import ProdukUmkm from "./umkm/produk";
import PenjualanProduk from "./umkm/penjualan";
@@ -21,6 +22,7 @@ const Ekonomi = new Elysia({
.use(LowonganKerja)
.use(ProgramKemiskinan)
.use(StrukturOrganisasi)
.use(KategoriProduk)
.use(Umkm)
.use(ProdukUmkm)
.use(PenjualanProduk)

View File

@@ -0,0 +1,31 @@
import prisma from "@/lib/prisma";
import Elysia from "elysia";
const KategoriProduk = new Elysia({
prefix: "/kategoriproduk",
})
.get("/find-many-all", async () => {
try {
const data = await prisma.kategoriProduk.findMany({
where: {
isActive: true,
deletedAt: null,
},
orderBy: { nama: 'asc' },
});
return {
success: true,
message: "Berhasil mengambil semua kategori produk",
data,
};
} catch (e) {
console.error("Error di KategoriProduk find-many-all:", e);
return {
success: false,
message: "Gagal mengambil data kategori produk",
};
}
});
export default KategoriProduk;