diff --git a/MIND/PLAN/refactor-umkm-edit-pages-pattern.md b/MIND/PLAN/refactor-umkm-edit-pages-pattern.md new file mode 100644 index 00000000..dbb16198 --- /dev/null +++ b/MIND/PLAN/refactor-umkm-edit-pages-pattern.md @@ -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 +- [x] Refactor Data UMKM edit page +- [ ] Run build and fix any errors +- [ ] Update version in package.json +- [ ] Commit and push to task branch +- [ ] Merge to stg branch diff --git a/MIND/PLAN/task-refactor-umkm-edit-pages-pattern.md b/MIND/PLAN/task-refactor-umkm-edit-pages-pattern.md new file mode 100644 index 00000000..c6aaf3b8 --- /dev/null +++ b/MIND/PLAN/task-refactor-umkm-edit-pages-pattern.md @@ -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`. +3. [x] Implement the pattern in `ekonomi/umkm/data-umkm/[id]/edit/page.tsx`. +4. [ ] Run `bun run build` to verify. +5. [ ] Update `package.json` version. +6. [ ] Commit with message: "feat(admin): refactor UMKM edit pages to match berita pattern". +7. [ ] Create summary in `MIND/SUMMARY/refactor-umkm-edit-pages-pattern-summary.md`. diff --git a/MIND/SUMMARY/refactor-umkm-edit-pages-pattern-summary.md b/MIND/SUMMARY/refactor-umkm-edit-pages-pattern-summary.md new file mode 100644 index 00000000..88a45161 --- /dev/null +++ b/MIND/SUMMARY/refactor-umkm-edit-pages-pattern-summary.md @@ -0,0 +1,13 @@ +# 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 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 improvements. +3. **UI Consistency**: Standardized colors and component usage across UMKM edit pages. +4. **UX Improvement**: Added a "Batal" button that resets the form to its original data state. +5. **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. diff --git a/package.json b/package.json index 8d615e4a..eb8498cf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "desa-darmasaba", - "version": "0.1.21", + "version": "0.1.22", "private": true, "scripts": { "dev": "next dev", diff --git a/src/app/admin/(dashboard)/ekonomi/umkm/data-umkm/[id]/edit/page.tsx b/src/app/admin/(dashboard)/ekonomi/umkm/data-umkm/[id]/edit/page.tsx index a1e11d8f..df2f05eb 100644 --- a/src/app/admin/(dashboard)/ekonomi/umkm/data-umkm/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/umkm/data-umkm/[id]/edit/page.tsx @@ -1,30 +1,38 @@ -'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() { +function EditDataUmkm() { const router = useRouter(); const params = useParams(); const id = params.id as string; @@ -35,16 +43,45 @@ export default function EditDataUmkm() { const [isSubmitting, setIsSubmitting] = useState(false); const [isInitialLoading, setIsInitialLoading] = useState(true); + const [formData, setFormData] = useState({ + nama: "", + pemilik: "", + kategoriId: "", + deskripsi: "", + alamat: "", + kontak: "", + imageId: "", + }); + + const [originalData, setOriginalData] = useState({ + 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; + if (data) { + const initialForm = { nama: data.nama || "", pemilik: data.pemilik || "", kategoriId: data.kategoriId || "", @@ -52,9 +89,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 +104,20 @@ export default function EditDataUmkm() { setIsInitialLoading(false); }; init(); - }, [id, state.findUnique, state.update]); + }, [id]); + + const handleChange = (field: string, 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 +133,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 +152,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 (
@@ -108,69 +176,92 @@ export default function EditDataUmkm() { } return ( - - + + {/* Header */} + - Edit Data UMKM + + Edit Data UMKM + - - - {/* Logo / Image UMKM */} + {/* Form */} + + + {/* Logo / Foto 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" - > - - - - - - - - - - + + Logo / Foto UMKM + + { + 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" + > + + + + + + + + + + + + - - - Klik atau tarik gambar di sini - - - Maksimal 3MB - - - - - ) : ( - - Preview + {previewImage && ( + + Preview Logo { setPreviewImage(null); setFile(null); - state.update.form.imageId = ""; }} + style={{ boxShadow: '0 2px 6px rgba(0,0,0,0.15)' }} > @@ -183,15 +274,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)} /> (state.update.form.pemilik = e.target.value)} + value={formData.pemilik} + onChange={(e) => handleChange("pemilik", e.target.value)} /> @@ -203,39 +294,61 @@ export default function EditDataUmkm() { data={umkmState.kategoriProduk.findManyAll.data?.map(v => ({ 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 || "")} /> (state.update.form.kontak = e.target.value)} + value={formData.kontak} + onChange={(e) => handleChange("kontak", e.target.value)} /> (state.update.form.alamat = e.target.value)} + value={formData.alamat} + onChange={(e) => handleChange("alamat", e.target.value)} /> - Deskripsi UMKM - (state.update.form.deskripsi = val)} + + Deskripsi UMKM + + + setFormData((prev) => ({ ...prev, deskripsi: htmlContent })) + } /> - - + @@ -243,3 +356,5 @@ export default function EditDataUmkm() { ); } + +export default EditDataUmkm; diff --git a/src/app/admin/(dashboard)/ekonomi/umkm/produk/[id]/edit/page.tsx b/src/app/admin/(dashboard)/ekonomi/umkm/produk/[id]/edit/page.tsx index ca109def..bd3cac66 100644 --- a/src/app/admin/(dashboard)/ekonomi/umkm/produk/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/umkm/produk/[id]/edit/page.tsx @@ -1,31 +1,39 @@ -'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() { +function EditProdukUmkm() { const router = useRouter(); const params = useParams(); const id = params.id as string; @@ -36,17 +44,53 @@ export default function EditProdukUmkm() { const [isSubmitting, setIsSubmitting] = useState(false); const [isInitialLoading, setIsInitialLoading] = useState(true); + const [formData, setFormData] = useState({ + nama: "", + harga: 0, + stok: 0, + umkmId: "", + deskripsi: "", + imageId: "", + kategoriId: "", + }); + + const [originalData, setOriginalData] = useState({ + nama: "", + harga: 0, + stok: 0, + umkmId: "", + deskripsi: "", + imageId: "", + kategoriId: "", + imageUrl: "" + }); + + const isHtmlEmpty = (html: string) => { + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + 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; + if (data) { + const initialForm = { nama: data.nama || "", harga: data.harga || 0, stok: data.stok || 0, @@ -54,9 +98,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 +113,20 @@ export default function EditProdukUmkm() { setIsInitialLoading(false); }; init(); - }, [id, state.findUnique, state.update]); + }, [id]); + + const handleChange = (field: string, value: any) => { + 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 +142,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 +161,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 (
@@ -108,57 +185,92 @@ export default function EditProdukUmkm() { } return ( - - + + {/* Header */} + - Edit Produk UMKM + + Edit Produk UMKM + - - + {/* Form */} + + + {/* Foto Produk */} - Foto Produk - {!previewImage ? ( - { - const file = files[0]; - setFile(file); - setPreviewImage(URL.createObjectURL(file)); - }} - maxSize={3 * 1024 ** 2} - accept={IMAGE_MIME_TYPE} - radius="md" - > - - - - - - Pilih gambar produk - Maksimal 3MB - - - - ) : ( - - Preview - { - setPreviewImage(null); - setFile(null); - state.update.form.imageId = ""; + + Foto Produk + + { + 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" + > + + + + + + + + + + + + + + {previewImage && ( + + Preview Produk + { + setPreviewImage(null); + setFile(null); + }} + style={{ boxShadow: '0 2px 6px rgba(0,0,0,0.15)' }} > @@ -166,6 +278,7 @@ export default function EditProdukUmkm() { )} + {/* UMKM Pemilik */} ({ 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 */} - Deskripsi Produk - (state.update.form.deskripsi = val)} + + Deskripsi Produk + + + setFormData((prev) => ({ ...prev, deskripsi: htmlContent })) + } /> - - + {/* Action Buttons */} + + + ); } + +export default EditProdukUmkm;