feat(admin): refactor UMKM edit pages to match berita pattern

This commit is contained in:
2026-04-24 14:20:40 +08:00
parent 30fbed73c9
commit 7f5588f69e
6 changed files with 490 additions and 181 deletions

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
- [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

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`.
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`.

View File

@@ -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.

View File

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

View File

@@ -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 { import {
ActionIcon,
Box, Box,
Button, Button,
Group, Group,
Image,
Paper, Paper,
Select,
Stack, Stack,
Text,
TextInput, TextInput,
Title, Title,
Text,
Select,
ActionIcon,
Image,
Loader, Loader,
Center Center,
} from '@mantine/core'; } from "@mantine/core";
import { Dropzone, IMAGE_MIME_TYPE } from '@mantine/dropzone'; import { Dropzone, IMAGE_MIME_TYPE } from "@mantine/dropzone";
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import {
import { useRouter, useParams } from 'next/navigation'; IconArrowBack,
import { useEffect, useState } from 'react'; IconPhoto,
import { toast } from 'react-toastify'; IconUpload,
import { useProxy } from 'valtio/utils'; IconX,
import umkmState from '../../../../../_state/ekonomi/umkm/umkm'; } from "@tabler/icons-react";
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor'; import { useParams, useRouter } from "next/navigation";
import ApiFetch from '@/lib/api-fetch'; 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 router = useRouter();
const params = useParams(); const params = useParams();
const id = params.id as string; const id = params.id as string;
@@ -35,16 +43,45 @@ export default function EditDataUmkm() {
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const [isInitialLoading, setIsInitialLoading] = useState(true); 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(() => { useEffect(() => {
const init = async () => { const init = async () => {
await Promise.all([ await Promise.all([
umkmState.kategoriProduk.findManyAll.load(), umkmState.kategoriProduk.findManyAll.load(),
state.findUnique.load(id) umkmState.umkm.findUnique.load(id)
]); ]);
if (state.findUnique.data) { const data = umkmState.umkm.findUnique.data;
const data = state.findUnique.data; if (data) {
state.update.form = { const initialForm = {
nama: data.nama || "", nama: data.nama || "",
pemilik: data.pemilik || "", pemilik: data.pemilik || "",
kategoriId: data.kategoriId || "", kategoriId: data.kategoriId || "",
@@ -52,9 +89,14 @@ export default function EditDataUmkm() {
alamat: data.alamat || "", alamat: data.alamat || "",
kontak: data.kontak || "", kontak: data.kontak || "",
imageId: data.imageId || "", imageId: data.imageId || "",
isActive: data.isActive ?? true,
}; };
setFormData(initialForm);
setOriginalData({
...initialForm,
imageUrl: data.image?.url || ""
});
if (data.image?.url) { if (data.image?.url) {
setPreviewImage(data.image.url); setPreviewImage(data.image.url);
} }
@@ -62,13 +104,20 @@ export default function EditDataUmkm() {
setIsInitialLoading(false); setIsInitialLoading(false);
}; };
init(); init();
}, [id, state.findUnique, state.update]); }, [id]);
const handleChange = (field: string, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
const handleUpdate = async () => { 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); setIsSubmitting(true);
try { try {
// 1. Upload image if new file selected let uploadedImageId = formData.imageId;
let uploadedImageId = state.update.form.imageId;
if (file) { if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ const res = await ApiFetch.api.fileStorage.create.post({
file, file,
@@ -84,10 +133,14 @@ export default function EditDataUmkm() {
} }
} }
// 2. Submit UMKM data // Update proxy state
state.update.form.imageId = uploadedImageId; umkmState.umkm.update.form = {
const success = await state.update.submit(id); ...umkmState.umkm.update.form,
...formData,
imageId: uploadedImageId
};
const success = await umkmState.umkm.update.submit(id);
if (success) { if (success) {
router.push('/admin/ekonomi/umkm/data-umkm'); 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) { if (isInitialLoading) {
return ( return (
<Center h={400}> <Center h={400}>
@@ -108,69 +176,92 @@ export default function EditDataUmkm() {
} }
return ( return (
<Box> <Box px={{ base: 0, md: 'lg' }} py="xs">
<Group mb="lg"> {/* Header */}
<Group mb="md">
<Button <Button
variant="subtle" variant="subtle"
onClick={() => router.back()} onClick={() => router.back()}
leftSection={<IconArrowBack size={20} />} p="xs"
radius="md"
> >
Kembali <IconArrowBack color={colors["blue-button"]} size={24} />
</Button> </Button>
<Title order={3}>Edit Data UMKM</Title> <Title order={4} ml="sm" c="dark">
Edit Data UMKM
</Title>
</Group> </Group>
<Paper withBorder p="xl" radius="md" shadow="sm"> {/* Form */}
<Stack gap="lg"> <Paper
{/* Logo / Image UMKM */} 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> <Box>
<Text fw={500} size="sm" mb={4}>Logo / Foto UMKM</Text> <Text fw="bold" fz="sm" mb={6}>
{!previewImage ? ( Logo / Foto UMKM
<Dropzone </Text>
onDrop={(files) => { <Dropzone
const file = files[0]; onDrop={(files) => {
setFile(file); const selectedFile = files[0];
setPreviewImage(URL.createObjectURL(file)); if (selectedFile) {
}} setFile(selectedFile);
maxSize={3 * 1024 ** 2} setPreviewImage(URL.createObjectURL(selectedFile));
accept={IMAGE_MIME_TYPE} }
radius="md" }}
> onReject={() =>
<Group justify="center" gap="xl" mih={120} style={{ pointerEvents: 'none' }}> toast.error("File tidak valid, gunakan format gambar")
<Dropzone.Accept> }
<IconUpload size={42} stroke={1.5} /> maxSize={5 * 1024 ** 2}
</Dropzone.Accept> accept={IMAGE_MIME_TYPE}
<Dropzone.Reject> radius="md"
<IconX size={42} stroke={1.5} /> p="xl"
</Dropzone.Reject> >
<Dropzone.Idle> <Group justify="center" gap="xl" mih={140}>
<IconPhoto size={42} stroke={1.5} /> <Dropzone.Accept>
</Dropzone.Idle> <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> {previewImage && (
<Text size="xl" inline> <Box mt="sm" pos="relative" style={{ textAlign: 'center' }}>
Klik atau tarik gambar di sini <Image
</Text> src={previewImage}
<Text size="sm" c="dimmed" inline mt={7}> alt="Preview Logo"
Maksimal 3MB radius="md"
</Text> style={{
</Box> maxHeight: 220,
</Group> objectFit: "contain",
</Dropzone> border: `1px solid ${colors["blue-button"]}`,
) : ( }}
<Box pos="relative" w="fit-content"> loading="lazy"
<Image src={previewImage} h={200} radius="md" alt="Preview" /> />
<ActionIcon <ActionIcon
color="red"
variant="filled" variant="filled"
color="red"
radius="xl"
size="sm"
pos="absolute" pos="absolute"
top={5} top={5}
right={5} right={5}
onClick={() => { onClick={() => {
setPreviewImage(null); setPreviewImage(null);
setFile(null); setFile(null);
state.update.form.imageId = "";
}} }}
style={{ boxShadow: '0 2px 6px rgba(0,0,0,0.15)' }}
> >
<IconX size={14} /> <IconX size={14} />
</ActionIcon> </ActionIcon>
@@ -183,15 +274,15 @@ export default function EditDataUmkm() {
label="Nama UMKM / Bisnis" label="Nama UMKM / Bisnis"
placeholder="Contoh: Warung Sate Bu Komang" placeholder="Contoh: Warung Sate Bu Komang"
required required
value={state.update.form.nama} value={formData.nama}
onChange={(e) => (state.update.form.nama = e.target.value)} onChange={(e) => handleChange("nama", e.target.value)}
/> />
<TextInput <TextInput
label="Nama Pemilik" label="Nama Pemilik"
placeholder="Masukkan nama lengkap pemilik" placeholder="Masukkan nama lengkap pemilik"
required required
value={state.update.form.pemilik} value={formData.pemilik}
onChange={(e) => (state.update.form.pemilik = e.target.value)} onChange={(e) => handleChange("pemilik", e.target.value)}
/> />
</Group> </Group>
@@ -203,39 +294,61 @@ export default function EditDataUmkm() {
data={umkmState.kategoriProduk.findManyAll.data?.map(v => ({ data={umkmState.kategoriProduk.findManyAll.data?.map(v => ({
value: v.id, label: v.nama value: v.id, label: v.nama
})) || []} })) || []}
value={state.update.form.kategoriId} value={formData.kategoriId}
onChange={(val) => (state.update.form.kategoriId = val || "")} onChange={(val) => handleChange("kategoriId", val || "")}
/> />
<TextInput <TextInput
label="Nomor WA / Kontak" label="Nomor WA / Kontak"
placeholder="Contoh: 08123456789" placeholder="Contoh: 08123456789"
value={state.update.form.kontak} value={formData.kontak}
onChange={(e) => (state.update.form.kontak = e.target.value)} onChange={(e) => handleChange("kontak", e.target.value)}
/> />
</Group> </Group>
<TextInput <TextInput
label="Alamat Lengkap" label="Alamat Lengkap"
placeholder="Masukkan alamat fisik usaha" placeholder="Masukkan alamat fisik usaha"
value={state.update.form.alamat} value={formData.alamat}
onChange={(e) => (state.update.form.alamat = e.target.value)} onChange={(e) => handleChange("alamat", e.target.value)}
/> />
<Box> <Box>
<Text fw={500} size="sm" mb={4}>Deskripsi UMKM</Text> <Text fw="bold" fz="sm" mb={4}>
<CreateEditor Deskripsi UMKM
value={state.update.form.deskripsi || ""} </Text>
onChange={(val) => (state.update.form.deskripsi = val)} <EditEditor
value={formData.deskripsi}
onChange={(htmlContent) =>
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }))
}
/> />
</Box> </Box>
<Group justify="flex-end" mt="xl"> {/* Action Buttons */}
<Button <Group justify="right" mt="md">
color="blue" <Button
onClick={handleUpdate} variant="outline"
loading={isSubmitting} 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> </Button>
</Group> </Group>
</Stack> </Stack>
@@ -243,3 +356,5 @@ export default function EditDataUmkm() {
</Box> </Box>
); );
} }
export default EditDataUmkm;

View File

@@ -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 { import {
ActionIcon,
Box, Box,
Button, Button,
Group, Group,
Image,
Paper, Paper,
Select,
Stack, Stack,
Text,
TextInput, TextInput,
Title, Title,
Text, Loader,
Select,
ActionIcon,
Image,
NumberInput, NumberInput,
Center, Center,
Loader } from "@mantine/core";
} from '@mantine/core'; import { Dropzone, IMAGE_MIME_TYPE } from "@mantine/dropzone";
import { Dropzone, IMAGE_MIME_TYPE } from '@mantine/dropzone'; import {
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; IconArrowBack,
import { useRouter, useParams } from 'next/navigation'; IconPhoto,
import { useEffect, useState } from 'react'; IconUpload,
import { toast } from 'react-toastify'; IconX,
import { useProxy } from 'valtio/utils'; } from "@tabler/icons-react";
import umkmState from '../../../../../_state/ekonomi/umkm/umkm'; import { useParams, useRouter } from "next/navigation";
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor'; import { useEffect, useState } from "react";
import ApiFetch from '@/lib/api-fetch'; 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 router = useRouter();
const params = useParams(); const params = useParams();
const id = params.id as string; const id = params.id as string;
@@ -36,17 +44,53 @@ export default function EditProdukUmkm() {
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const [isInitialLoading, setIsInitialLoading] = useState(true); 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(() => { useEffect(() => {
const init = async () => { const init = async () => {
await Promise.all([ await Promise.all([
umkmState.umkm.findMany.load(1, 100), umkmState.umkm.findMany.load(),
umkmState.kategoriProduk.findManyAll.load(), umkmState.kategoriProduk.findManyAll.load(),
state.findUnique.load(id) umkmState.produk.findUnique.load(id)
]); ]);
if (state.findUnique.data) { const data = umkmState.produk.findUnique.data;
const data = state.findUnique.data; if (data) {
state.update.form = { const initialForm = {
nama: data.nama || "", nama: data.nama || "",
harga: data.harga || 0, harga: data.harga || 0,
stok: data.stok || 0, stok: data.stok || 0,
@@ -54,9 +98,14 @@ export default function EditProdukUmkm() {
deskripsi: data.deskripsi || "", deskripsi: data.deskripsi || "",
imageId: data.imageId || "", imageId: data.imageId || "",
kategoriId: data.kategoriId || "", kategoriId: data.kategoriId || "",
isActive: data.isActive ?? true,
}; };
setFormData(initialForm);
setOriginalData({
...initialForm,
imageUrl: data.image?.url || ""
});
if (data.image?.url) { if (data.image?.url) {
setPreviewImage(data.image.url); setPreviewImage(data.image.url);
} }
@@ -64,12 +113,20 @@ export default function EditProdukUmkm() {
setIsInitialLoading(false); setIsInitialLoading(false);
}; };
init(); init();
}, [id, state.findUnique, state.update]); }, [id]);
const handleChange = (field: string, value: any) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
const handleUpdate = async () => { 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); setIsSubmitting(true);
try { try {
let uploadedImageId = state.update.form.imageId; let uploadedImageId = formData.imageId;
if (file) { if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ const res = await ApiFetch.api.fileStorage.create.post({
file, file,
@@ -85,9 +142,14 @@ export default function EditProdukUmkm() {
} }
} }
state.update.form.imageId = uploadedImageId; // Update proxy state
const success = await state.update.submit(id); umkmState.produk.update.form = {
...umkmState.produk.update.form,
...formData,
imageId: uploadedImageId
};
const success = await umkmState.produk.update.submit(id);
if (success) { if (success) {
router.push('/admin/ekonomi/umkm/produk'); 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) { if (isInitialLoading) {
return ( return (
<Center h={400}> <Center h={400}>
@@ -108,57 +185,92 @@ export default function EditProdukUmkm() {
} }
return ( return (
<Box> <Box px={{ base: 0, md: 'lg' }} py="xs">
<Group mb="lg"> {/* Header */}
<Group mb="md">
<Button <Button
variant="subtle" variant="subtle"
onClick={() => router.back()} onClick={() => router.back()}
leftSection={<IconArrowBack size={20} />} p="xs"
radius="md"
> >
Kembali <IconArrowBack color={colors["blue-button"]} size={24} />
</Button> </Button>
<Title order={3}>Edit Produk UMKM</Title> <Title order={4} ml="sm" c="dark">
Edit Produk UMKM
</Title>
</Group> </Group>
<Paper withBorder p="xl" radius="md" shadow="sm"> {/* Form */}
<Stack gap="lg"> <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> <Box>
<Text fw={500} size="sm" mb={4}>Foto Produk</Text> <Text fw="bold" fz="sm" mb={6}>
{!previewImage ? ( Foto Produk
<Dropzone </Text>
onDrop={(files) => { <Dropzone
const file = files[0]; onDrop={(files) => {
setFile(file); const selectedFile = files[0];
setPreviewImage(URL.createObjectURL(file)); if (selectedFile) {
}} setFile(selectedFile);
maxSize={3 * 1024 ** 2} setPreviewImage(URL.createObjectURL(selectedFile));
accept={IMAGE_MIME_TYPE} }
radius="md" }}
> onReject={() =>
<Group justify="center" gap="xl" mih={120} style={{ pointerEvents: 'none' }}> toast.error("File tidak valid, gunakan format gambar")
<Dropzone.Idle> }
<IconPhoto size={42} stroke={1.5} /> maxSize={5 * 1024 ** 2}
</Dropzone.Idle> accept={IMAGE_MIME_TYPE}
<Box> radius="md"
<Text size="xl" inline>Pilih gambar produk</Text> p="xl"
<Text size="sm" c="dimmed" inline mt={7}>Maksimal 3MB</Text> >
</Box> <Group justify="center" gap="xl" mih={140}>
</Group> <Dropzone.Accept>
</Dropzone> <IconUpload size={48} color={colors["blue-button"]} stroke={1.5} />
) : ( </Dropzone.Accept>
<Box pos="relative" w="fit-content"> <Dropzone.Reject>
<Image src={previewImage} h={200} radius="md" alt="Preview" /> <IconX size={48} color="red" stroke={1.5} />
<ActionIcon </Dropzone.Reject>
color="red" <Dropzone.Idle>
variant="filled" <IconPhoto size={48} color="#868e96" stroke={1.5} />
pos="absolute" </Dropzone.Idle>
top={5} </Group>
right={5} </Dropzone>
onClick={() => {
setPreviewImage(null); {previewImage && (
setFile(null); <Box mt="sm" pos="relative" style={{ textAlign: 'center' }}>
state.update.form.imageId = ""; <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} /> <IconX size={14} />
</ActionIcon> </ActionIcon>
@@ -166,6 +278,7 @@ export default function EditProdukUmkm() {
)} )}
</Box> </Box>
{/* UMKM Pemilik */}
<Select <Select
label="Pilih UMKM Pemilik" label="Pilih UMKM Pemilik"
placeholder="Siapa pemilik produk ini?" placeholder="Siapa pemilik produk ini?"
@@ -174,18 +287,20 @@ export default function EditProdukUmkm() {
data={umkmState.umkm.findMany.data?.map(v => ({ data={umkmState.umkm.findMany.data?.map(v => ({
value: v.id, label: v.nama value: v.id, label: v.nama
})) || []} })) || []}
value={state.update.form.umkmId} value={formData.umkmId}
onChange={(val) => (state.update.form.umkmId = val || "")} onChange={(val) => handleChange("umkmId", val || "")}
/> />
{/* Nama Produk */}
<TextInput <TextInput
label="Nama Produk" label="Nama Produk"
placeholder="Contoh: Kripik Singkong Pedas" placeholder="Masukkan nama produk"
value={formData.nama}
onChange={(e) => handleChange("nama", e.target.value)}
required required
value={state.update.form.nama}
onChange={(e) => (state.update.form.nama = e.target.value)}
/> />
{/* Harga & Stok */}
<Group grow> <Group grow>
<NumberInput <NumberInput
label="Harga Produk (Rp)" label="Harga Produk (Rp)"
@@ -194,19 +309,20 @@ export default function EditProdukUmkm() {
min={0} min={0}
thousandSeparator="." thousandSeparator="."
decimalSeparator="," decimalSeparator=","
value={state.update.form.harga} value={formData.harga}
onChange={(val) => (state.update.form.harga = Number(val))} onChange={(val) => handleChange("harga", Number(val))}
/> />
<NumberInput <NumberInput
label="Stok" label="Stok"
placeholder="0" placeholder="0"
required required
min={0} min={0}
value={state.update.form.stok} value={formData.stok}
onChange={(val) => (state.update.form.stok = Number(val))} onChange={(val) => handleChange("stok", Number(val))}
/> />
</Group> </Group>
{/* Kategori */}
<Select <Select
label="Kategori Produk" label="Kategori Produk"
placeholder="Pilih kategori produk" placeholder="Pilih kategori produk"
@@ -214,23 +330,54 @@ export default function EditProdukUmkm() {
data={umkmState.kategoriProduk.findManyAll.data?.map(v => ({ data={umkmState.kategoriProduk.findManyAll.data?.map(v => ({
value: v.id, label: v.nama value: v.id, label: v.nama
})) || []} })) || []}
value={state.update.form.kategoriId} value={formData.kategoriId}
onChange={(val) => (state.update.form.kategoriId = val || "")} onChange={(val) => handleChange("kategoriId", val || "")}
/> />
{/* Deskripsi */}
<Box> <Box>
<Text fw={500} size="sm" mb={4}>Deskripsi Produk</Text> <Text fz="sm" fw="bold" mb={4}>
<CreateEditor Deskripsi Produk
value={state.update.form.deskripsi || ""} </Text>
onChange={(val) => (state.update.form.deskripsi = val)} <EditEditor
value={formData.deskripsi}
onChange={(htmlContent) =>
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }))
}
/> />
</Box> </Box>
<Group justify="flex-end" mt="xl"> {/* Action Buttons */}
<Button color="blue" onClick={handleUpdate} loading={isSubmitting}>Simpan Perubahan</Button> <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> </Group>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>
); );
} }
export default EditProdukUmkm;