diff --git a/package.json b/package.json index c25cb87d..c32725d5 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,9 @@ "version": "0.1.5", "private": true, "scripts": { - "dev": "bun --bun next dev", - "build": "bun --bun next build", - "start": "bun --bun next start" + "dev": "next dev", + "build": "next build", + "start": "next start" }, "prisma": { "seed": "bun run prisma/seed.ts" diff --git a/src/app/admin/(dashboard)/_com/createEditor.tsx b/src/app/admin/(dashboard)/_com/createEditor.tsx index 7878e59a..86cf608c 100644 --- a/src/app/admin/(dashboard)/_com/createEditor.tsx +++ b/src/app/admin/(dashboard)/_com/createEditor.tsx @@ -7,6 +7,7 @@ import Underline from '@tiptap/extension-underline'; import TextAlign from '@tiptap/extension-text-align'; import Superscript from '@tiptap/extension-superscript'; import SubScript from '@tiptap/extension-subscript'; +import { useEffect } from 'react'; type CreateEditorProps = { value: string; @@ -32,6 +33,13 @@ export default function CreateEditor({ value, onChange }: CreateEditorProps) { }, }); + // 👇 Tambahkan efek untuk sinkronisasi value dari luar (resetForm) + useEffect(() => { + if (editor && value !== editor.getHTML()) { + editor.commands.setContent(value || ''); + } + }, [value, editor]); + return ( diff --git a/src/app/admin/(dashboard)/_com/editEditor.tsx b/src/app/admin/(dashboard)/_com/editEditor.tsx index 66317cc2..004a0dc0 100644 --- a/src/app/admin/(dashboard)/_com/editEditor.tsx +++ b/src/app/admin/(dashboard)/_com/editEditor.tsx @@ -47,6 +47,7 @@ export default function EditEditor({ value, onChange }: EditEditorProps) { editor.off('update', updateHandler); }; }, [editor, onChange]); + return ( diff --git a/src/app/admin/(dashboard)/_com/selectIconEdit.tsx b/src/app/admin/(dashboard)/_com/selectIconEdit.tsx index 093a41df..c221672f 100644 --- a/src/app/admin/(dashboard)/_com/selectIconEdit.tsx +++ b/src/app/admin/(dashboard)/_com/selectIconEdit.tsx @@ -1,6 +1,6 @@ -'use client' +'use client'; -import { Box, rem, Select } from '@mantine/core'; +import { Box, Group, rem, Select, SelectProps } from '@mantine/core'; import { IconAmbulance, IconCash, @@ -25,7 +25,7 @@ import { IconTrophy, IconTruckFilled, IconBuilding, - IconAlertTriangle + IconAlertTriangle, } from '@tabler/icons-react'; const iconMap = { @@ -38,26 +38,26 @@ const iconMap = { scale: { label: 'Scale', icon: IconScale }, clipboard: { label: 'Clipboard', icon: IconClipboardTextFilled }, trash: { label: 'Trash', icon: IconTrashFilled }, - lingkunganSehat: {label: 'Lingkungan Sehat', icon: IconHomeEco}, - sumberOksigen: {label: 'Sumber Oksigen', icon: IconChristmasTreeFilled}, - ekonomiBerkelanjutan: {label: 'Ekonomi Berkelanjutan', icon: IconTrendingUp}, - mencegahBencana: {label: 'Mencegah Bencana', icon: IconShieldFilled}, - rumah: {label: 'Rumah', icon: IconHome}, - pohon: {label: 'Pohon', icon: IconTree}, - air: {label: 'Air', icon: IconDroplet}, - bantuan: {label: 'Bantuan', icon: IconCash}, - pelatihan: {label: 'Pelatihan', icon: IconSchool}, - subsidi: {label: 'Subsidi', icon: IconShoppingCart}, - layananKesehatan: {label: 'Layanan Kesehatan', icon: IconHospital}, - polisi: {label: 'Polisi', icon: IconShieldFilled}, - ambulans: {label: 'Ambulans', icon: IconAmbulance}, - pemadam: {label: 'Pemadam', icon: IconFiretruck}, - rumahSakit: {label: 'Rumah Sakit', icon: IconHospital}, - bangunan: {label: 'Bangunan', icon: IconBuilding}, - darurat: {label: 'Darurat', icon: IconAlertTriangle}, + lingkunganSehat: { label: 'Lingkungan Sehat', icon: IconHomeEco }, + sumberOksigen: { label: 'Sumber Oksigen', icon: IconChristmasTreeFilled }, + ekonomiBerkelanjutan: { label: 'Ekonomi Berkelanjutan', icon: IconTrendingUp }, + mencegahBencana: { label: 'Mencegah Bencana', icon: IconShieldFilled }, + rumah: { label: 'Rumah', icon: IconHome }, + pohon: { label: 'Pohon', icon: IconTree }, + air: { label: 'Air', icon: IconDroplet }, + bantuan: { label: 'Bantuan', icon: IconCash }, + pelatihan: { label: 'Pelatihan', icon: IconSchool }, + subsidi: { label: 'Subsidi', icon: IconShoppingCart }, + layananKesehatan: { label: 'Layanan Kesehatan', icon: IconHospital }, + polisi: { label: 'Polisi', icon: IconShieldFilled }, + ambulans: { label: 'Ambulans', icon: IconAmbulance }, + pemadam: { label: 'Pemadam', icon: IconFiretruck }, + rumahSakit: { label: 'Rumah Sakit', icon: IconHospital }, + bangunan: { label: 'Bangunan', icon: IconBuilding }, + darurat: { label: 'Darurat', icon: IconAlertTriangle }, }; -type IconKey = keyof typeof iconMap; +export type IconKey = keyof typeof iconMap; const iconList = Object.entries(iconMap).map(([value, data]) => ({ value, @@ -67,44 +67,52 @@ const iconList = Object.entries(iconMap).map(([value, data]) => ({ export default function SelectIconProgramEdit({ onChange, value, + ...props }: { - onChange: (value: IconKey) => void; - value: IconKey; -}) { - const IconComponent = iconMap[value]?.icon || null; - + onChange: (value: IconKey | '') => void; + value: IconKey | ''; +} & Omit) { return ( { - pengumumanState.pengumuman.create.form.categoryPengumumanId = val ?? ""; - }} data={pengumumanState.category.findMany.data?.map((item) => ({ label: item.name, value: item.id, - }))} + })) || []} + value={pengumumanState.pengumuman.create.form.categoryPengumumanId || null} + onChange={(val: string | null) => { + if (val) { + const selected = pengumumanState.category.findMany.data?.find( + (item) => item.id === val + ); + if (selected) { + pengumumanState.pengumuman.create.form.categoryPengumumanId = selected.id; + } + } else { + pengumumanState.pengumuman.create.form.categoryPengumumanId = ''; + } + }} searchable + clearable nothingFoundMessage="Tidak ditemukan" + required /> {/* Deskripsi Singkat */} (pengumumanState.pengumuman.create.form.deskripsi = val.target.value)} label="Deskripsi Singkat" placeholder="Masukkan deskripsi singkat" @@ -112,6 +135,17 @@ function CreatePengumuman() { {/* Tombol Submit */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/[id]/page.tsx b/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/[id]/page.tsx index c92b307c..4c9f4c26 100644 --- a/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/[id]/page.tsx +++ b/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/[id]/page.tsx @@ -9,7 +9,8 @@ import { Paper, Stack, TextInput, - Title + Title, + Loader } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -26,6 +27,12 @@ function EditKategoriPotensi() { nama: '', }); + const [originalData, setOriginalData] = useState({ + nama: '', + }); + + const [isSubmitting, setIsSubmitting] = useState(false); + // Load data dari backend -> isi ke formData lokal useEffect(() => { const loadKategori = async () => { @@ -38,6 +45,9 @@ function EditKategoriPotensi() { setFormData({ nama: data.nama || '', }); + setOriginalData({ + nama: data.nama || '', + }); } } catch (error) { console.error('Error loading kategori potensi:', error); @@ -55,8 +65,16 @@ function EditKategoriPotensi() { })); }; + const handleResetForm = () => { + setFormData({ + nama: originalData.nama, + }); + toast.info("Form dikembalikan ke data awal"); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); // Update global state hanya pas submit editState.update.form = { ...editState.update.form, @@ -69,6 +87,8 @@ function EditKategoriPotensi() { } catch (error) { console.error('Error updating kategori potensi:', error); toast.error('Terjadi kesalahan saat memperbarui kategori potensi'); + } finally { + setIsSubmitting(false); } }; @@ -106,6 +126,17 @@ function EditKategoriPotensi() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/create/page.tsx b/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/create/page.tsx index 19903663..5647f386 100644 --- a/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/create/page.tsx +++ b/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/create/page.tsx @@ -8,15 +8,18 @@ import { Paper, Stack, TextInput, - Title + Title, + Loader } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; +import { useState } from 'react'; import { useProxy } from 'valtio/utils'; function CreateKategoriPotensi() { const createState = useProxy(potensiDesaState.kategoriPotensi); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { createState.create.form = { @@ -25,23 +28,30 @@ function CreateKategoriPotensi() { }; const handleSubmit = async () => { - await createState.create.create(); - resetForm(); - router.push('/admin/desa/potensi/kategori-potensi'); + try { + setIsSubmitting(true); + await createState.create.create(); + resetForm(); + router.push('/admin/desa/potensi/kategori-potensi'); + } catch (error) { + console.error('Error creating kategori potensi:', error); + } finally { + setIsSubmitting(false); + } }; return ( {/* Header dengan back button */} - + Tambah Kategori Potensi @@ -60,12 +70,23 @@ function CreateKategoriPotensi() { (createState.create.form.nama = e.target.value)} required /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/desa/potensi/list-potensi/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/potensi/list-potensi/[id]/edit/page.tsx index 301133e9..bf2197e6 100644 --- a/src/app/admin/(dashboard)/desa/potensi/list-potensi/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/desa/potensi/list-potensi/[id]/edit/page.tsx @@ -15,7 +15,9 @@ import { Stack, Text, TextInput, - Title + Title, + Loader, + ActionIcon } from "@mantine/core"; import { Dropzone } from "@mantine/dropzone"; import { IconArrowBack, IconPhoto, IconUpload, IconX } from "@tabler/icons-react"; @@ -38,6 +40,16 @@ function EditPotensi() { content: "", imageId: "", }); + const [isSubmitting, setIsSubmitting] = useState(false); + + const [originalData, setOriginalData] = useState({ + name: "", + deskripsi: "", + kategoriId: "", + content: "", + imageId: "", + imageUrl: "", + }); // handle input changes const handleChange = (field: string, value: string) => { @@ -46,11 +58,11 @@ function EditPotensi() { useEffect(() => { potensiDesaState.kategoriPotensi.findMany.load(); - + const loadPotensi = async () => { const id = params?.id as string; if (!id) return; - + try { const data = await potensiState.edit.load(id); if (data) { @@ -61,35 +73,45 @@ function EditPotensi() { content: data.content || "", imageId: data.imageId || "", }); - - // // merge, bukan replace - // setFormData((prev) => ({ - // ...prev, - // name: data.name ?? prev.name, - // deskripsi: data.deskripsi ?? prev.deskripsi, - // kategoriId: data.kategoriId ?? prev.kategoriId, - // content: data.content ?? prev.content, - // imageId: data.imageId ?? prev.imageId, - // })); - - if (data?.image?.link) { - setPreviewImage(data.image.link); - } + + setOriginalData({ + name: data.name || "", + deskripsi: data.deskripsi || "", + kategoriId: data.kategoriId || "", + content: data.content || "", + imageId: data.imageId || "", + imageUrl: data.image?.link || "", + }); + setPreviewImage(data.image.link); + } } catch (error) { console.error("Error loading potensi:", error); toast.error("Gagal memuat data potensi"); } }; - + loadPotensi(); }, [params?.id]); - + + const handleResetForm = () => { + setFormData({ + name: originalData.name || "", + deskripsi: originalData.deskripsi || "", + kategoriId: originalData.kategoriId || "", + content: originalData.content || "", + imageId: originalData.imageId || "" + }); + setPreviewImage(originalData.imageUrl || null); + setFile(null); + toast.info("Form dikembalikan ke data awal"); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); let imageId = formData.imageId; - if (file) { const res = await ApiFetch.api.fileStorage.create.post({ file, @@ -115,20 +137,22 @@ function EditPotensi() { } catch (error) { console.error("Error updating potensi:", error); toast.error("Terjadi kesalahan saat memperbarui potensi"); + } finally { + setIsSubmitting(false); } }; return ( - + Edit Potensi Desa @@ -164,6 +188,32 @@ function EditPotensi() { handleChange("kategoriId", val || "")} label="Kategori" @@ -178,7 +228,7 @@ function EditPotensi() { searchable required error={!formData.kategoriId ? "Pilih kategori" : undefined} - /> + /> */} @@ -219,25 +269,45 @@ function EditPotensi() { Seret gambar atau klik untuk memilih file - Maksimal 5MB, format gambar wajib + Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp {previewImage && ( - + Preview Gambar + + {/* Tombol hapus (pojok kanan atas) */} + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -255,17 +325,29 @@ function EditPotensi() { + {/* Tombol Batal */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/desa/potensi/list-potensi/create/page.tsx b/src/app/admin/(dashboard)/desa/potensi/list-potensi/create/page.tsx index ebc0879d..d18bca1b 100644 --- a/src/app/admin/(dashboard)/desa/potensi/list-potensi/create/page.tsx +++ b/src/app/admin/(dashboard)/desa/potensi/list-potensi/create/page.tsx @@ -14,7 +14,9 @@ import { Stack, Text, TextInput, - Title + Title, + Loader, + ActionIcon } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; @@ -28,30 +30,39 @@ function CreatePotensi() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); useEffect(() => { potensiDesaState.kategoriPotensi.findMany.load(); }, []); const handleSubmit = async () => { - if (!file) return toast.warn('Pilih file gambar terlebih dahulu'); + try { + setIsSubmitting(true); + if (!file) return toast.warn('Pilih file gambar terlebih dahulu'); - const res = await ApiFetch.api.fileStorage.create.post({ - file, - name: file.name, - }); + const res = await ApiFetch.api.fileStorage.create.post({ + file, + name: file.name, + }); - const uploaded = res.data?.data; - if (!uploaded?.id) { - return toast.error('Gagal upload gambar'); + const uploaded = res.data?.data; + if (!uploaded?.id) { + return toast.error('Gagal upload gambar'); + } + + potensiState.create.form.imageId = uploaded.id; + + await potensiState.create.create(); + + resetForm(); + router.push('/admin/desa/potensi/list-potensi'); + } catch (error) { + console.error('Error creating potensi:', error); + toast.error('Terjadi kesalahan saat menambahkan potensi'); + } finally { + setIsSubmitting(false); } - - potensiState.create.form.imageId = uploaded.id; - - await potensiState.create.create(); - - resetForm(); - router.push('/admin/desa/potensi/list-potensi'); }; const resetForm = () => { @@ -71,9 +82,9 @@ function CreatePotensi() { {/* Header */} - + Tambah Potensi Desa @@ -90,7 +101,7 @@ function CreatePotensi() { {/* Judul */} (potensiState.create.form.name = val.target.value)} label="Judul" placeholder="Masukkan judul potensi" @@ -112,6 +123,32 @@ function CreatePotensi() { {/* Kategori */} + /> */} {/* Upload Gambar */} @@ -139,7 +176,7 @@ function CreatePotensi() { }} onReject={() => toast.error('File tidak valid, gunakan format gambar')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} radius="md" p="xl" > @@ -157,17 +194,44 @@ function CreatePotensi() { Seret gambar atau klik untuk memilih file (maks 5MB) + + Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp + {previewImage && ( - + Preview Gambar + + {/* Tombol hapus (pojok kanan atas) */} + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -187,6 +251,17 @@ function CreatePotensi() { {/* Tombol Simpan */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/lambang_desa/page.tsx b/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/lambang_desa/page.tsx index 4aec56e8..6d182103 100644 --- a/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/lambang_desa/page.tsx +++ b/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/lambang_desa/page.tsx @@ -1,152 +1,244 @@ -/* eslint-disable react-hooks/exhaustive-deps */ -'use client' +'use client'; import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'; import colors from '@/con/colors'; -import { Alert, Box, Button, Center, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; +import { + Alert, + Box, + Button, + Center, + Group, + Loader, + Paper, + Stack, + Text, + TextInput, + Title, +} from '@mantine/core'; import { IconAlertCircle, IconArrowBack } 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'; +// 🧩 Type untuk form +interface FormData { + judul: string; + deskripsi: string; +} + +// 🧩 Main Component function Page() { - const lambangState = useProxy(stateProfileDesa.lambangDesa) - const router = useRouter() - const params = useParams() - const [isSubmitting, setIsSubmitting] = useState(false); - // Load data - useEffect(() => { - const loadData = async () => { - const id = params?.id as string; - if (!id) { - toast.error("ID tidak valid"); - router.push("/admin/desa/profile/profile-desa"); - return; - } + const router = useRouter(); + const params = useParams(); - try { - const data = await lambangState.findUnique.load(id); - lambangState.update.initialize(data); - } catch (error) { - console.error("Error loading lambang:", error); - toast.error("Gagal memuat data lambang desa"); - } - }; + const [formData, setFormData] = useState({ judul: '', deskripsi: '' }); + const [originalData, setOriginalData] = useState({ judul: '', deskripsi: '' }); + const [isLoading, setIsLoading] = useState(true); + const [isSubmitting, setIsSubmitting] = useState(false); + const [loadError, setLoadError] = useState(null); - loadData(); + // 🧭 Load data awal + useEffect(() => { + const loadData = async () => { + const id = params?.id as string; + if (!id) { + toast.error('ID tidak valid'); + router.push('/admin/desa/profile/profile-desa'); + return; + } - return () => { - lambangState.update.reset(); - lambangState.findUnique.reset(); - }; - }, [params?.id, router]); + setIsLoading(true); + setLoadError(null); - const handleSubmit = async () => { - if (isSubmitting || !lambangState.update.form.judul.trim()) { - toast.error("Judul wajib diisi"); - return; - } - - setIsSubmitting(true); - - try { - const success = await lambangState.update.submit(); - - if (success) { - toast.success("Data berhasil disimpan"); - router.push("/admin/desa/profile/profile-desa"); - } - } catch (error) { - console.error("Error update lambang desa:", error); - toast.error("Terjadi kesalahan saat update lambang desa"); - } finally { - setIsSubmitting(false); + try { + const data = await stateProfileDesa.lambangDesa.findUnique.load(id); + + if (data) { + const initial: FormData = { + judul: data.judul || '', + deskripsi: data.deskripsi || '', + }; + setFormData(initial); + setOriginalData(initial); + + // Penting untuk isi id di state sebelum submit + stateProfileDesa.lambangDesa.update.initialize(data); + } else { + setLoadError('Data tidak ditemukan'); } + } catch (error) { + console.error('Error loading lambang:', error); + setLoadError('Gagal memuat data lambang desa'); + toast.error('Gagal memuat data lambang desa'); + } finally { + setIsLoading(false); + } }; - const handleBack = () => router.back(); + loadData(); - // Loading state - if (lambangState.findUnique.loading || lambangState.update.loading) { - return ( - -
- Memuat data... -
-
- ); + return () => { + stateProfileDesa.lambangDesa.update.reset(); + stateProfileDesa.lambangDesa.findUnique.reset(); + }; + }, [params?.id, router]); + + // 🔁 Reset form + const handleResetForm = () => { + setFormData(originalData); + toast.info('Form dikembalikan ke data awal'); + }; + + // 💾 Submit handler + const handleSubmit = async () => { + if (!formData.judul.trim()) { + toast.error('Judul wajib diisi'); + return; } - // Error state - if (lambangState.findUnique.error) { - return ( - - - - } color="red"> - Error - {lambangState.findUnique.error} - - - - ); - } + setIsSubmitting(true); + try { + const state = stateProfileDesa.lambangDesa; + state.update.form.judul = formData.judul; + state.update.form.deskripsi = formData.deskripsi; + const success = await state.update.submit(); + + if (success) { + toast.success('Data berhasil disimpan'); + router.push('/admin/desa/profile/profile-desa'); + } else { + toast.error('Gagal menyimpan data'); + } + } catch (error) { + console.error('Error update lambang desa:', error); + toast.error('Terjadi kesalahan saat update lambang desa'); + } finally { + setIsSubmitting(false); + } + }; + + // 📝 Handlers + const handleJudulChange = (e: React.ChangeEvent) => { + setFormData(prev => ({ ...prev, judul: e.target.value })); + }; + + const handleDeskripsiChange = (html: string) => { + setFormData(prev => ({ ...prev, deskripsi: html })); + }; + + const handleBack = () => router.back(); + + // 🔄 Loading + if (isLoading) { return ( - - - - - Edit Lambang Desa - - - - - Edit Lambang Desa - - {/* Judul */} - Judul} - placeholder="Judul lambang" - defaultValue={lambangState.update.form.judul} - onChange={(e) => lambangState.update.form.judul = e.currentTarget.value} - error={!lambangState.update.form.judul && "Judul wajib diisi"} - /> - - {/* Deskripsi */} - - Deskripsi - lambangState.update.form.deskripsi = val} - /> - - - {/* Buttons */} - - - - - - - - - + +
+ + + + Memuat data lambang desa... + + +
+
); + } + + // ❌ Error + if (loadError) { + return ( + + + + } color="red" title="Terjadi Kesalahan" radius="md"> + {loadError} + + + + + ); + } + + // 🧱 UI utama + return ( + + + {/* Header */} + + + + Edit Lambang Desa + + + + {/* Form */} + + + {/* Judul */} + Judul} + placeholder="Masukkan judul lambang desa" + value={formData.judul} + onChange={handleJudulChange} + error={!formData.judul.trim() && 'Judul wajib diisi'} + required + size="md" + radius="md" + /> + + {/* Deskripsi */} + + + Deskripsi + + + + + {/* Tombol Aksi */} + + + + + + + + + ); } export default Page; diff --git a/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/maskot_desa/page.tsx b/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/maskot_desa/page.tsx index 92829667..a33601da 100644 --- a/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/maskot_desa/page.tsx +++ b/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/maskot_desa/page.tsx @@ -5,7 +5,7 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'; import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; -import { Alert, Box, Button, Center, Group, Image, Paper, SimpleGrid, Stack, Text, TextInput, Title } from '@mantine/core'; +import { Alert, Box, Button, Center, Group, Image, Loader, Paper, SimpleGrid, Stack, Text, TextInput, Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconAlertCircle, IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -19,8 +19,8 @@ function Page() { const params = useParams(); const [images, setImages] = useState< - Array<{ file: File | null; preview: string; label: string; imageId?: string }> ->([]); + Array<{ file: File | null; preview: string; label: string; imageId?: string }> + >([]); const [formData, setFormData] = useState({ judul: '', deskripsi: '', @@ -28,6 +28,12 @@ function Page() { }); const [isSubmitting, setIsSubmitting] = useState(false); + const [originalData, setOriginalData] = useState({ + judul: "", + deskripsi: "", + images: [] as Array<{ label: string; imageId: string }> + }); + // Load data useEffect(() => { const loadData = async () => { @@ -52,6 +58,17 @@ function Page() { })), }); + setOriginalData({ + judul: data.judul || '', + deskripsi: data.deskripsi || '', + images: (data.images || []).map((img: any) => ({ + label: img.label, + imageId: img.image?.id ?? '', + preview: img.image?.link ?? '', + })), + }); + + if (data?.images?.length > 0 && data.images[0].image?.link) { setImages(data.images.map((img: any) => ({ file: null, @@ -77,15 +94,36 @@ function Page() { const handleBack = () => router.back(); + const handleResetForm = () => { + setFormData({ + judul: originalData.judul, + deskripsi: originalData.deskripsi, + images: originalData.images.map((img) => ({ + label: img.label, + imageId: img.imageId, + })), + }); + + setImages( + originalData.images.map((img: any) => ({ + file: null, + preview: img.preview, // pakai preview masing-masing, bukan cuma satu + label: img.label, + imageId: img.imageId, + })) + ); + + toast.info("Form dikembalikan ke data awal"); + }; + + const handleSubmit = async () => { if (isSubmitting || !formData.judul.trim()) { toast.error("Judul wajib diisi"); return; } - - setIsSubmitting(true); - try { + setIsSubmitting(true); const uploadedImages = []; // Upload semua gambar baru @@ -95,7 +133,7 @@ function Page() { uploadedImages.push({ imageId: img.imageId, label: img.label }); continue; } - + // upload baru const res = await ApiFetch.api.fileStorage.create.post({ file: img.file, @@ -108,7 +146,7 @@ function Page() { } uploadedImages.push({ imageId: uploaded.id, label: img.label || "main" }); } - + // Update ke global state maskotState.update.updateField("judul", formData.judul); @@ -161,9 +199,9 @@ function Page() { - + Edit Maskot Desa @@ -175,7 +213,7 @@ function Page() { Judul} placeholder="Masukkan judul maskot" - defaultValue={formData.judul} + value={formData.judul} onChange={(e) => setFormData({ ...formData, judul: e.currentTarget.value })} error={!formData.judul && "Judul wajib diisi"} /> @@ -231,7 +269,7 @@ function Page() { setImages(updated); }} > - Hapus +
{/* Buttons */} - + + {/* Tombol Batal */} - + + {/* Tombol Simpan */} + diff --git a/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/sejarah_desa/page.tsx b/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/sejarah_desa/page.tsx index c30ad4d8..7bbd5c16 100644 --- a/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/sejarah_desa/page.tsx +++ b/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/sejarah_desa/page.tsx @@ -1,146 +1,272 @@ -/* eslint-disable react-hooks/exhaustive-deps */ -'use client' +'use client'; import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'; import colors from '@/con/colors'; -import { Alert, Box, Button, Center, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; +import { + Alert, + Box, + Button, + Center, + Group, + Loader, + Paper, + Stack, + Text, + TextInput, + Title +} from '@mantine/core'; import { IconAlertCircle, IconArrowBack } 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'; +// 🔹 Types +interface FormData { + judul: string; + deskripsi: string; +} + +// 🔹 Main Component function Page() { - const sejarahState = useProxy(stateProfileDesa.sejarahDesa) - const router = useRouter() - const params = useParams() - const [isSubmitting, setIsSubmitting] = useState(false); + const router = useRouter(); + const params = useParams(); - // Load data + // 🧩 Local State + const [formData, setFormData] = useState({ + judul: '', + deskripsi: '', + }); + const [originalData, setOriginalData] = useState({ + judul: '', + deskripsi: '', + }); + const [isLoading, setIsLoading] = useState(true); + const [isSubmitting, setIsSubmitting] = useState(false); + const [loadError, setLoadError] = useState(null); + + // 🧭 Load Initial Data useEffect(() => { const loadData = async () => { const id = params?.id as string; if (!id) { - toast.error("ID tidak valid"); - router.push("/admin/desa/profile/profile-desa"); + toast.error('ID tidak valid'); + router.push('/admin/desa/profile/profile-desa'); return; } + setIsLoading(true); + setLoadError(null); + try { - const data = await sejarahState.findUnique.load(id); + const data = await stateProfileDesa.sejarahDesa.findUnique.load(id); + if (data) { - sejarahState.update.initialize(data); + const initialData: FormData = { + judul: data.judul || '', + deskripsi: data.deskripsi || '', + }; + + setFormData(initialData); + setOriginalData(initialData); + + stateProfileDesa.sejarahDesa.update.initialize(data); + } else { + setLoadError('Data tidak ditemukan'); } } catch (error) { - console.error("Error loading sejarah:", error); - toast.error("Gagal memuat data sejarah desa"); + console.error('Error loading sejarah:', error); + setLoadError('Gagal memuat data sejarah desa'); + toast.error('Gagal memuat data sejarah desa'); + } finally { + setIsLoading(false); } }; loadData(); return () => { - sejarahState.update.reset(); - sejarahState.findUnique.reset(); + stateProfileDesa.sejarahDesa.update.reset(); + stateProfileDesa.sejarahDesa.findUnique.reset(); }; }, [params?.id, router]); + // 🔄 Check if form has changes + + + // 🔁 Reset Form to Original Data + const handleResetForm = () => { + setFormData(originalData); + toast.info('Form dikembalikan ke data awal'); + }; + + // 💾 Submit Handler const handleSubmit = async () => { - if (isSubmitting || !sejarahState.update.form.judul.trim()) { - toast.error("Judul wajib diisi"); + // Validation + if (!formData.judul.trim()) { + toast.error('Judul wajib diisi'); return; } setIsSubmitting(true); + try { - const success = await sejarahState.update.submit(); + // Access original state directly (not proxy) + const originalState = stateProfileDesa.sejarahDesa; + + // Update form data + originalState.update.form.judul = formData.judul; + originalState.update.form.deskripsi = formData.deskripsi; + + // Submit + const success = await originalState.update.submit(); + if (success) { - toast.success("Data berhasil disimpan"); - router.push("/admin/desa/profile/profile-desa"); + toast.success('Data berhasil disimpan'); + router.push('/admin/desa/profile/profile-desa'); + } else { + toast.error('Gagal menyimpan data'); } } catch (error) { - console.error("Error update sejarah desa:", error); - toast.error("Terjadi kesalahan saat update sejarah desa"); + console.error('Error update sejarah desa:', error); + toast.error('Terjadi kesalahan saat update sejarah desa'); } finally { setIsSubmitting(false); } }; - const handleBack = () => router.back(); + // 📝 Form Field Handlers + const handleJudulChange = (e: React.ChangeEvent) => { + setFormData(prev => ({ ...prev, judul: e.target.value })); + }; - // Loading state - if (sejarahState.findUnique.loading || sejarahState.update.loading) { + const handleDeskripsiChange = (html: string) => { + setFormData(prev => ({ ...prev, deskripsi: html })); + }; + + const handleBack = () => { + router.back(); + }; + // 🔄 Loading State + if (isLoading) { return (
- Memuat data... + + + + Memuat data... + +
); } - // Error state - if (sejarahState.findUnique.error) { + // ❌ Error State + if (loadError) { return ( - + - - } color="red"> - Error - {sejarahState.findUnique.error} + } + color="red" + title="Terjadi Kesalahan" + radius="md" + > + {loadError} + ); } return ( - - - + + + {/* Header */} + - Edit Sejarah Desa + + Edit Sejarah Desa + - - - Edit Sejarah Desa - - {/* Judul */} + {/* Form */} + + + {/* Form Title */} + + + Informasi Sejarah Desa + + + {/* Judul Field */} Judul} - placeholder="Judul sejarah" - defaultValue={sejarahState.update.form.judul} - onChange={(e) => sejarahState.update.form.judul = e.currentTarget.value} - error={!sejarahState.update.form.judul && "Judul wajib diisi"} + label={Judul Sejarah} + placeholder="Masukkan judul sejarah desa" + value={formData.judul} + onChange={handleJudulChange} + error={!formData.judul.trim() && 'Judul wajib diisi'} + required + size="md" + radius="md" /> - {/* Deskripsi */} + {/* Deskripsi Field */} - Deskripsi + + Deskripsi Sejarah + sejarahState.update.form.deskripsi = val} + value={formData.deskripsi} + onChange={handleDeskripsiChange} /> + - {/* Buttons */} - + {/* Action Buttons */} + + {/* Tombol Batal */} - @@ -150,4 +276,4 @@ function Page() { ); } -export default Page; +export default Page; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/visi_misi_desa/page.tsx b/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/visi_misi_desa/page.tsx index 481105ad..938172b1 100644 --- a/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/visi_misi_desa/page.tsx +++ b/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/visi_misi_desa/page.tsx @@ -1,155 +1,247 @@ -/* eslint-disable react-hooks/exhaustive-deps */ -'use client' +'use client'; + import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'; import colors from '@/con/colors'; -import { Alert, Box, Button, Center, Group, Paper, Stack, Text, Title } from '@mantine/core'; +import { + Alert, + Box, + Button, + Center, + Group, + Loader, + Paper, + Stack, + Text, + Title, +} from '@mantine/core'; import { IconAlertCircle, IconArrowBack } 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'; +// 🔹 Types +interface FormData { + visi: string; + misi: string; +} + +// 🔹 Main Component function Page() { - const visiMisiState = useProxy(stateProfileDesa.visiMisiDesa) - const router = useRouter() - const params = useParams() - const [isSubmitting, setIsSubmitting] = useState(false); + const router = useRouter(); + const params = useParams(); + const [formData, setFormData] = useState({ visi: '', misi: '' }); + const [originalData, setOriginalData] = useState({ visi: '', misi: '' }); + const [isLoading, setIsLoading] = useState(true); + const [isSubmitting, setIsSubmitting] = useState(false); + const [loadError, setLoadError] = useState(null); - // Load data - useEffect(() => { - const loadData = async () => { - const id = params?.id as string; - if (!id) { - toast.error("ID tidak valid"); - router.push("/admin/desa/profile/profile-desa"); - return; - } + // 🧭 Load Data + useEffect(() => { + const loadData = async () => { + const id = params?.id as string; + if (!id) { + toast.error('ID tidak valid'); + router.push('/admin/desa/profile/profile-desa'); + return; + } - try { - const data = await visiMisiState.findUnique.load(id); - visiMisiState.update.initialize(data); - } catch (error) { - console.error("Error loading visi misi:", error); - toast.error("Gagal memuat data visi misi desa"); - } - }; + setIsLoading(true); + setLoadError(null); - loadData(); + try { + const data = await stateProfileDesa.visiMisiDesa.findUnique.load(id); + if (data) { + const initialData: FormData = { + visi: data.visi || '', + misi: data.misi || '', + }; + setFormData(initialData); + setOriginalData(initialData); - return () => { - visiMisiState.update.reset(); - visiMisiState.findUnique.reset(); - }; - }, [params?.id, router]); - - const handleSubmit = async () => { - if (isSubmitting || !visiMisiState.update.form.visi.trim()) { - toast.error("Visi wajib diisi"); - return; - } - - setIsSubmitting(true); - - try { - const success = await visiMisiState.update.submit(); - - if (success) { - toast.success("Data berhasil disimpan"); - router.push("/admin/desa/profile/profile-desa"); - } - } catch (error) { - console.error("Error update visi misi desa:", error); - toast.error("Terjadi kesalahan saat update visi misi desa"); - } finally { - setIsSubmitting(false); + // set id ke state agar submit pakai endpoint benar + stateProfileDesa.visiMisiDesa.update.initialize(data); + } else { + setLoadError('Data tidak ditemukan'); } + } catch (error) { + console.error('Error load visi misi:', error); + setLoadError('Gagal memuat data visi misi desa'); + toast.error('Gagal memuat data visi misi desa'); + } finally { + setIsLoading(false); + } }; - const handleBack = () => router.back(); + loadData(); - // Loading state - if (visiMisiState.findUnique.loading || visiMisiState.update.loading) { - return ( - -
- Memuat data... -
-
- ); + return () => { + stateProfileDesa.visiMisiDesa.update.reset(); + stateProfileDesa.visiMisiDesa.findUnique.reset(); + }; + }, [params?.id, router]); + + // 🔄 Reset Form + const handleResetForm = () => { + setFormData(originalData); + toast.info('Form dikembalikan ke data awal'); + }; + + // 💾 Submit + const handleSubmit = async () => { + if (!formData.visi.trim()) { + toast.error('Visi wajib diisi'); + return; } - // Error state - if (visiMisiState.findUnique.error) { - return ( - - - - } color="red"> - Error - {visiMisiState.findUnique.error} - - - - ); - } + setIsSubmitting(true); + try { + const originalState = stateProfileDesa.visiMisiDesa; + // update data form ke state sebelum submit + originalState.update.form.visi = formData.visi; + originalState.update.form.misi = formData.misi; + + const success = await originalState.update.submit(); + + if (success) { + toast.success('Data berhasil disimpan'); + router.push('/admin/desa/profile/profile-desa'); + } else { + toast.error('Gagal menyimpan data'); + } + } catch (error) { + console.error('Error update visi misi desa:', error); + toast.error('Terjadi kesalahan saat update visi misi desa'); + } finally { + setIsSubmitting(false); + } + }; + + // 🧭 Field handlers + const handleVisiChange = (html: string) => setFormData(prev => ({ ...prev, visi: html })); + const handleMisiChange = (html: string) => setFormData(prev => ({ ...prev, misi: html })); + const handleBack = () => router.back(); + + // ⏳ Loading + if (isLoading) { return ( - - - - - - Edit Visi Misi Desa - - - - - Edit Visi Misi Desa - - {/* Visi */} - - Visi - visiMisiState.update.form.visi = val} - /> - - - {/* Misi */} - - Misi - visiMisiState.update.form.misi = val} - /> - - - {/* Buttons */} - - - - - - - - - + +
+ + + + Memuat data... + + +
+
); + } + + // ❌ Error + if (loadError) { + return ( + + + + } + color="red" + title="Terjadi Kesalahan" + radius="md" + > + {loadError} + + + + + ); + } + + // ✅ UI + return ( + + + {/* Header */} + + + + Edit Visi & Misi Desa + + + + {/* Form */} + + + + + Informasi Visi & Misi Desa + + + + {/* Visi */} + + + Visi + + + + + {/* Misi */} + + + Misi + + + + + {/* Actions */} + + + + + + + + + ); } export default Page; diff --git a/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/[id]/edit/page.tsx index fda2ae79..d7c3b632 100644 --- a/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/[id]/edit/page.tsx @@ -4,6 +4,7 @@ import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'; import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, @@ -12,7 +13,8 @@ import { Stack, Text, TextInput, - Title + Title, + Loader } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; @@ -35,6 +37,16 @@ function EditPerbekelDariMasaKeMasa() { imageId: '' }); + const [isSubmitting, setIsSubmitting] = useState(false); + + const [originalData, setOriginalData] = useState({ + nama: '', + daerah: '', + periode: '', + imageId: '', + imageUrl: "", + }); + // load data pertama kali useEffect(() => { const loadFoto = async () => { @@ -49,9 +61,14 @@ function EditPerbekelDariMasaKeMasa() { periode: data.periode || '', imageId: data.imageId || '' }); - if (data?.imageGalleryFoto?.link) { - setPreviewImage(data.imageGalleryFoto.link); - } + setOriginalData({ + nama: data.nama || '', + daerah: data.daerah || '', + periode: data.periode || '', + imageId: data.imageId || '', + imageUrl: data.image.link || '', + }) + setPreviewImage(data.image.link); } } catch (error) { console.error('Error loading foto:', error); @@ -69,8 +86,22 @@ function EditPerbekelDariMasaKeMasa() { })); }; + const handleResetForm = () => { + setFormData({ + nama: originalData.nama, + daerah: originalData.daerah, + periode: originalData.periode, + imageId: originalData.imageId, + }); + setPreviewImage(originalData.imageUrl || null); + setFile(null); + toast.info("Form dikembalikan ke data awal"); + }; + + const handleSubmit = async () => { try { + setIsSubmitting(true); // update global state hanya sekali pas submit state.update.form = { ...state.update.form, ...formData }; @@ -90,15 +121,17 @@ function EditPerbekelDariMasaKeMasa() { } catch (error) { console.error('Error updating perbekel dari masa ke masa:', error); toast.error('Terjadi kesalahan saat memperbarui perbekel dari masa ke masa'); + } finally { + setIsSubmitting(false); } }; return ( - + Edit Perbekel Dari Masa Ke Masa @@ -135,7 +168,7 @@ function EditPerbekelDariMasaKeMasa() { }} onReject={() => toast.error('File tidak valid, gunakan format gambar')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} radius="md" p="xl" > @@ -154,25 +187,45 @@ function EditPerbekelDariMasaKeMasa() { Seret gambar atau klik untuk memilih file - Maksimal 5MB, format gambar wajib + Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp
{previewImage && ( - + Preview Gambar + + {/* Tombol hapus (pojok kanan atas) */} + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -194,6 +247,17 @@ function EditPerbekelDariMasaKeMasa() { /> + + + {/* Tombol Simpan */}
diff --git a/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/create/page.tsx b/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/create/page.tsx index 2f499e55..4a8f6aaa 100644 --- a/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/create/page.tsx +++ b/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/create/page.tsx @@ -2,7 +2,7 @@ import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'; import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; -import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; +import { Loader, ActionIcon, Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; @@ -14,6 +14,7 @@ function CreatePerbekelDariMasaKeMasa() { const state = useProxy(stateProfileDesa.mantanPerbekel); const router = useRouter(); const [previewImage, setPreviewImage] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); const [file, setFile] = useState(null); const resetForm = () => { @@ -28,31 +29,39 @@ function CreatePerbekelDariMasaKeMasa() { }; const handleSubmit = async () => { - if (!file) { - return toast.warn('Pilih file gambar terlebih dahulu'); + try { + setIsSubmitting(true); + if (!file) { + return toast.warn('Pilih file gambar terlebih dahulu'); + } + + const res = await ApiFetch.api.fileStorage.create.post({ + file, + name: file.name, + }); + + const uploaded = res.data?.data; + if (!uploaded?.id) return toast.error('Gagal upload gambar'); + + state.create.form.imageId = uploaded.id; + await state.create.create(); + resetForm(); + router.push('/admin/desa/profile/profile-perbekel-dari-masa-ke-masa'); + } catch (error) { + console.error(error); + toast.error('Gagal menambahkan perbekel dari masa ke masa'); + } finally { + setIsSubmitting(false); } - - const res = await ApiFetch.api.fileStorage.create.post({ - file, - name: file.name, - }); - - const uploaded = res.data?.data; - if (!uploaded?.id) return toast.error('Gagal upload gambar'); - - state.create.form.imageId = uploaded.id; - await state.create.create(); - resetForm(); - router.push('/admin/desa/profile/profile-perbekel-dari-masa-ke-masa'); }; return ( {/* Back button + Title */} - + Create Perbekel Dari Masa Ke Masa @@ -70,21 +79,21 @@ function CreatePerbekelDariMasaKeMasa() { (state.create.form.nama = e.target.value)} required /> (state.create.form.daerah = e.target.value)} required /> (state.create.form.periode = e.target.value)} required /> @@ -102,7 +111,7 @@ function CreatePerbekelDariMasaKeMasa() { }} onReject={() => toast.error('File tidak valid, gunakan format gambar')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} radius="md" p="xl" > @@ -118,25 +127,63 @@ function CreatePerbekelDariMasaKeMasa() { - Seret gambar atau klik untuk memilih file (maks 5MB) + Seret gambar atau klik untuk memilih file + + + Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp {previewImage && ( - + Preview Gambar + + {/* Tombol hapus (pojok kanan atas) */} + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} {/* Submit */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/desa/profile/profile-perbekel/[id]/page.tsx b/src/app/admin/(dashboard)/desa/profile/profile-perbekel/[id]/page.tsx index 771e3f37..1a916bfe 100644 --- a/src/app/admin/(dashboard)/desa/profile/profile-perbekel/[id]/page.tsx +++ b/src/app/admin/(dashboard)/desa/profile/profile-perbekel/[id]/page.tsx @@ -126,8 +126,8 @@ function ProfilePerbekel() { handleFileChange(files[0])} onReject={() => toast.error('File tidak valid.')} - maxSize={5 * 1024 ** 2} // 5MB - accept={{ 'image/*': [] }} + maxSize={5 * 1024 ** 2} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} > diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/_lib/layoutTabs.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/_lib/layoutTabs.tsx index dca2dfe2..ff6d98fc 100644 --- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/_lib/layoutTabs.tsx +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/_lib/layoutTabs.tsx @@ -2,23 +2,22 @@ 'use client' import colors from '@/con/colors'; import { + ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, - Title, - Tooltip, - ScrollArea, + Title } from '@mantine/core'; -import { usePathname, useRouter } from 'next/navigation'; -import React, { useEffect, useState } from 'react'; import { - IconFileAnalytics, IconCoins, + IconFileAnalytics, IconShoppingCart, IconWallet, } from '@tabler/icons-react'; +import { usePathname, useRouter } from 'next/navigation'; +import React, { useEffect, useState } from 'react'; function LayoutTabs({ children }: { children: React.ReactNode }) { const router = useRouter(); @@ -29,29 +28,25 @@ function LayoutTabs({ children }: { children: React.ReactNode }) { label: "APB Desa", value: "apbdesa", href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa", - icon: , - tooltip: "Lihat ringkasan Anggaran Pendapatan dan Belanja Desa", + icon: }, { label: "Pendapatan", value: "pendapatan", href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan", icon: , - tooltip: "Kelola data pendapatan desa", }, { label: "Belanja", value: "belanja", href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja", icon: , - tooltip: "Atur data belanja desa", }, { label: "Pembiayaan", value: "pembiayaan", href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan", icon: , - tooltip: "Kelola data pembiayaan desa", }, ]; @@ -104,26 +99,19 @@ function LayoutTabs({ children }: { children: React.ReactNode }) { }} > {tabs.map((tab, i) => ( - - - {tab.label} - - + {tab.label} + ))} diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/[id]/edit/page.tsx index 956f84e1..121be66a 100644 --- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/[id]/edit/page.tsx @@ -1,11 +1,14 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable react-hooks/exhaustive-deps */ -'use client' +'use client'; import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa'; import colors from '@/con/colors'; import { + Alert, Box, Button, + Group, + Loader, MultiSelect, Paper, Skeleton, @@ -13,8 +16,6 @@ import { Text, TextInput, Title, - Tooltip, - Group, } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack } from '@tabler/icons-react'; @@ -23,81 +24,132 @@ import { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; +// ==================== HELPERS ==================== +const safeStringArray = (arr: any[]): string[] => { + if (!Array.isArray(arr)) return []; + return arr + .filter(item => item != null && item !== '') + .map(item => String(item)) + .filter(item => item.trim() !== ''); +}; + +const createEmptyForm = () => ({ + tahun: '', + pendapatanIds: [] as string[], + belanjaIds: [] as string[], + pembiayaanIds: [] as string[], +}); + +// ==================== COMPONENT ==================== function EditAPBDesa() { const apbState = useProxy(PendapatanAsliDesa.ApbDesa); const router = useRouter(); const params = useParams(); - const [formData, setFormData] = useState({ - tahun: '', - pendapatanIds: [] as string[], - belanjaIds: [] as string[], - pembiayaanIds: [] as string[], - }); + const [formData, setFormData] = useState(createEmptyForm()); + const [originalData, setOriginalData] = useState(createEmptyForm()); + const [isSubmitting, setIsSubmitting] = useState(false); + const [isLoading, setIsLoading] = useState(true); - // Load APB desa by id → hanya update formData, bukan global state + // ==================== LOAD DATA ==================== useEffect(() => { const loadAPBdesa = async () => { const id = params?.id as string; - if (!id) return; + if (!id) { + toast.error('ID tidak valid'); + router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa'); + return; + } try { + setIsLoading(true); const data = await apbState.update.load(id); - if (data) { - setFormData({ - tahun: String(data.tahun || ''), - pendapatanIds: data.pendapatan?.map((p: any) => p.id) || [], - belanjaIds: data.belanja?.map((b: any) => b.id) || [], - pembiayaanIds: data.pembiayaan?.map((p: any) => p.id) || [], - }); + + if (!data) { + toast.error('Data APB Desa tidak ditemukan'); + return; } - } catch (error) { - console.error("Error loading APBdesa:", error); - toast.error("Gagal memuat data APBdesa"); + + const normalized = { + tahun: String(data.tahun || ''), + pendapatanIds: safeStringArray(data.pendapatan?.map((p: any) => p.id) || []), + belanjaIds: safeStringArray(data.belanja?.map((b: any) => b.id) || []), + pembiayaanIds: safeStringArray(data.pembiayaan?.map((p: any) => p.id) || []), + }; + + setFormData(normalized); + setOriginalData(normalized); + } catch (err) { + console.error('Error loading APBdesa:', err); + toast.error('Gagal memuat data APB Desa'); + } finally { + setIsLoading(false); } }; loadAPBdesa(); }, [params?.id]); + // ==================== HANDLERS ==================== const handleChange = (field: keyof typeof formData, value: any) => { setFormData(prev => ({ ...prev, [field]: value })); }; + const handleResetForm = () => { + setFormData(originalData); + toast.info('Form dikembalikan ke data awal'); + }; + const handleSubmit = async () => { try { - // update global state cuma pas submit + setIsSubmitting(true); + + if (!formData.tahun.trim()) { + toast.warning('Tahun harus diisi'); + return; + } + apbState.update.form = { ...apbState.update.form, tahun: Number(formData.tahun), - pendapatanIds: formData.pendapatanIds, - belanjaIds: formData.belanjaIds, - pembiayaanIds: formData.pembiayaanIds, + pendapatanIds: safeStringArray(formData.pendapatanIds), + belanjaIds: safeStringArray(formData.belanjaIds), + pembiayaanIds: safeStringArray(formData.pembiayaanIds), }; await apbState.update.update(); - toast.success("APB Desa berhasil diperbarui!"); - router.push("/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa"); + toast.success('APB Desa berhasil diperbarui!'); + router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa'); } catch (error) { - console.error("Error updating APBdesa:", error); - toast.error("Terjadi kesalahan saat memperbarui APBdesa"); + console.error('Error updating APBdesa:', error); + toast.error('Terjadi kesalahan saat memperbarui APB Desa'); + } finally { + setIsSubmitting(false); } }; + // ==================== UI ==================== + if (isLoading) { + return ( + + + Memuat data APB Desa... + + ); + } + return ( {/* Header */} - - - + Edit APB Desa @@ -117,30 +169,46 @@ function EditAPBDesa() { handleChange("tahun", e.target.value)} + onChange={(e) => handleChange('tahun', e.target.value)} label={Tahun} placeholder="Masukkan tahun anggaran" required /> {/* Selects */} - handleChange("pendapatanIds", ids)} + onSelectionChange={(ids) => handleChange('pendapatanIds', ids)} /> - handleChange("belanjaIds", ids)} + onSelectionChange={(ids) => handleChange('belanjaIds', ids)} /> - handleChange("pembiayaanIds", ids)} + onSelectionChange={(ids) => handleChange('pembiayaanIds', ids)} /> {/* Save Button */} - + + + ); +} - /* --- Sub Components --- */ +// ==================== SUB COMPONENT ==================== +function SelectAPBItem({ + label, + state, + selectedIds, + onSelectionChange, +}: { + label: string; + state: any; + selectedIds: string[]; + onSelectionChange: (ids: string[]) => void; +}) { + const proxyState = useProxy(state); - function SelectPendapatan({ - selectedIds, - onSelectionChange, - }: { - selectedIds: string[]; - onSelectionChange: (ids: string[]) => void; - }) { - const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan); + useShallowEffect(() => { + proxyState.findMany.load(); + }, []); - useShallowEffect(() => { - pendapatanState.findMany.load(); - }, []); + const data = proxyState.findMany.data; + const isLoading = !data; - if (!pendapatanState.findMany.data) { - return ; - } + const options = + data + ?.filter((item: any) => item?.id) + .map((item: any) => ({ + value: String(item.id), + label: String(item?.name || '(Tanpa Nama)'), + })) || []; + if (isLoading) { return ( - Pendapatan} - data={pendapatanState.findMany.data.map((p: any) => ({ - value: p.id, - label: p.name, - }))} - value={selectedIds} - onChange={onSelectionChange} - searchable - clearable - placeholder="Pilih pendapatan..." - nothingFoundMessage="Tidak ditemukan" - /> + + {label} + + ); } - function SelectBelanja({ - selectedIds, - onSelectionChange, - }: { - selectedIds: string[]; - onSelectionChange: (ids: string[]) => void; - }) { - const belanjaState = useProxy(PendapatanAsliDesa.belanja); - - useShallowEffect(() => { - belanjaState.findMany.load(); - }, []); - - if (!belanjaState.findMany.data) { - return ; - } - + if (options.length === 0) { return ( - Belanja} - data={belanjaState.findMany.data.map((b: any) => ({ - value: b.id, - label: b.name, - }))} - value={selectedIds} - onChange={onSelectionChange} - searchable - clearable - placeholder="Pilih belanja..." - nothingFoundMessage="Tidak ditemukan" - /> + + + Tidak ada data {label.toLowerCase()} tersedia. + + ); } - function SelectPembiayaan({ - selectedIds, - onSelectionChange, - }: { - selectedIds: string[]; - onSelectionChange: (ids: string[]) => void; - }) { - const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan); - - useShallowEffect(() => { - pembiayaanState.findMany.load(); - }, []); - - if (!pembiayaanState.findMany.data) { - return ; - } - - return ( - Pembiayaan} - data={pembiayaanState.findMany.data.map((p: any) => ({ - value: p.id, - label: p.name, - }))} - value={selectedIds} - onChange={onSelectionChange} - searchable - clearable - placeholder="Pilih pembiayaan..." - nothingFoundMessage="Tidak ditemukan" - /> - ); - } + return ( + {label}} + data={options} + value={selectedIds} + onChange={(ids) => onSelectionChange(safeStringArray(ids))} + searchable + clearable + placeholder={`Pilih ${label.toLowerCase()}...`} + nothingFoundMessage="Tidak ditemukan" + /> + ); } export default EditAPBDesa; diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/[id]/page.tsx index ee57dc50..67bc460f 100644 --- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/[id]/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/[id]/page.tsx @@ -9,8 +9,7 @@ import { Paper, Skeleton, Stack, - Text, - Tooltip + Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; @@ -81,7 +80,7 @@ function DetailAPBDesa() { Detail APB Desa - + @@ -159,36 +158,32 @@ function DetailAPBDesa() { - - - + - - - + diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/create/page.tsx index 797cc92f..ad347bc2 100644 --- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/create/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/create/page.tsx @@ -6,23 +6,26 @@ import { Box, Button, Group, + Loader, MultiSelect, Paper, Skeleton, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; function CreateAPBDesa() { const apbDesaState = useProxy(PendapatanAsliDesa.ApbDesa); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { apbDesaState.create.form = { @@ -34,20 +37,26 @@ function CreateAPBDesa() { }; const handleSubmit = async () => { - await apbDesaState.create.submit(); - resetForm(); - router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa'); + try { + setIsSubmitting(true); + await apbDesaState.create.submit(); + resetForm(); + router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa'); + } catch (error) { + console.error('Error creating APB Desa:', error); + toast.error('Gagal membuat APB Desa'); + } finally { + setIsSubmitting(false); + } }; return ( {/* Header */} - - - + Tambah APB Desa @@ -65,7 +74,7 @@ function CreateAPBDesa() { { apbDesaState.create.form.tahun = Number(val.target.value); }} @@ -97,6 +106,17 @@ function CreateAPBDesa() { {/* Action */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/page.tsx index ff0c6464..e87b21f1 100644 --- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/page.tsx @@ -5,8 +5,8 @@ import { Button, Center, Group, - Paper, Pagination, + Paper, Skeleton, Stack, Table, @@ -15,8 +15,7 @@ import { TableTh, TableThead, TableTr, - Text, - Tooltip, + Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; @@ -82,20 +81,18 @@ function ListAPBDesa({ search }: { search: string }) { List APB Desa - - - + @@ -138,20 +135,18 @@ function ListAPBDesa({ search }: { search: string }) { )} - - - + )) diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/[id]/page.tsx index 7c03d4a8..533a12db 100644 --- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/[id]/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/[id]/page.tsx @@ -7,11 +7,11 @@ import { Box, Button, Group, + Loader, Paper, Stack, TextInput, - Title, - Tooltip + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -23,12 +23,18 @@ function EditBelanja() { const belanjaState = useProxy(PendapatanAsliDesa.belanja); const router = useRouter(); const params = useParams(); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ name: '', value: '', }); + const [originalData, setOriginalData] = useState({ + name: '', + value: '', + }); + // format angka ke rupiah const formatRupiah = (value: number | string) => { const number = @@ -59,6 +65,10 @@ function EditBelanja() { name: data.name || '', value: String(data.value || ''), }); + setOriginalData({ + name: data.name || '', + value: String(data.value || ''), + }); } } catch (error) { console.error("Error loading belanja:", error); @@ -71,6 +81,7 @@ function EditBelanja() { const handleSubmit = async () => { try { + setIsSubmitting(true); belanjaState.update.form = { ...belanjaState.update.form, name: formData.name, @@ -83,23 +94,31 @@ function EditBelanja() { } catch (error) { console.error("Error updating jenis belanja:", error); toast.error("Terjadi kesalahan saat memperbarui jenis belanja"); + } finally { + setIsSubmitting(false); } }; + const handleResetForm = () => { + setFormData({ + name: originalData.name, + value: originalData.value, + }); + toast.info("Form dikembalikan ke data awal"); + }; + return ( {/* Header */} - - - + Edit Jenis Belanja @@ -138,6 +157,17 @@ function EditBelanja() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/create/page.tsx index 7cc80cf9..fee6e242 100644 --- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/create/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/create/page.tsx @@ -6,21 +6,23 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; -import { useProxy } from 'valtio/utils'; +import { useState } from 'react'; import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; function CreateBelanja() { const belanjaState = useProxy(PendapatanAsliDesa.belanja); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const formatRupiah = (value: number | string) => { const number = @@ -48,25 +50,31 @@ function CreateBelanja() { return toast.warn('Lengkapi semua field terlebih dahulu'); } - await belanjaState.create.submit(); - resetForm(); - router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja'); + try { + setIsSubmitting(true); + await belanjaState.create.submit(); + resetForm(); + router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja'); + } catch (error) { + console.error('Error creating belanja:', error); + toast.error('Gagal menambahkan jenis belanja'); + } finally { + setIsSubmitting(false); + } }; return ( {/* Header dengan back button */} - - - + Tambah Jenis Belanja @@ -85,7 +93,7 @@ function CreateBelanja() { Nama Jenis Belanja} placeholder="Masukkan nama jenis belanja" - defaultValue={belanjaState.create.form.name} + value={belanjaState.create.form.name} onChange={(e) => (belanjaState.create.form.name = e.target.value)} required /> @@ -94,7 +102,7 @@ function CreateBelanja() { type="text" label={Nilai} placeholder="Masukkan nilai belanja" - defaultValue={formatRupiah(belanjaState.create.form.value)} + value={formatRupiah(belanjaState.create.form.value)} onChange={(e) => { const raw = e.currentTarget.value; belanjaState.create.form.value = unformatRupiah(raw); @@ -103,6 +111,17 @@ function CreateBelanja() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/page.tsx index 467c8142..1a09d875 100644 --- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/page.tsx @@ -17,8 +17,7 @@ import { TableThead, TableTr, Text, - Title, - Tooltip + Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; @@ -96,18 +95,16 @@ function ListBelanja({ search }: { search: string }) { Daftar Belanja - - - + @@ -138,34 +135,30 @@ function ListBelanja({ search }: { search: string }) { - - - - - - + + diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/[id]/page.tsx index 25df9c5b..c7e642dd 100644 --- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/[id]/page.tsx @@ -6,11 +6,11 @@ import { Box, Button, Group, + Loader, Paper, Stack, TextInput, - Title, - Tooltip + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -22,12 +22,18 @@ function EditPembiayaan() { const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan); const router = useRouter(); const params = useParams(); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ name: '', value: '', }); + const [originalData, setOriginalData] = useState({ + name: '', + value: '', + }); + const formatRupiah = (value: number | string) => { const number = typeof value === 'number' @@ -56,6 +62,10 @@ function EditPembiayaan() { name: data.name || '', value: String(data.value || ''), }); + setOriginalData({ + name: data.name || '', + value: String(data.value || ''), + }); } } catch (error) { console.error('Error loading pembiayaan:', error); @@ -66,8 +76,17 @@ function EditPembiayaan() { loadPembiayaan(); }, [params?.id]); + const handleResetForm = () => { + setFormData({ + name: originalData.name, + value: originalData.value, + }); + toast.info('Form dikembalikan ke data awal'); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); pembiayaanState.update.form = { ...pembiayaanState.update.form, name: formData.name, @@ -80,6 +99,8 @@ function EditPembiayaan() { } catch (error) { console.error('Error updating jenis pembiayaan:', error); toast.error('Terjadi kesalahan saat memperbarui jenis pembiayaan'); + } finally { + setIsSubmitting(false); } }; @@ -87,16 +108,14 @@ function EditPembiayaan() { {/* Header */} - - - + Edit Jenis Pembiayaan @@ -135,6 +154,17 @@ function EditPembiayaan() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/create/page.tsx index a432a431..6b808b45 100644 --- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/create/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/create/page.tsx @@ -1,26 +1,27 @@ 'use client'; -import React from 'react'; -import { useProxy } from 'valtio/utils'; import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa'; -import { useRouter } from 'next/navigation'; import colors from '@/con/colors'; import { Box, Button, Group, + Loader, Paper, Stack, - Title, - TextInput, Text, - Tooltip, + TextInput, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; function CreatePembiayaan() { const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const formatRupiah = (value: number | string) => { const number = @@ -44,29 +45,35 @@ function CreatePembiayaan() { }; const handleSubmit = async () => { - if (!pembiayaanState.create.form.name || !pembiayaanState.create.form.value) { - return toast.warn('Nama dan nilai wajib diisi'); - } + try { + setIsSubmitting(true); + if (!pembiayaanState.create.form.name || !pembiayaanState.create.form.value) { + return toast.warn('Nama dan nilai wajib diisi'); + } - await pembiayaanState.create.submit(); - resetForm(); - router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan'); + await pembiayaanState.create.submit(); + resetForm(); + router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan'); + } catch (error) { + console.error('Error creating pembiayaan:', error); + toast.error('Gagal menambahkan jenis pembiayaan'); + } finally { + setIsSubmitting(false); + } }; return ( {/* Header */} - - - + Tambah Jenis Pembiayaan @@ -85,7 +92,7 @@ function CreatePembiayaan() { Nama Jenis Pembiayaan} placeholder="Masukkan nama jenis pembiayaan" - defaultValue={pembiayaanState.create.form.name} + value={pembiayaanState.create.form.name} onChange={(e) => { pembiayaanState.create.form.name = e.currentTarget.value; }} @@ -96,7 +103,7 @@ function CreatePembiayaan() { type="text" label={Nilai} placeholder="Masukkan nilai" - defaultValue={formatRupiah(pembiayaanState.create.form.value)} + value={formatRupiah(pembiayaanState.create.form.value)} onChange={(e) => { const raw = e.currentTarget.value; pembiayaanState.create.form.value = unformatRupiah(raw); @@ -105,6 +112,17 @@ function CreatePembiayaan() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/page.tsx index 627f5894..f9c265a6 100644 --- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/page.tsx @@ -1,9 +1,11 @@ 'use client' +import colors from '@/con/colors'; import { Box, Button, Center, Group, + Pagination, Paper, Skeleton, Stack, @@ -14,19 +16,16 @@ import { TableThead, TableTr, Text, - Title, - Tooltip, - Pagination, + Title } from '@mantine/core'; -import React, { useState } from 'react'; -import HeaderSearch from '../../../_com/header'; -import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; -import PendapatanAsliDesa from '../../../_state/ekonomi/PADesa'; -import { useProxy } from 'valtio/utils'; -import { useRouter } from 'next/navigation'; import { useShallowEffect } from '@mantine/hooks'; -import colors from '@/con/colors'; +import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { useProxy } from 'valtio/utils'; +import HeaderSearch from '../../../_com/header'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; +import PendapatanAsliDesa from '../../../_state/ekonomi/PADesa'; function Pembiayaan() { const [search, setSearch] = useState(""); @@ -95,18 +94,16 @@ function ListPembiayaan({ search }: { search: string }) { Daftar Pembiayaan - - - + diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/[id]/page.tsx index cf8e65f0..0ae681e0 100644 --- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/[id]/page.tsx @@ -6,11 +6,11 @@ import { Box, Button, Group, + Loader, Paper, Stack, TextInput, - Title, - Tooltip + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -22,6 +22,12 @@ function EditPendapatan() { const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan); const router = useRouter(); const params = useParams(); + const [isSubmitting, setIsSubmitting] = useState(false); + + const [originalData, setOriginalData] = useState({ + name: "", + value: "", + }); const [formData, setFormData] = useState({ name: '', @@ -56,6 +62,10 @@ function EditPendapatan() { name: data.name ?? '', value: data.value?.toString() ?? '', }); + setOriginalData({ + name: data.name ?? '', + value: data.value?.toString() ?? '', + }); } } catch (error) { console.error('Error loading pendapatan:', error); @@ -73,8 +83,18 @@ function EditPendapatan() { })); }; + const handleResetForm = () => { + setFormData({ + name: originalData.name, + value: originalData.value, + }); + toast.info("Form dikembalikan ke data awal"); + }; + + const handleSubmit = async () => { try { + setIsSubmitting(true); pendapatanState.update.form = { ...pendapatanState.update.form, name: formData.name, @@ -87,23 +107,24 @@ function EditPendapatan() { } catch (error) { console.error('Error updating jenis pendapatan:', error); toast.error('Terjadi kesalahan saat memperbarui jenis pendapatan'); + } finally { + setIsSubmitting(false); } + }; return ( {/* Header with Back Button */} - - - + Edit Jenis Pendapatan @@ -140,6 +161,17 @@ function EditPendapatan() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/create/page.tsx index 663ae35b..75fc530f 100644 --- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/create/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/create/page.tsx @@ -5,19 +5,22 @@ import { Box, Button, Group, + Loader, Paper, Stack, TextInput, - Title, - Tooltip + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; function CreatePendapatan() { const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const formatRupiah = (value: number | string) => { const number = typeof value === 'number' ? value : Number(value.replace(/\D/g, '')); @@ -40,25 +43,31 @@ function CreatePendapatan() { }; const handleSubmit = async () => { - await pendapatanState.create.submit(); - resetForm(); - router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan'); + try { + setIsSubmitting(true); + await pendapatanState.create.submit(); + resetForm(); + router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan'); + } catch (error) { + console.error('Error creating pendapatan:', error); + toast.error('Gagal menambahkan jenis pendapatan'); + } finally { + setIsSubmitting(false); + } }; return ( {/* Header dengan tombol back + judul */} - - - + Tambah Jenis Pendapatan @@ -75,7 +84,7 @@ function CreatePendapatan() { > { pendapatanState.create.form.name = val.target.value; }} @@ -86,7 +95,7 @@ function CreatePendapatan() { { const raw = val.currentTarget.value; const cleanValue = unformatRupiah(raw); @@ -98,6 +107,17 @@ function CreatePendapatan() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/page.tsx index 47ba7b86..9c0fe175 100644 --- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/page.tsx @@ -17,8 +17,7 @@ import { TableThead, TableTr, Text, - Title, - Tooltip + Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; @@ -96,18 +95,16 @@ function ListPendapatan({ search }: { search: string }) { Daftar Pendapatan - - - + diff --git a/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/[id]/page.tsx index 016cb96b..31f4eb71 100644 --- a/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/[id]/page.tsx @@ -6,15 +6,15 @@ import { Box, Button, Group, + Loader, Paper, Stack, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; -import { useEffect, useState, useCallback } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; import demografiPekerjaan from '../../../_state/ekonomi/demografi-pekerjaan'; @@ -29,12 +29,17 @@ export default function EditDemografiPekerjaan() { const router = useRouter(); const { id } = useParams() as { id: string }; const stateDemografi = useProxy(demografiPekerjaan); - + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ pekerjaan: '', lakiLaki: 0, perempuan: 0, }); + const [originalData, setOriginalData] = useState({ + pekerjaan: '', + lakiLaki: 0, + perempuan: 0, + }); // ✅ Load data hanya sekali di awal (tidak reset form) useEffect(() => { @@ -42,6 +47,7 @@ export default function EditDemografiPekerjaan() { const loadData = async () => { try { + setIsSubmitting(true); stateDemografi.update.id = id; await stateDemografi.findUnique.load(id); @@ -52,10 +58,17 @@ export default function EditDemografiPekerjaan() { lakiLaki: Number(data.lakiLaki ?? 0), perempuan: Number(data.perempuan ?? 0), }); + setOriginalData({ + pekerjaan: data.pekerjaan ?? '', + lakiLaki: Number(data.lakiLaki ?? 0), + perempuan: Number(data.perempuan ?? 0), + }); } } catch (error) { console.error('Error loading data:', error); toast.error('Gagal memuat data'); + } finally { + setIsSubmitting(false); } }; @@ -76,9 +89,19 @@ export default function EditDemografiPekerjaan() { [] ); + const handleResetForm = () => { + setFormData({ + pekerjaan: originalData.pekerjaan, + lakiLaki: Number(originalData.lakiLaki), + perempuan: Number(originalData.perempuan), + }); + toast.info("Form dikembalikan ke data awal"); + }; + // ✅ Submit hanya update global state sekali const handleSubmit = async () => { try { + setIsSubmitting(true); stateDemografi.update.id = id; stateDemografi.update.form = { ...formData }; @@ -89,6 +112,8 @@ export default function EditDemografiPekerjaan() { } catch (error) { console.error('Error updating data:', error); toast.error('Gagal memperbarui data'); + } finally { + setIsSubmitting(false); } }; @@ -96,16 +121,14 @@ export default function EditDemografiPekerjaan() { {/* Header */} - - - + Edit Demografi Pekerjaan @@ -148,6 +171,17 @@ export default function EditDemografiPekerjaan() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/create/page.tsx index d590b347..4d280abf 100644 --- a/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/create/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/create/page.tsx @@ -7,22 +7,24 @@ import { Box, Button, Group, + Loader, Paper, Stack, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; import { useProxy } from 'valtio/utils'; import demografiPekerjaan from '../../../_state/ekonomi/demografi-pekerjaan'; +import { toast } from 'react-toastify'; function CreateDemografiPekerjaan() { const stateDemografi = useProxy(demografiPekerjaan); const [chartData, setChartData] = useState([]); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { stateDemografi.create.form = { @@ -33,32 +35,37 @@ function CreateDemografiPekerjaan() { }; const handleSubmit = async () => { - const id = await stateDemografi.create.create(); - if (id) { - const idStr = String(id); - await stateDemografi.findUnique.load(idStr); - if (stateDemografi.findUnique.data) { - setChartData([stateDemografi.findUnique.data]); + try { + const id = await stateDemografi.create.create(); + if (id) { + const idStr = String(id); + await stateDemografi.findUnique.load(idStr); + if (stateDemografi.findUnique.data) { + setChartData([stateDemografi.findUnique.data]); + } } + resetForm(); + router.push('/admin/ekonomi/demografi-pekerjaan'); + } catch (error) { + console.error('Error creating demografi pekerjaan:', error); + toast.error('Terjadi kesalahan saat menambah demografi pekerjaan'); + } finally { + setIsSubmitting(false); } - resetForm(); - router.push('/admin/ekonomi/demografi-pekerjaan'); }; return ( {/* Header */} - - - + Tambah Demografi Pekerjaan @@ -77,7 +84,7 @@ function CreateDemografiPekerjaan() { { stateDemografi.create.form.pekerjaan = val.currentTarget.value; @@ -87,7 +94,7 @@ function CreateDemografiPekerjaan() { { stateDemografi.create.form.lakiLaki = Number(val.currentTarget.value); @@ -97,7 +104,7 @@ function CreateDemografiPekerjaan() { { stateDemografi.create.form.perempuan = Number(val.currentTarget.value); @@ -106,6 +113,17 @@ function CreateDemografiPekerjaan() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/page.tsx b/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/page.tsx index 1b41b2ae..ea4af5a2 100644 --- a/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/page.tsx @@ -6,7 +6,9 @@ import { Box, Button, Center, + Flex, Group, + Pagination, Paper, Skeleton, Stack, @@ -17,10 +19,7 @@ import { TableThead, TableTr, Text, - Title, - Tooltip, - Pagination, - Flex, + Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; @@ -111,16 +110,14 @@ function ListDemografiPekerjaan({ search }: { search: string }) { List Demografi Pekerjaan - - - + diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/[id]/page.tsx index b7a217a4..8187abde 100644 --- a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/[id]/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/[id]/page.tsx @@ -6,24 +6,24 @@ import colors from '@/con/colors'; import { Box, Button, + Group, + Loader, Paper, Stack, TextInput, - Title, - Group, - Tooltip, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; -import { useProxy } from 'valtio/utils'; import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; function EditJumlahPendudukMiskin() { const router = useRouter(); const params = useParams() as { id: string }; const stateJPM = useProxy(jumlahPendudukMiskin); - + const [isSubmitting, setIsSubmitting] = useState(false); const id = params.id; // 🔹 State lokal untuk form @@ -32,6 +32,11 @@ function EditJumlahPendudukMiskin() { totalPoorPopulation: 0, }); + const [originalData, setOriginalData] = useState({ + year: 0, + totalPoorPopulation: 0, + }); + // 🔹 Load data awal dari backend useEffect(() => { if (!id) return; @@ -45,6 +50,10 @@ function EditJumlahPendudukMiskin() { year: data.year || 0, totalPoorPopulation: data.totalPoorPopulation || 0, }); + setOriginalData({ + year: data.year || 0, + totalPoorPopulation: data.totalPoorPopulation || 0, + }); } } catch (error) { console.error('Gagal memuat data:', error); @@ -63,9 +72,18 @@ function EditJumlahPendudukMiskin() { })); }; + const handleResetForm = () => { + setFormData({ + year: originalData.year, + totalPoorPopulation: originalData.totalPoorPopulation, + }); + toast.info('Form dikembalikan ke data awal'); + }; + // 🔹 Submit form const handleSubmit = async () => { try { + setIsSubmitting(true); stateJPM.update.id = id; // update global state cuma saat submit stateJPM.update.form = { ...formData }; @@ -76,22 +94,22 @@ function EditJumlahPendudukMiskin() { } catch (error) { console.error('Gagal menyimpan data:', error); toast.error('Terjadi kesalahan saat menyimpan data'); + } finally { + setIsSubmitting(false); } }; return ( - - - + Edit Jumlah Penduduk Miskin @@ -127,6 +145,17 @@ function EditJumlahPendudukMiskin() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/create/page.tsx index fdebedfd..363688f5 100644 --- a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/create/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/create/page.tsx @@ -1,18 +1,20 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-explicit-any */ 'use client'; -import { Box, Button, Group, Paper, Stack, TextInput, Title, Tooltip } from '@mantine/core'; +import colors from '@/con/colors'; +import { Box, Button, Group, Loader, Paper, Stack, TextInput, Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; import { useProxy } from 'valtio/utils'; -import colors from '@/con/colors'; import jumlahPendudukMiskin from '../../../_state/ekonomi/jumlah-penduduk-miskin'; +import { toast } from 'react-toastify'; export default function CreateJumlahPendudukMiskin() { const stateJPM = useProxy(jumlahPendudukMiskin); const [chartData, setChartData] = useState([]); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { stateJPM.create.form = { @@ -22,27 +24,33 @@ export default function CreateJumlahPendudukMiskin() { }; const handleSubmit = async () => { - const id = await stateJPM.create.create(); - if (id) { - const idStr = String(id); - await stateJPM.findUnique.load(idStr); - if (stateJPM.findUnique.data) { - setChartData([stateJPM.findUnique.data]); + try { + setIsSubmitting(true); + const id = await stateJPM.create.create(); + if (id) { + const idStr = String(id); + await stateJPM.findUnique.load(idStr); + if (stateJPM.findUnique.data) { + setChartData([stateJPM.findUnique.data]); + } } + resetForm(); + router.push('/admin/ekonomi/jumlah-penduduk-miskin'); + } catch (error) { + console.error(error) + toast.error(error instanceof Error ? error.message : "Gagal menambahkan jumlah penduduk miskin") + } finally { + setIsSubmitting(false); } - resetForm(); - router.push('/admin/ekonomi/jumlah-penduduk-miskin'); }; return ( {/* Header */} - - - + Tambah Jumlah Penduduk Miskin @@ -61,7 +69,7 @@ export default function CreateJumlahPendudukMiskin() { { const value = e.currentTarget.value; @@ -73,7 +81,7 @@ export default function CreateJumlahPendudukMiskin() { { stateJPM.create.form.totalPoorPopulation = Number(e.currentTarget.value); @@ -82,6 +90,17 @@ export default function CreateJumlahPendudukMiskin() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/page.tsx index 57fde078..c588820c 100644 --- a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/page.tsx @@ -16,11 +16,10 @@ import { TableThead, TableTr, Text, - Title, - Tooltip, + Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; -import { IconEdit, IconSearch, IconTrash, IconPlus } from '@tabler/icons-react'; +import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; import { useProxy } from 'valtio/utils'; @@ -101,18 +100,16 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) { Daftar Jumlah Penduduk Miskin - - - + diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/_lib/layoutTabs.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/_lib/layoutTabs.tsx index 8b884c50..ee70c101 100644 --- a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/_lib/layoutTabs.tsx +++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/_lib/layoutTabs.tsx @@ -2,18 +2,17 @@ 'use client' import colors from '@/con/colors'; import { + ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, - Title, - Tooltip, - ScrollArea, + Title } from '@mantine/core'; +import { IconSchool, IconUsers } from '@tabler/icons-react'; import { usePathname, useRouter } from 'next/navigation'; import React, { useEffect, useState } from 'react'; -import { IconUsers, IconSchool } from '@tabler/icons-react'; function LayoutTabs({ children }: { children: React.ReactNode }) { const router = useRouter(); @@ -24,15 +23,13 @@ function LayoutTabs({ children }: { children: React.ReactNode }) { label: "Pengangguran Berdasarkan Usia", value: "pengangguranberdasarkanusia", href: "/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia", - icon: , - tooltip: "Data pengangguran menurut kelompok usia", + icon: }, { label: "Pengangguran Berdasarkan Pendidikan", value: "pengangguranberdasarkanpendidikan", href: "/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan", - icon: , - tooltip: "Data pengangguran menurut tingkat pendidikan", + icon: }, ]; @@ -78,26 +75,19 @@ function LayoutTabs({ children }: { children: React.ReactNode }) { }} > {tabs.map((tab, i) => ( - - - {tab.label} - - + {tab.label} + ))} diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/[id]/page.tsx index 7e069310..e2991004 100644 --- a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/[id]/page.tsx @@ -2,10 +2,11 @@ 'use client'; import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur'; import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Stack, TextInput, Title, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Loader, Paper, Stack, TextInput, Title } from '@mantine/core'; import { IconArrowBack } 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'; function EditGrafikBerdasarkanPendidikan() { @@ -13,6 +14,7 @@ function EditGrafikBerdasarkanPendidikan() { const params = useParams() as { id: string }; const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanPendidikan); const id = params.id; + const [isSubmitting, setIsSubmitting] = useState(false); // state lokal untuk form const [formData, setFormData] = useState({ @@ -23,6 +25,14 @@ function EditGrafikBerdasarkanPendidikan() { S1: '', }); + const [originalData, setOriginalData] = useState({ + SD: '', + SMP: '', + SMA: '', + D3: '', + S1: '', + }); + useEffect(() => { if (id) { stategrafik.findUnique.load(id).then(() => { @@ -35,41 +45,64 @@ function EditGrafikBerdasarkanPendidikan() { D3: data.D3 || '', S1: data.S1 || '', }); + setOriginalData({ + SD: data.SD || '', + SMP: data.SMP || '', + SMA: data.SMA || '', + D3: data.D3 || '', + S1: data.S1 || '', + }); } }); } }, [id]); - const handleChange = (field: keyof typeof formData) => - (e: React.ChangeEvent) => { - setFormData((prev) => ({ - ...prev, - [field]: e.currentTarget.value, - })); - }; + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.currentTarget; + setFormData((prev) => ({ ...prev, [name]: value })); + }; + + + + const handleResetForm = () => { + setFormData({ + SD: originalData.SD, + SMP: originalData.SMP, + SMA: originalData.SMA, + D3: originalData.D3, + S1: originalData.S1, + }); + toast.info("Form dikembalikan ke data awal"); + }; const handleSubmit = async () => { - stategrafik.update.id = id; - stategrafik.update.form = { ...formData }; // update global state pas submit aja - await stategrafik.update.submit(); - router.push( - '/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan' - ); + try { + setIsSubmitting(true); + stategrafik.update.id = id; + stategrafik.update.form = { ...formData }; // update global state pas submit aja + await stategrafik.update.submit(); + router.push( + '/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan' + ); + } catch (error) { + console.error(error); + toast.error('Terjadi kesalahan saat memperbarui data grafik'); + } finally { + setIsSubmitting(false); + } }; return ( - - - + Edit Grafik Pengangguran Berdasarkan Pendidikan @@ -85,42 +118,58 @@ function EditGrafikBerdasarkanPendidikan() { > + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/create/page.tsx index e57a797a..c530352d 100644 --- a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/create/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/create/page.tsx @@ -1,19 +1,20 @@ 'use client'; /* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-explicit-any */ -import React from 'react'; -import { useRouter } from 'next/navigation'; import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur'; -import { useProxy } from 'valtio/utils'; -import { useState } from 'react'; import colors from '@/con/colors'; -import { Box, Button, Paper, Stack, Title, TextInput, Group, Tooltip } from '@mantine/core'; +import { Box, Button, Loader, Group, Paper, Stack, TextInput, Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; function CreateGrafikBerdasarkanPendidikan() { const router = useRouter(); const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanPendidikan); const [donutData, setDonutData] = useState([]); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { stategrafik.create.form = { @@ -27,28 +28,34 @@ function CreateGrafikBerdasarkanPendidikan() { }; const handleSubmit = async () => { - const id = await stategrafik.create.create(); - if (id) { - const idStr = String(id); - await stategrafik.findUnique.load(idStr); - if (stategrafik.findUnique.data) { - setDonutData([stategrafik.findUnique.data]); + try { + setIsSubmitting(true); + const id = await stategrafik.create.create(); + if (id) { + const idStr = String(id); + await stategrafik.findUnique.load(idStr); + if (stategrafik.findUnique.data) { + setDonutData([stategrafik.findUnique.data]); + } } + resetForm(); + router.push( + '/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan' + ); + } catch (error) { + console.error(error); + toast.error('Terjadi kesalahan saat menambahkan data grafik'); + } finally { + setIsSubmitting(false); } - resetForm(); - router.push( - '/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan' - ); }; return ( - - - + Tambah Data Pengangguran Berdasarkan Pendidikan @@ -67,7 +74,7 @@ function CreateGrafikBerdasarkanPendidikan() { label="SD" type="number" placeholder="Masukkan jumlah" - defaultValue={stategrafik.create.form.SD} + value={stategrafik.create.form.SD} onChange={(val) => (stategrafik.create.form.SD = val.currentTarget.value)} required /> @@ -75,7 +82,7 @@ function CreateGrafikBerdasarkanPendidikan() { label="SMP" type="number" placeholder="Masukkan jumlah" - defaultValue={stategrafik.create.form.SMP} + value={stategrafik.create.form.SMP} onChange={(val) => (stategrafik.create.form.SMP = val.currentTarget.value)} required /> @@ -83,7 +90,7 @@ function CreateGrafikBerdasarkanPendidikan() { label="SMA" type="number" placeholder="Masukkan jumlah" - defaultValue={stategrafik.create.form.SMA} + value={stategrafik.create.form.SMA} onChange={(val) => (stategrafik.create.form.SMA = val.currentTarget.value)} required /> @@ -91,7 +98,7 @@ function CreateGrafikBerdasarkanPendidikan() { label="D3" type="number" placeholder="Masukkan jumlah" - defaultValue={stategrafik.create.form.D3} + value={stategrafik.create.form.D3} onChange={(val) => (stategrafik.create.form.D3 = val.currentTarget.value)} required /> @@ -99,12 +106,23 @@ function CreateGrafikBerdasarkanPendidikan() { label="S1" type="number" placeholder="Masukkan jumlah" - defaultValue={stategrafik.create.form.S1} + value={stategrafik.create.form.S1} onChange={(val) => (stategrafik.create.form.S1 = val.currentTarget.value)} required /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/page.tsx index 74137342..825f6358 100644 --- a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/page.tsx @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ 'use client' import colors from '@/con/colors'; +import { DonutChart } from '@mantine/charts'; import { Box, Button, @@ -17,15 +18,13 @@ import { TableThead, TableTr, Text, - Title, - Tooltip, + Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; import { useProxy } from 'valtio/utils'; -import { DonutChart } from '@mantine/charts'; import HeaderSearch from '../../../_com/header'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import grafikNganggur from '../../../_state/ekonomi/usia-kerja-nganggur'; @@ -116,20 +115,18 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) { List Pengangguran Berdasarkan Pendidikan - - - + @@ -165,34 +162,30 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) { {item.D3} {item.S1} - - - + - - - + )) @@ -224,20 +217,22 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) { Grafik Pengangguran Berdasarkan Pendidikan - {donutData.length > 0 ? ( - - ) : ( - - Belum ada data untuk ditampilkan dalam grafik - - )} +
+ {donutData.length > 0 ? ( + + ) : ( + + Belum ada data untuk ditampilkan dalam grafik + + )} +
diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/[id]/page.tsx index f5e77711..ce974a57 100644 --- a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/[id]/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/[id]/page.tsx @@ -5,18 +5,18 @@ import colors from '@/con/colors'; import { Box, Button, + Group, + Loader, Paper, Stack, TextInput, - Title, - Group, - Tooltip, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; -import { useProxy } from 'valtio/utils'; import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() { const router = useRouter(); @@ -26,6 +26,8 @@ function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() { ); const id = params.id; + const [isSubmitting, setIsSubmitting] = useState(false); + // ✅ state lokal, controlled const [formData, setFormData] = useState({ usia18_25: '', @@ -34,6 +36,13 @@ function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() { usia46_keatas: '', }); + const [originalData, setOriginalData] = useState({ + usia18_25: '', + usia26_35: '', + usia36_45: '', + usia46_keatas: '', + }); + // load data dari global state -> masukin ke local state useEffect(() => { if (id) { @@ -46,6 +55,13 @@ function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() { usia36_45: data.usia36_45 || '', usia46_keatas: data.usia46_keatas || '', }); + setOriginalData({ + usia18_25: data.usia18_25 || '', + usia26_35: data.usia26_35 || '', + usia36_45: data.usia36_45 || '', + usia46_keatas: data.usia46_keatas || '', + }); + } }); } @@ -58,8 +74,19 @@ function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() { })); }; + const handleResetForm = () => { + setFormData({ + usia18_25: originalData.usia18_25, + usia26_35: originalData.usia26_35, + usia36_45: originalData.usia36_45, + usia46_keatas: originalData.usia46_keatas, + }); + toast.info('Form dikembalikan ke data awal'); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); // ✅ baru update global state pas submit stategrafik.update.id = id; stategrafik.update.form = { ...formData }; @@ -73,22 +100,22 @@ function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() { } catch (error) { console.error(error); toast.error('Terjadi kesalahan saat memperbarui data grafik'); + } finally { + setIsSubmitting(false); } }; return ( - - - + Edit Grafik Pengangguran Berdasarkan Usia Kerja @@ -137,6 +164,17 @@ function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create/page.tsx index 77c362fd..677d488b 100644 --- a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create/page.tsx @@ -2,18 +2,20 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ 'use client'; -import React, { useState } from 'react'; -import { useRouter } from 'next/navigation'; -import { useProxy } from 'valtio/utils'; import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur'; import colors from '@/con/colors'; -import { Box, Button, Paper, Stack, Title, TextInput, Group, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Loader, Paper, Stack, TextInput, Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; function CreateGrafikBerdasarkanUsiaKerjaYangMenganggur() { const router = useRouter(); const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanUsiaKerjaNganggur); const [donutData, setDonutData] = useState([]); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { stategrafik.create.form = { @@ -26,27 +28,33 @@ function CreateGrafikBerdasarkanUsiaKerjaYangMenganggur() { }; const handleSubmit = async () => { - const id = await stategrafik.create.create(); - if (id) { - const idStr = String(id); - await stategrafik.findUnique.load(idStr); - if (stategrafik.findUnique.data) { - setDonutData([stategrafik.findUnique.data]); + try { + setIsSubmitting(true); + const id = await stategrafik.create.create(); + if (id) { + const idStr = String(id); + await stategrafik.findUnique.load(idStr); + if (stategrafik.findUnique.data) { + setDonutData([stategrafik.findUnique.data]); + } } + resetForm(); + router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia'); + } catch (error) { + console.error('Error creating:', error); + toast.error('Terjadi kesalahan saat membuat data'); + } finally { + setIsSubmitting(false); } - resetForm(); - router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia'); }; return ( {/* Header */} - - - + Tambah Data Pengangguran Berdasarkan Usia @@ -66,7 +74,7 @@ function CreateGrafikBerdasarkanUsiaKerjaYangMenganggur() { label="Usia 18 - 25" type="number" placeholder="Masukkan jumlah" - defaultValue={stategrafik.create.form.usia18_25} + value={stategrafik.create.form.usia18_25} onChange={(val) => (stategrafik.create.form.usia18_25 = val.currentTarget.value)} required /> @@ -74,7 +82,7 @@ function CreateGrafikBerdasarkanUsiaKerjaYangMenganggur() { label="Usia 26 - 35" type="number" placeholder="Masukkan jumlah" - defaultValue={stategrafik.create.form.usia26_35} + value={stategrafik.create.form.usia26_35} onChange={(val) => (stategrafik.create.form.usia26_35 = val.currentTarget.value)} required /> @@ -82,7 +90,7 @@ function CreateGrafikBerdasarkanUsiaKerjaYangMenganggur() { label="Usia 36 - 45" type="number" placeholder="Masukkan jumlah" - defaultValue={stategrafik.create.form.usia36_45} + value={stategrafik.create.form.usia36_45} onChange={(val) => (stategrafik.create.form.usia36_45 = val.currentTarget.value)} required /> @@ -90,13 +98,24 @@ function CreateGrafikBerdasarkanUsiaKerjaYangMenganggur() { label="Usia 46 +" type="number" placeholder="Masukkan jumlah" - defaultValue={stategrafik.create.form.usia46_keatas} + value={stategrafik.create.form.usia46_keatas} onChange={(val) => (stategrafik.create.form.usia46_keatas = val.currentTarget.value)} required /> {/* Submit Button */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/[id]/edit/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/[id]/edit/page.tsx index 22ae288d..9b6b8872 100644 --- a/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/[id]/edit/page.tsx @@ -7,6 +7,7 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, @@ -31,6 +32,7 @@ function EditDetailDataPengangguran() { const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran); const router = useRouter(); const params = useParams(); + const [isSubmitting, setIsSubmitting] = useState(false); // --- state lokal form const [formData, setFormData] = useState({ @@ -42,6 +44,15 @@ function EditDetailDataPengangguran() { percentageChange: 0, }); + const [originalData, setOriginalData] = useState({ + month: '', + year: new Date().getFullYear(), + educatedUnemployment: 0, + uneducatedUnemployment: 0, + totalUnemployment: 0, + percentageChange: 0, + }); + // --- hitung total + persentase perubahan const calculateTotalAndChange = useCallback( async (data: typeof formData) => { @@ -109,6 +120,15 @@ function EditDetailDataPengangguran() { totalUnemployment: data.totalUnemployment, percentageChange: data.percentageChange || 0, }); + + setOriginalData({ + month: data.month, + year: yearValue, + educatedUnemployment: data.educatedUnemployment, + uneducatedUnemployment: data.uneducatedUnemployment, + totalUnemployment: data.totalUnemployment, + percentageChange: data.percentageChange || 0, + }); } catch (err) { console.error('Error loading detail:', err); toast.error('Gagal memuat data detail'); @@ -118,9 +138,22 @@ function EditDetailDataPengangguran() { loadDetail(); }, [params?.id]); + const handleResetForm = () => { + setFormData({ + month: originalData.month, + year: originalData.year, + educatedUnemployment: originalData.educatedUnemployment, + uneducatedUnemployment: originalData.uneducatedUnemployment, + totalUnemployment: originalData.totalUnemployment, + percentageChange: originalData.percentageChange, + }); + toast.info("Form dikembalikan ke data awal"); + }; + // --- submit form const handleSubmit = async () => { try { + setIsSubmitting(true); const { total, percentageChange } = await calculateTotalAndChange(formData); stateDetail.update.form = { @@ -137,6 +170,8 @@ function EditDetailDataPengangguran() { } catch (err) { console.error('Error updating:', err); toast.error('Terjadi kesalahan saat memperbarui data'); + } finally { + setIsSubmitting(false); } }; @@ -205,6 +240,17 @@ function EditDetailDataPengangguran() { + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/[id]/page.tsx index a4043b22..3138479b 100644 --- a/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/[id]/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/[id]/page.tsx @@ -2,7 +2,7 @@ import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import jumlahPengangguranState from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-pengangguran'; import colors from '@/con/colors'; -import { Box, Button, Flex, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -103,32 +103,28 @@ function DetailJumlahPengangguran() { {/* Tombol Edit & Hapus */} - - - + - - - +
diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/create/page.tsx index df0cb26a..d27bef0f 100644 --- a/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/create/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/create/page.tsx @@ -7,23 +7,25 @@ import { Box, Button, Group, + Loader, + NumberInput, Paper, + Select, Stack, Text, - NumberInput, - Title, - Select, - Tooltip, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; +import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; function CreateJumlahPengangguran() { const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran); const [chartData, setChartData] = useState([]); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const monthOptions = [ 'Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', @@ -73,15 +75,23 @@ function CreateJumlahPengangguran() { }; const handleSubmit = async () => { - await calculateTotalAndChange(); - const id = await stateDetail.create.create(); - if (id) { - await stateDetail.findUnique.load(String(id)); - if (stateDetail.findUnique.data) { - setChartData([stateDetail.findUnique.data]); + try { + setIsSubmitting(true); + await calculateTotalAndChange(); + const id = await stateDetail.create.create(); + if (id) { + await stateDetail.findUnique.load(String(id)); + if (stateDetail.findUnique.data) { + setChartData([stateDetail.findUnique.data]); + } + resetForm(); + router.push('/admin/ekonomi/jumlah-pengangguran'); } - resetForm(); - router.push('/admin/ekonomi/jumlah-pengangguran'); + } catch (error) { + console.error("Error creating jumlah pengangguran:", error); + toast.error("Gagal menambahkan data pengangguran"); + } finally { + setIsSubmitting(false); } }; @@ -89,16 +99,14 @@ function CreateJumlahPengangguran() { {/* Header */} - - - + Tambah Data Pengangguran @@ -179,7 +187,19 @@ function CreateJumlahPengangguran() { {/* Action Button */} - + + {/* Tombol Batal */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/page.tsx index ff3a8209..698ba948 100644 --- a/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/page.tsx @@ -1,17 +1,17 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ 'use client' import colors from '@/con/colors'; +import { BarChart } from '@mantine/charts'; import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, - Text, Title, Tooltip + Text, Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; import { useProxy } from 'valtio/utils'; -import { BarChart } from '@mantine/charts'; import HeaderSearch from '../../_com/header'; import jumlahPengangguranState from '../../_state/ekonomi/jumlah-pengangguran'; @@ -85,16 +85,14 @@ function ListDetailDataPengangguran({ search }: { search: string }) { Daftar Detail Data Pengangguran - - - + diff --git a/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/[id]/edit/page.tsx b/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/[id]/edit/page.tsx index ec501ee0..0cc7b4b6 100644 --- a/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/[id]/edit/page.tsx @@ -7,12 +7,12 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -24,6 +24,7 @@ function EditLowonganKerja() { const lowonganState = useProxy(lowonganKerjaState); const router = useRouter(); const params = useParams(); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ posisi: '', @@ -36,6 +37,17 @@ function EditLowonganKerja() { notelp: '', }); + const [originalData, setOriginalData] = useState({ + posisi: '', + namaPerusahaan: '', + lokasi: '', + tipePekerjaan: '', + gaji: '', + deskripsi: '', + kualifikasi: '', + notelp: '', + }) + // load data sekali aja ketika mount / id berubah useEffect(() => { const loadLowongan = async () => { @@ -55,6 +67,16 @@ function EditLowonganKerja() { kualifikasi: data.kualifikasi || '', notelp: data.notelp || '', }); + setOriginalData({ + posisi: data.posisi || '', + namaPerusahaan: data.namaPerusahaan || '', + lokasi: data.lokasi || '', + tipePekerjaan: data.tipePekerjaan || '', + gaji: data.gaji || '', + deskripsi: data.deskripsi || '', + kualifikasi: data.kualifikasi || '', + notelp: data.notelp || '', + }); } } catch (error) { console.error("Error loading lowongan kerja:", error); @@ -71,9 +93,23 @@ function EditLowonganKerja() { [field]: value, })); }; + const handleResetForm = () => { + setFormData({ + posisi: originalData.posisi, + namaPerusahaan: originalData.namaPerusahaan, + lokasi: originalData.lokasi, + tipePekerjaan: originalData.tipePekerjaan, + gaji: originalData.gaji, + deskripsi: originalData.deskripsi, + kualifikasi: originalData.kualifikasi, + notelp: originalData.notelp, + }); + toast.info("Form dikembalikan ke data awal"); + }; const handleSubmit = async () => { try { + setIsSubmitting(true); lowonganState.update.id = params?.id as string; lowonganState.update.form = { ...formData }; @@ -83,18 +119,17 @@ function EditLowonganKerja() { } catch (error) { console.error("Error updating lowongan kerja:", error); toast.error("Terjadi kesalahan saat memperbarui lowongan kerja"); + } finally { + setIsSubmitting(false); } }; return ( - {/* Header dengan tombol back */} - - - + Edit Lowongan Kerja Lokal @@ -179,6 +214,17 @@ function EditLowonganKerja() { + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/[id]/page.tsx index 980529aa..c9776908 100644 --- a/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/[id]/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/[id]/page.tsx @@ -1,6 +1,6 @@ 'use client' -import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -108,32 +108,28 @@ function DetailLowonganKerjaLokal() { - - - + - - - + diff --git a/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/create/page.tsx index 606100e8..4ee31ec4 100644 --- a/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/create/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/create/page.tsx @@ -4,22 +4,25 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useProxy } from 'valtio/utils'; import CreateEditor from '../../../_com/createEditor'; import lowonganKerjaState from '../../../_state/ekonomi/lowongan-kerja'; +import { useState } from 'react'; +import { toast } from 'react-toastify'; function CreateLowonganKerja() { const lowonganState = useProxy(lowonganKerjaState); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { lowonganState.create.form = { @@ -35,25 +38,32 @@ function CreateLowonganKerja() { }; const handleSubmit = async () => { - await lowonganState.create.create(); - resetForm(); - router.push('/admin/ekonomi/lowongan-kerja-lokal'); + try { + setIsSubmitting(true); + await lowonganState.create.create(); + resetForm(); + router.push('/admin/ekonomi/lowongan-kerja-lokal'); + } catch (error) { + console.error('Error creating lowongan kerja:', error); + toast.error( + error instanceof Error ? error.message : 'Gagal membuat lowongan kerja' + ); + } finally { + setIsSubmitting(false); + } }; return ( - {/* Header dengan tombol kembali */} - - - + Tambah Lowongan Kerja Lokal @@ -70,7 +80,7 @@ function CreateLowonganKerja() { > (lowonganState.create.form.posisi = val.target.value) } @@ -79,7 +89,7 @@ function CreateLowonganKerja() { required /> (lowonganState.create.form.namaPerusahaan = val.target.value) } @@ -88,7 +98,7 @@ function CreateLowonganKerja() { required /> (lowonganState.create.form.notelp = val.target.value) } @@ -97,7 +107,7 @@ function CreateLowonganKerja() { required /> (lowonganState.create.form.lokasi = val.target.value) } @@ -106,7 +116,7 @@ function CreateLowonganKerja() { required /> (lowonganState.create.form.tipePekerjaan = val.target.value) } @@ -115,7 +125,7 @@ function CreateLowonganKerja() { required /> (lowonganState.create.form.gaji = val.target.value) } @@ -150,6 +160,17 @@ function CreateLowonganKerja() { {/* Tombol Simpan */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/page.tsx b/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/page.tsx index 2c4452ac..149bed60 100644 --- a/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/page.tsx @@ -16,8 +16,7 @@ import { TableThead, TableTr, Text, - Title, - Tooltip + Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react'; @@ -69,7 +68,6 @@ function ListLowonganKerjaLokal({ search }: { search: string }) { Daftar Lowongan Kerja Lokal - - diff --git a/src/app/admin/(dashboard)/ekonomi/pasar-desa/_lib/layoutTabs.tsx b/src/app/admin/(dashboard)/ekonomi/pasar-desa/_lib/layoutTabs.tsx index 98809ce9..a4edbe61 100644 --- a/src/app/admin/(dashboard)/ekonomi/pasar-desa/_lib/layoutTabs.tsx +++ b/src/app/admin/(dashboard)/ekonomi/pasar-desa/_lib/layoutTabs.tsx @@ -2,18 +2,17 @@ 'use client' import colors from '@/con/colors'; import { + ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, - Title, - Tooltip, - ScrollArea, + Title } from '@mantine/core'; +import { IconCategory, IconShoppingBag } from '@tabler/icons-react'; import { usePathname, useRouter } from 'next/navigation'; import React, { useEffect, useState } from 'react'; -import { IconShoppingBag, IconCategory } from '@tabler/icons-react'; function LayoutTabs({ children }: { children: React.ReactNode }) { const router = useRouter(); @@ -30,8 +29,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) { label: "Kategori Produk", value: "kategoriproduk", href: "/admin/ekonomi/pasar-desa/kategori-produk", - icon: , - tooltip: "Atur kategori produk pasar desa", + icon: }, ]; @@ -84,26 +82,19 @@ function LayoutTabs({ children }: { children: React.ReactNode }) { }} > {tabs.map((tab, i) => ( - - - {tab.label} - - + {tab.label} + ))} diff --git a/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/[id]/page.tsx index 451a4a71..12c82847 100644 --- a/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/[id]/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/[id]/page.tsx @@ -1,32 +1,32 @@ /* eslint-disable react-hooks/exhaustive-deps */ 'use client' -import React, { useEffect, useState } from 'react'; -import { useParams, useRouter } from 'next/navigation'; -import { useProxy } from 'valtio/utils'; -import pasarDesaState from '../../../../_state/ekonomi/pasar-desa/pasar-desa'; import colors from '@/con/colors'; import { Box, Button, Group, + Loader, Paper, Stack, - Title, - TextInput, - Tooltip, Text, + TextInput, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; +import { useParams, useRouter } from 'next/navigation'; +import React, { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; +import pasarDesaState from '../../../../_state/ekonomi/pasar-desa/pasar-desa'; function EditKategoriProduk() { const router = useRouter(); const params = useParams(); const id = params?.id as string; const statePasar = useProxy(pasarDesaState.kategoriProduk); - + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ nama: '' }); - const [loading, setLoading] = useState(true); + const [originalData, setOriginalData] = useState({ nama: '' }); useEffect(() => { const loadKategoriProduk = async () => { @@ -41,12 +41,11 @@ function EditKategoriProduk() { // simpan data ke state lokal setFormData({ nama: data.nama || '' }); + setOriginalData({ nama: data.nama || '' }); } } catch (error) { console.error('Error loading kategori produk:', error); toast.error('Gagal memuat data kategori produk'); - } finally { - setLoading(false); } }; @@ -60,8 +59,16 @@ function EditKategoriProduk() { })); }; + const handleResetForm = () => { + setFormData({ + nama: originalData.nama, + }); + toast.info('Form dikembalikan ke data awal'); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); if (!formData.nama.trim()) { toast.error('Nama kategori produk tidak boleh kosong'); return; @@ -82,27 +89,23 @@ function EditKategoriProduk() { } catch (error) { console.error('Error updating kategori produk:', error); toast.error('Terjadi kesalahan saat memperbarui kategori produk'); + } finally { + setIsSubmitting(false); } }; - if (loading) { - return Loading...; - } - return ( {/* Header dengan tombol back */} - - - + Edit Kategori Produk @@ -128,6 +131,17 @@ function EditKategoriProduk() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/create/page.tsx index 93598be7..f7066fa2 100644 --- a/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/create/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/create/page.tsx @@ -5,15 +5,15 @@ import { Box, Button, Group, + Loader, Paper, Stack, TextInput, - Title, - Tooltip + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; import pasarDesaState from '../../../../_state/ekonomi/pasar-desa/pasar-desa'; @@ -21,6 +21,7 @@ import pasarDesaState from '../../../../_state/ekonomi/pasar-desa/pasar-desa'; function CreateKategoriProduk() { const router = useRouter(); const statePasar = useProxy(pasarDesaState.kategoriProduk); + const [isSubmitting, setIsSubmitting] = useState(false); useEffect(() => { statePasar.findMany.load(); @@ -33,29 +34,34 @@ function CreateKategoriProduk() { }; const handleSubmit = async () => { - if (!statePasar.create.form.nama) { - return toast.warn('Nama kategori produk wajib diisi'); + try { + if (!statePasar.create.form.nama) { + return toast.warn('Nama kategori produk wajib diisi'); + } + setIsSubmitting(true); + await statePasar.create.create(); + resetForm(); + router.push('/admin/ekonomi/pasar-desa/kategori-produk'); + } catch (error) { + console.error(error) + toast.error('Gagal menambahkan kategori produk'); + } finally { + setIsSubmitting(false); } - - await statePasar.create.create(); - resetForm(); - router.push('/admin/ekonomi/pasar-desa/kategori-produk'); }; return ( {/* Header dengan tombol kembali */} - - - + Tambah Kategori Produk @@ -74,12 +80,23 @@ function CreateKategoriProduk() { (statePasar.create.form.nama = e.target.value)} required /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/page.tsx b/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/page.tsx index 0115aefd..9c83d9b9 100644 --- a/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/page.tsx @@ -1,6 +1,6 @@ 'use client' import colors from '@/con/colors'; -import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core'; +import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconEdit, IconPlus, IconSearch, IconX } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; @@ -68,7 +68,6 @@ function ListKategoriProduk({ search2 }: { search2: string }) { Daftar Kategori Produk - - @@ -99,7 +97,6 @@ function ListKategoriProduk({ search2 }: { search2: string }) { - - - - - + )) ) : ( diff --git a/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/[id]/edit/page.tsx index 17ebd925..70cb1458 100644 --- a/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/[id]/edit/page.tsx @@ -5,17 +5,18 @@ import pasarDesaState from '@/app/admin/(dashboard)/_state/ekonomi/pasar-desa/pa import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, MultiSelect, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; @@ -41,6 +42,7 @@ function EditPasarDesa() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ nama: '', harga: 0, @@ -51,6 +53,17 @@ function EditPasarDesa() { kontak: '', }); + const [originalData, setOriginalData] = useState({ + nama: '', + harga: 0, + alamatUsaha: '', + imageId: '', + imageUrl: "", + rating: 0, + kategoriId: [], + kontak: '', + }); + // load data awal useEffect(() => { pasarState.kategoriProduk.findManyAll.load(); @@ -71,6 +84,16 @@ function EditPasarDesa() { kategoriId: data.KategoriToPasar?.map((k: any) => k.kategoriId) || [], kontak: data.kontak || '', }); + setOriginalData({ + nama: data.nama || '', + harga: data.harga || 0, + alamatUsaha: data.alamatUsaha || '', + imageId: data.imageId || '', + imageUrl: data.image?.link || "", + rating: data.rating || 0, + kategoriId: data.KategoriToPasar?.map((k: any) => k.kategoriId) || [], + kontak: data.kontak || '', + }); if (data.image?.link) setPreviewImage(data.image.link); } } catch (error) { @@ -88,8 +111,25 @@ function EditPasarDesa() { setFormData((prev) => ({ ...prev, [key]: value })); }; + const handleResetForm = () => { + setFormData({ + nama: originalData.nama, + harga: originalData.harga, + alamatUsaha: originalData.alamatUsaha, + imageId: originalData.imageId, + rating: originalData.rating, + kategoriId: (originalData as any)?.KategoriToPasar?.map((k: any) => k.kategoriId) || [], + kontak: originalData.kontak, + }); + setPreviewImage(originalData.imageUrl || null); + setFile(null); + toast.info("Form dikembalikan ke data awal"); + }; + + const handleSubmit = async () => { try { + setIsSubmitting(true); // upload image kalau ada file baru let imageId = formData.imageId; if (file) { @@ -111,17 +151,17 @@ function EditPasarDesa() { } catch (error) { console.error('Error updating pasar desa:', error); toast.error('Terjadi kesalahan saat memperbarui pasar desa'); + } finally { + setIsSubmitting(false); } }; return ( - - - + Edit Pasar Desa @@ -151,7 +191,7 @@ function EditPasarDesa() { }} onReject={() => toast.error('File tidak valid, gunakan format gambar')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} radius="md" p="xl" > @@ -170,25 +210,45 @@ function EditPasarDesa() { Seret gambar atau klik untuk memilih file - Maksimal 5MB, format gambar wajib + Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp {previewImage && ( - + + + {/* Tombol hapus (pojok kanan atas) */} + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -257,6 +317,17 @@ function EditPasarDesa() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/[id]/page.tsx index 96f52523..a36b7d9d 100644 --- a/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/[id]/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/[id]/page.tsx @@ -1,13 +1,13 @@ 'use client' +import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import colors from '@/con/colors'; -import { Box, Button, Paper, Stack, Text, Image, Skeleton, Group, Tooltip } from '@mantine/core'; -import { IconArrowBack, IconTrash, IconEdit } from '@tabler/icons-react'; -import { useRouter, useParams } from 'next/navigation'; -import React, { useState } from 'react'; +import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; +import { useShallowEffect } from '@mantine/hooks'; +import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; +import { useParams, useRouter } from 'next/navigation'; +import { useState } from 'react'; import { useProxy } from 'valtio/utils'; import pasarDesaState from '../../../../_state/ekonomi/pasar-desa/pasar-desa'; -import { useShallowEffect } from '@mantine/hooks'; -import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; function DetailPasarDesa() { const statePasar = useProxy(pasarDesaState); @@ -123,32 +123,28 @@ function DetailPasarDesa() { - - - + - - - + diff --git a/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/create/page.tsx index 72265081..10202db1 100644 --- a/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/create/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/create/page.tsx @@ -3,17 +3,18 @@ import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, MultiSelect, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; @@ -28,6 +29,7 @@ export default function CreatePasarDesa() { const statePasar = useProxy(pasarDesaState); const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); useEffect(() => { statePasar.kategoriProduk.findManyAll.load(); @@ -48,36 +50,42 @@ export default function CreatePasarDesa() { }; const handleSubmit = async () => { - if (!file) { - return toast.warn('Silakan pilih file gambar terlebih dahulu'); + try { + setIsSubmitting(true); + if (!file) { + return toast.warn('Silakan pilih file gambar terlebih dahulu'); + } + + const res = await ApiFetch.api.fileStorage.create.post({ + file, + name: file.name, + }); + + const uploaded = res.data?.data; + if (!uploaded?.id) { + return toast.error('Gagal mengunggah gambar, silakan coba lagi'); + } + + statePasar.pasarDesa.create.form.imageId = uploaded.id; + await statePasar.pasarDesa.create.create(); + + resetForm(); + router.push('/admin/ekonomi/pasar-desa/produk-pasar-desa'); + } catch (error) { + console.error('Error creating kategori produk:', error); + toast.error('Gagal membuat kategori produk'); + } finally { + setIsSubmitting(false); } - - const res = await ApiFetch.api.fileStorage.create.post({ - file, - name: file.name, - }); - - const uploaded = res.data?.data; - if (!uploaded?.id) { - return toast.error('Gagal mengunggah gambar, silakan coba lagi'); - } - - statePasar.pasarDesa.create.form.imageId = uploaded.id; - await statePasar.pasarDesa.create.create(); - - resetForm(); - router.push('/admin/ekonomi/pasar-desa/produk-pasar-desa'); }; return ( {/* Header dengan tombol kembali */} - - - + Tambah Produk Pasar Desa @@ -108,7 +116,7 @@ export default function CreatePasarDesa() { }} onReject={() => toast.error('File tidak valid, gunakan format gambar')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} radius="md" p="xl" > @@ -129,7 +137,7 @@ export default function CreatePasarDesa() { {previewImage && ( - + + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -145,7 +171,7 @@ export default function CreatePasarDesa() { (statePasar.pasarDesa.create.form.nama = e.target.value)} required /> @@ -155,7 +181,7 @@ export default function CreatePasarDesa() { type="number" label="Harga Produk" placeholder="Masukkan harga produk" - defaultValue={statePasar.pasarDesa.create.form.harga} + value={statePasar.pasarDesa.create.form.harga} onChange={(e) => (statePasar.pasarDesa.create.form.harga = Number(e.target.value))} required /> @@ -168,7 +194,7 @@ export default function CreatePasarDesa() { step={0.1} label="Rating Produk (0–5)" placeholder="Masukkan rating produk" - defaultValue={statePasar.pasarDesa.create.form.rating} + value={statePasar.pasarDesa.create.form.rating} onChange={(e) => { const value = Number(e.target.value); if (value >= 0 && value <= 5) { @@ -181,7 +207,7 @@ export default function CreatePasarDesa() { (statePasar.pasarDesa.create.form.alamatUsaha = e.target.value)} /> @@ -190,7 +216,7 @@ export default function CreatePasarDesa() { label="Kontak" type="number" placeholder="Masukkan kontak" - defaultValue={statePasar.pasarDesa.create.form.kontak} + value={statePasar.pasarDesa.create.form.kontak} onChange={(e) => (statePasar.pasarDesa.create.form.kontak = e.target.value)} /> @@ -210,6 +236,17 @@ export default function CreatePasarDesa() { {/* Tombol Submit */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/page.tsx b/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/page.tsx index fc14e3f3..e7e1b444 100644 --- a/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/page.tsx @@ -16,8 +16,7 @@ import { TableThead, TableTr, Text, - Title, - Tooltip + Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react'; @@ -68,18 +67,16 @@ function ListPasarDesa({ search }: { search: string }) { Daftar Produk Pasar Desa - - - + diff --git a/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/[id]/edit/page.tsx index 6dbb6b03..5eae8178 100644 --- a/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/[id]/edit/page.tsx @@ -9,18 +9,18 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; -import { useEffect, useState, useCallback } from 'react'; -import { useProxy } from 'valtio/utils'; +import { useCallback, useEffect, useState } from 'react'; import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; type Statistik = { tahun: string; @@ -50,6 +50,8 @@ function EditProgramKemiskinan() { const stateProgram = useProxy(programKemiskinanState); const [formData, setFormData] = useState(initialForm); + const [isSubmitting, setIsSubmitting] = useState(false); + const [originalData, setOriginalData] = useState(initialForm); // Load data 1x dari global state → isi local state useEffect(() => { @@ -69,6 +71,15 @@ function EditProgramKemiskinan() { jumlah: data.statistik?.jumlah?.toString() ?? '', }, }); + setOriginalData({ + nama: data.nama ?? '', + deskripsi: data.deskripsi ?? '', + icon: data.icon ?? '', + statistik: { + tahun: data.statistik?.tahun?.toString() ?? '', + jumlah: data.statistik?.jumlah?.toString() ?? '', + }, + }); } } catch (err) { console.error('Error load data:', err); @@ -100,8 +111,22 @@ function EditProgramKemiskinan() { [] ); + const handleResetForm = () => { + setFormData({ + nama: originalData.nama, + deskripsi: originalData.deskripsi, + icon: originalData.icon, + statistik: { + tahun: originalData.statistik.tahun, + jumlah: originalData.statistik.jumlah, + }, + }); + toast.info('Form dikembalikan ke data awal'); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); stateProgram.update.id = id; stateProgram.update.form = formData; await stateProgram.update.update(); @@ -111,6 +136,8 @@ function EditProgramKemiskinan() { } catch (error) { console.error('Error update program:', error); toast.error('Terjadi kesalahan saat memperbarui program'); + } finally { + setIsSubmitting(false); } }; @@ -118,16 +145,14 @@ function EditProgramKemiskinan() { {/* Header */} - - - + Edit Program Kemiskinan @@ -195,6 +220,17 @@ function EditProgramKemiskinan() { + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/[id]/page.tsx index 5e4acf64..25f58c09 100644 --- a/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/[id]/page.tsx @@ -8,17 +8,16 @@ import { Paper, Skeleton, Stack, - Text, - Tooltip, + Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useState } from 'react'; import { useProxy } from 'valtio/utils'; +import { IconKey, IconMapper } from '../../../_com/iconMap'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import programKemiskinanState from '../../../_state/ekonomi/program-kemiskinan'; -import { IconKey, IconMapper } from '../../../_com/iconMap'; function DetailProgramKemiskinan() { const programState = useProxy(programKemiskinanState); @@ -123,33 +122,29 @@ function DetailProgramKemiskinan() { {/* Action Buttons */} - - - + - - - + diff --git a/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/create/page.tsx index cf4c0696..2c00adf1 100644 --- a/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/create/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/create/page.tsx @@ -7,27 +7,28 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; -import { useProxy } from 'valtio/utils'; -import programKemiskinanState from '../../../_state/ekonomi/program-kemiskinan'; -import CreateEditor from '../../../_com/createEditor'; -import SelectIconProgram from '../../../_com/selectIcon'; import { useState } from 'react'; import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; +import CreateEditor from '../../../_com/createEditor'; +import SelectIconProgram from '../../../_com/selectIcon'; +import programKemiskinanState from '../../../_state/ekonomi/program-kemiskinan'; function CreateProgramKemiskinan() { const programState = useProxy(programKemiskinanState); const router = useRouter(); const [lineChart, setLineChart] = useState([]); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { programState.create.form = { nama: '', @@ -41,40 +42,46 @@ function CreateProgramKemiskinan() { }; const handleSubmit = async () => { - if (!programState.create.form.nama || !programState.create.form.deskripsi) { - return toast.warn('Judul dan deskripsi wajib diisi'); - } - - const id = await programState.create.create(); - if (id) { - const idStr = String(id); - await programState.findUnique.load(idStr); - if (programState.findUnique.data) { - setLineChart([programState.findUnique.data]); + try { + setIsSubmitting(true); + if (!programState.create.form.nama || !programState.create.form.deskripsi) { + return toast.warn('Judul dan deskripsi wajib diisi'); } - toast.success('Program berhasil ditambahkan'); - } else { - toast.error('Gagal menambahkan program, coba lagi'); - } - resetForm(); - router.push('/admin/ekonomi/program-kemiskinan'); + const id = await programState.create.create(); + if (id) { + const idStr = String(id); + await programState.findUnique.load(idStr); + if (programState.findUnique.data) { + setLineChart([programState.findUnique.data]); + } + toast.success('Program berhasil ditambahkan'); + } else { + toast.error('Gagal menambahkan program, coba lagi'); + } + + resetForm(); + router.push('/admin/ekonomi/program-kemiskinan'); + } catch (error) { + console.error('Gagal menyimpan data:', error); + toast.error('Terjadi kesalahan saat menyimpan data'); + } finally { + setIsSubmitting(false); + } }; return ( {/* Header dengan tombol back */} - - - + Tambah Program Kemiskinan @@ -93,7 +100,7 @@ function CreateProgramKemiskinan() { (programState.create.form.nama = val.target.value)} required /> @@ -128,7 +135,7 @@ function CreateProgramKemiskinan() { (programState.create.form.statistik.jumlah = val.target.value) } @@ -138,7 +145,7 @@ function CreateProgramKemiskinan() { /> (programState.create.form.statistik.tahun = val.target.value) } @@ -150,6 +157,17 @@ function CreateProgramKemiskinan() { {/* Tombol Submit */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/page.tsx b/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/page.tsx index def7ec5e..36935bee 100644 --- a/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/page.tsx @@ -1,7 +1,7 @@ 'use client' /* eslint-disable @typescript-eslint/no-explicit-any */ import colors from '@/con/colors'; -import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core'; +import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; @@ -69,11 +69,9 @@ function ListProgramKemiskinan({ search }: { search: string }) { Daftar Program Kemiskinan - - - +
diff --git a/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/[id]/edit/page.tsx index f746afeb..51122258 100644 --- a/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/[id]/edit/page.tsx @@ -1,31 +1,31 @@ /* eslint-disable react-hooks/exhaustive-deps */ 'use client' +import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import grafikSektorUnggulan from '@/app/admin/(dashboard)/_state/ekonomi/sektor-unggulan-desa'; import colors from '@/con/colors'; import { Box, Button, Group, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; -import { useProxy } from 'valtio/utils'; import { toast } from 'react-toastify'; -import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; +import { useProxy } from 'valtio/utils'; function EditSektorUnggulanDesa() { const router = useRouter(); const params = useParams() as { id: string }; const stateGrafik = useProxy(grafikSektorUnggulan); - + const [isSubmitting, setIsSubmitting] = useState(false); const id = params.id; // state lokal buat form @@ -35,6 +35,12 @@ function EditSektorUnggulanDesa() { value: 0, }); + const [originalData, setOriginalData] = useState({ + name: '', + description: '', + value: 0, + }); + // Load data saat komponen mount useEffect(() => { if (id) { @@ -48,6 +54,11 @@ function EditSektorUnggulanDesa() { description: data.description || '', value: data.value || 0, }); + setOriginalData({ + name: data.name || '', + description: data.description || '', + value: data.value || 0, + }); } }) .catch((err) => { @@ -59,13 +70,14 @@ function EditSektorUnggulanDesa() { const handleChange = (field: keyof typeof formData) => - (e: React.ChangeEvent) => { - const value = field === 'value' ? Number(e.currentTarget.value) : e.currentTarget.value; - setFormData((prev) => ({ ...prev, [field]: value })); - }; + (e: React.ChangeEvent) => { + const value = field === 'value' ? Number(e.currentTarget.value) : e.currentTarget.value; + setFormData((prev) => ({ ...prev, [field]: value })); + }; const handleSubmit = async () => { try { + setIsSubmitting(true); stateGrafik.update.id = id; stateGrafik.update.form = { ...formData }; // update global pas submit await stateGrafik.update.submit(); @@ -74,22 +86,31 @@ function EditSektorUnggulanDesa() { } catch (error) { console.error('Error update sektor unggulan:', error); toast.error('Terjadi kesalahan saat memperbarui sektor unggulan'); + } finally { + setIsSubmitting(false); } }; + const handleResetForm = () => { + setFormData({ + name: originalData.name, + description: originalData.description, + value: originalData.value, + }); + toast.info('Form dikembalikan ke data awal'); + }; + return ( - - - + Edit Sektor Unggulan Desa @@ -132,6 +153,17 @@ function EditSektorUnggulanDesa() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/[id]/page.tsx index 631a4f52..ee8c1561 100644 --- a/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/[id]/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/[id]/page.tsx @@ -8,8 +8,7 @@ import { Paper, Skeleton, Stack, - Text, - Tooltip, + Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; @@ -92,36 +91,32 @@ function DetailSektorUnggulanDesa() { {/* Tombol Aksi */} - - - + - - - + diff --git a/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/create/page.tsx index 4761de26..09021f87 100644 --- a/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/create/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/create/page.tsx @@ -6,24 +6,26 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; import { useProxy } from 'valtio/utils'; -import grafikSektorUnggulan from '../../../_state/ekonomi/sektor-unggulan-desa'; import CreateEditor from '../../../_com/createEditor'; +import grafikSektorUnggulan from '../../../_state/ekonomi/sektor-unggulan-desa'; +import { toast } from 'react-toastify'; function CreateSektorUnggulanDesa() { const stateGrafik = useProxy(grafikSektorUnggulan); const [chartData, setChartData] = useState([]); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { stateGrafik.create.form = { @@ -34,32 +36,38 @@ function CreateSektorUnggulanDesa() { }; const handleSubmit = async () => { - const id = await stateGrafik.create.create(); - if (id) { - const idStr = String(id); - await stateGrafik.findUnique.load(idStr); - if (stateGrafik.findUnique.data) { - setChartData([stateGrafik.findUnique.data]); + try { + setIsSubmitting(true); + const id = await stateGrafik.create.create(); + if (id) { + const idStr = String(id); + await stateGrafik.findUnique.load(idStr); + if (stateGrafik.findUnique.data) { + setChartData([stateGrafik.findUnique.data]); + } } + resetForm(); + router.push('/admin/ekonomi/sektor-unggulan-desa'); + } catch (error) { + console.error('Error creating sektor unggulan:', error); + toast.error('Terjadi kesalahan saat menambahkan sektor unggulan'); + } finally { + setIsSubmitting(false); } - resetForm(); - router.push('/admin/ekonomi/sektor-unggulan-desa'); }; return ( {/* Header dengan back button */} - - - + Tambah Sektor Unggulan Desa @@ -78,7 +86,7 @@ function CreateSektorUnggulanDesa() { { stateGrafik.create.form.name = e.currentTarget.value; }} @@ -101,7 +109,7 @@ function CreateSektorUnggulanDesa() { label="Jumlah" type="number" placeholder="Masukkan jumlah" - defaultValue={stateGrafik.create.form.value} + value={stateGrafik.create.form.value} onChange={(e) => { stateGrafik.create.form.value = Number(e.currentTarget.value); }} @@ -109,6 +117,17 @@ function CreateSektorUnggulanDesa() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/page.tsx b/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/page.tsx index 3a7cc1e8..74d1153d 100644 --- a/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/page.tsx @@ -16,8 +16,7 @@ import { TableThead, TableTr, Text, - Title, - Tooltip, + Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react'; @@ -92,11 +91,9 @@ function ListSektorUnggulanDesa({ search }: { search: string }) { List Sektor Unggulan Desa - - - + {loading ? ( @@ -127,16 +124,14 @@ function ListSektorUnggulanDesa({ search }: { search: string }) { - - - + )) diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/_lib/layoutTabs.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/_lib/layoutTabs.tsx index 4425aa88..c85ac68f 100644 --- a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/_lib/layoutTabs.tsx +++ b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/_lib/layoutTabs.tsx @@ -8,8 +8,7 @@ import { TabsList, TabsPanel, TabsTab, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconBuildingCommunity, @@ -28,22 +27,19 @@ function LayoutTabs({ children }: { children: React.ReactNode }) { label: "Pegawai", value: "pegawai", href: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai", - icon: , - tooltip: "Kelola data pegawai BUMDesa", + icon: }, { label: "Posisi Organisasi", value: "posisiorganisasi", href: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi", - icon: , - tooltip: "Kelola daftar posisi organisasi", + icon: }, { label: "Struktur Organisasi", value: "strukturorganisasi", href: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/struktur-organisasi", - icon: , - tooltip: "Kelola struktur organisasi BUMDesa" + icon: } ]; @@ -96,26 +92,19 @@ function LayoutTabs({ children }: { children: React.ReactNode }) { }} > {tabs.map((tab, i) => ( - - - {tab.label} - - + {tab.label} + ))} @@ -135,7 +124,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) { ))} - + ); } diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/[id]/edit/page.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/[id]/edit/page.tsx index c1a8eef5..89046ef6 100644 --- a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/[id]/edit/page.tsx @@ -5,17 +5,18 @@ import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Select, Stack, Text, TextInput, - Title, - Tooltip + Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; @@ -28,7 +29,7 @@ export default function EditPegawaiBumDes() { const router = useRouter(); const { id } = useParams<{ id: string }>(); const stateOrganisasi = useProxy(stateStrukturBumDes.pegawai); - + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ namaLengkap: '', gelarAkademik: '', @@ -40,6 +41,18 @@ export default function EditPegawaiBumDes() { posisiId: '', isActive: true, }); + const [originalData, setOriginalData] = useState({ + namaLengkap: '', + gelarAkademik: '', + imageId: '', + tanggalMasuk: '', + email: '', + telepon: '', + alamat: '', + posisiId: '', + isActive: true, + imageUrl: '' + }); const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); @@ -68,6 +81,18 @@ export default function EditPegawaiBumDes() { posisiId: data.posisiId || '', isActive: data.isActive ?? true, }); + setOriginalData({ + namaLengkap: data.namaLengkap || '', + gelarAkademik: data.gelarAkademik || '', + imageId: data.imageId || '', + tanggalMasuk: data.tanggalMasuk || '', + email: data.email || '', + telepon: data.telepon || '', + alamat: data.alamat || '', + posisiId: data.posisiId || '', + isActive: data.isActive ?? true, + imageUrl: data.image?.link || '', + }); setPreviewImage(data.image?.link || null); } @@ -80,8 +105,26 @@ export default function EditPegawaiBumDes() { loadPegawai(); }, [id]); + const handleResetForm = () => { + setFormData({ + namaLengkap: originalData.namaLengkap, + gelarAkademik: originalData.gelarAkademik, + imageId: originalData.imageId, + tanggalMasuk: originalData.tanggalMasuk, + email: originalData.email, + telepon: originalData.telepon, + alamat: originalData.alamat, + posisiId: originalData.posisiId, + isActive: originalData.isActive, + }); + setPreviewImage(originalData.imageUrl || null); + setFile(null); + toast.info("Form dikembalikan ke data awal"); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); if (!formData.namaLengkap.trim()) { return toast.error('Nama lengkap tidak boleh kosong'); } @@ -104,17 +147,17 @@ export default function EditPegawaiBumDes() { } catch (error) { console.error('Error updating pegawai:', error); toast.error(error instanceof Error ? error.message : 'Gagal memperbarui data pegawai'); + } finally { + setIsSubmitting(false); } }; return ( - - - + Edit Data Pegawai PPID @@ -167,13 +210,13 @@ export default function EditPegawaiBumDes() { Seret gambar atau klik untuk memilih file - Maksimal 5MB, format gambar wajib + Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp {previewImage && ( - + + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -247,10 +308,21 @@ export default function EditPegawaiBumDes() { {/* Submit Button */} - + + {/* Tombol Batal */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/[id]/page.tsx index 7a0626e4..0e0f0dd7 100644 --- a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/[id]/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/[id]/page.tsx @@ -3,7 +3,7 @@ import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirma import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi'; import colors from '@/con/colors'; -import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -150,32 +150,28 @@ function DetailPegawai() { - - - + - - - + diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/create/page.tsx index dbf697ee..c0f86fb0 100644 --- a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/create/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/create/page.tsx @@ -4,7 +4,7 @@ import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi'; import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; -import { Box, Button, Group, Image, Paper, Select, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core'; +import { ActionIcon, Box, Button, Group, Image, Loader, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; @@ -20,6 +20,8 @@ function CreatePegawaiBumDes() { stateStrukturBumDes.posisiOrganisasi.findManyAll.load(); resetForm(); }, []); + const [file, setFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { stateOrganisasi.create.form = { @@ -33,6 +35,8 @@ function CreatePegawaiBumDes() { posisiId: "", isActive: true, }; + setPreviewImage(null); + setFile(null); }; const handleSubmit = async () => { @@ -41,15 +45,18 @@ function CreatePegawaiBumDes() { } try { + setIsSubmitting(true); // Upload gambar dulu + if (!file) { + return toast.warn('Silakan pilih file gambar terlebih dahulu'); + } const res = await ApiFetch.api.fileStorage.create.post({ - file: previewImage.file, - name: previewImage.file.name, + file, + name: file.name, }); - const uploaded = res.data?.data; if (!uploaded?.id) { - return toast.error("Gagal upload gambar"); + return toast.error('Gagal mengunggah gambar, silakan coba lagi'); } // Set status aktif secara otomatis @@ -69,17 +76,17 @@ function CreatePegawaiBumDes() { } catch (error) { console.error("Error creating pegawai:", error); toast.error("Terjadi kesalahan saat menambahkan pegawai"); + } finally { + setIsSubmitting(false); } }; return ( - - - + Tambah Pegawai BUMDesa @@ -98,7 +105,7 @@ function CreatePegawaiBumDes() { (stateOrganisasi.create.form.namaLengkap = e.currentTarget.value)} required /> @@ -107,7 +114,7 @@ function CreatePegawaiBumDes() { (stateOrganisasi.create.form.gelarAkademik = e.currentTarget.value)} /> @@ -165,7 +172,7 @@ function CreatePegawaiBumDes() { {previewImage && ( - + Preview Gambar @@ -182,6 +189,24 @@ function CreatePegawaiBumDes() { }} loading='lazy' /> + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -190,7 +215,7 @@ function CreatePegawaiBumDes() { label="Tanggal Masuk" type="date" placeholder="Contoh: 2022-01-01" - defaultValue={stateOrganisasi.create.form.tanggalMasuk} + value={stateOrganisasi.create.form.tanggalMasuk} onChange={(e) => (stateOrganisasi.create.form.tanggalMasuk = e.currentTarget.value)} /> @@ -200,7 +225,7 @@ function CreatePegawaiBumDes() { label="Email" type="email" placeholder="Contoh: email@example.com" - defaultValue={stateOrganisasi.create.form.email} + value={stateOrganisasi.create.form.email} onChange={(e) => (stateOrganisasi.create.form.email = e.currentTarget.value)} /> @@ -209,7 +234,7 @@ function CreatePegawaiBumDes() { (stateOrganisasi.create.form.telepon = e.currentTarget.value)} /> @@ -218,7 +243,7 @@ function CreatePegawaiBumDes() { (stateOrganisasi.create.form.alamat = e.currentTarget.value)} /> @@ -244,6 +269,17 @@ function CreatePegawaiBumDes() { + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/page.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/page.tsx index d56609de..fd17d099 100644 --- a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/page.tsx @@ -1,7 +1,7 @@ /* eslint-disable react-hooks/exhaustive-deps */ 'use client' import colors from '@/con/colors'; -import { Badge, Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, ThemeIcon, Title, Tooltip } from '@mantine/core'; +import { Badge, Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, ThemeIcon, Title } from '@mantine/core'; import { IconCheck, IconDeviceImacCog, IconPlus, IconSearch, IconX } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; @@ -59,16 +59,14 @@ function ListPegawaiBumdes({ search }: { search: string }) { Daftar Pegawai BUMDesa - - - +
Tidak ada data pegawai yang ditemukan @@ -82,16 +80,14 @@ function ListPegawaiBumdes({ search }: { search: string }) { Daftar Pegawai BUMDesa - - - +
diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/[id]/page.tsx index 0d8f7ed7..a58d838c 100644 --- a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/[id]/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/[id]/page.tsx @@ -5,7 +5,7 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi'; import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Loader, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; @@ -17,6 +17,7 @@ function EditPosisiOrganisasiBumDes() { const params = useParams(); const id = params?.id as string; const stateOrganisasi = useProxy(stateStrukturBumDes.posisiOrganisasi); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ nama: '', @@ -24,6 +25,12 @@ function EditPosisiOrganisasiBumDes() { hierarki: 0, }); + const [originalData, setOriginalData] = useState({ + nama: '', + deskripsi: '', + hierarki: 0, + }); + // Fungsi generik untuk update formData const handleChange = (field: keyof typeof formData, value: any) => { setFormData(prev => ({ ...prev, [field]: value })); @@ -42,6 +49,11 @@ function EditPosisiOrganisasiBumDes() { deskripsi: data.deskripsi || '', hierarki: data.hierarki || 0, }); + setOriginalData({ + nama: data.nama || '', + deskripsi: data.deskripsi || '', + hierarki: data.hierarki || 0, + }); } } catch (err) { console.error('Error loading posisi organisasi:', err); @@ -52,6 +64,15 @@ function EditPosisiOrganisasiBumDes() { loadPosisiOrganisasi(); }, [id]); + const handleResetForm = () => { + setFormData({ + nama: originalData.nama, + deskripsi: originalData.deskripsi, + hierarki: originalData.hierarki, + }); + toast.info("Form dikembalikan ke data awal"); + }; + const handleSubmit = async () => { if (!formData.nama.trim()) { toast.error('Nama posisi organisasi tidak boleh kosong'); @@ -59,6 +80,7 @@ function EditPosisiOrganisasiBumDes() { } try { + setIsSubmitting(true); // Update global state hanya saat submit stateOrganisasi.edit.form = { nama: formData.nama.trim(), @@ -78,17 +100,17 @@ function EditPosisiOrganisasiBumDes() { } catch (err) { console.error('Error updating posisi organisasi:', err); // toast error biasanya sudah ada di update + } finally { + setIsSubmitting(false); } }; return ( - - - + Edit Posisi Organisasi BUMDes @@ -134,10 +156,21 @@ function EditPosisiOrganisasiBumDes() { required /> - + + {/* Tombol Batal */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/create/page.tsx index 2045244c..1b267c36 100644 --- a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/create/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/create/page.tsx @@ -3,59 +3,54 @@ import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor'; import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi'; import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Loader, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; function CreatePosisiOrganisasiBumDes() { const router = useRouter(); const stateOrganisasi = useProxy(stateStrukturBumDes.posisiOrganisasi); - + const [isSubmitting, setIsSubmitting] = useState(false); + useEffect(() => { stateOrganisasi.findMany.load(); - // Initialize form with default values + }, []); + + const resetForm = () => { stateOrganisasi.create.form = { nama: "", deskripsi: "", hierarki: 0, }; - - return () => { - // Clean up form on unmount - stateOrganisasi.create.form = { - nama: "", - deskripsi: "", - hierarki: 0, - }; - }; - }, []); + } const handleSubmit = async () => { + setIsSubmitting(true); try { if (!stateOrganisasi.create.form.nama.trim()) { return toast.error('Nama posisi tidak boleh kosong'); } - + await stateOrganisasi.create.submit(); toast.success('Posisi organisasi berhasil ditambahkan'); router.push('/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi'); } catch (error) { toast.error('Gagal menambahkan posisi organisasi'); console.error('Error:', error); + } finally { + setIsSubmitting(false); } }; return ( - - - + Tambah Posisi Organisasi BUMDes @@ -73,11 +68,11 @@ function CreatePosisiOrganisasiBumDes() { (stateOrganisasi.create.form.nama = e.target.value)} required /> - + Deskripsi @@ -89,13 +84,13 @@ function CreatePosisiOrganisasiBumDes() { }} /> - + { const value = parseInt(e.target.value, 10); stateOrganisasi.create.form.hierarki = isNaN(value) ? 0 : value; @@ -103,10 +98,21 @@ function CreatePosisiOrganisasiBumDes() { required /> - + + {/* Tombol Batal */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/page.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/page.tsx index ea181a8f..bf9dc0a2 100644 --- a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/page.tsx @@ -1,6 +1,6 @@ 'use client' import colors from '@/con/colors'; -import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core'; +import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; @@ -68,16 +68,14 @@ function ListPosisiOrganisasiBumDes({ search }: { search: string }) { Daftar Posisi Organisasi BumDes - - - +
@@ -106,19 +104,16 @@ function ListPosisiOrganisasiBumDes({ search }: { search: string }) { {item.hierarki || '-'} - - - + - - )) diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/struktur-organisasi/page.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/struktur-organisasi/page.tsx index be24d78b..89ab5a7e 100644 --- a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/struktur-organisasi/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/struktur-organisasi/page.tsx @@ -2,7 +2,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable react-hooks/exhaustive-deps */ 'use client' -import { Box, Center, Image, Loader, Paper, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Center, Image, Loader, Paper, Stack, Text } from '@mantine/core'; import { IconUsers } from '@tabler/icons-react'; import { OrganizationChart } from 'primereact/organizationchart'; import { useEffect } from 'react'; @@ -110,20 +110,18 @@ function nodeTemplate(node: any) { return ( - - - + {name} {status} diff --git a/src/app/admin/(dashboard)/inovasi/ajukan-ide-inovatif/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/ajukan-ide-inovatif/[id]/page.tsx index f1857aec..51a1ab7f 100644 --- a/src/app/admin/(dashboard)/inovasi/ajukan-ide-inovatif/[id]/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/ajukan-ide-inovatif/[id]/page.tsx @@ -1,6 +1,6 @@ 'use client' import colors from '@/con/colors'; -import { Box, Button, Flex, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -67,21 +67,19 @@ function DetailAjukanIdeInofativDesa() { Detail Ajukan Ide Inovatif Desa - - - + {/* Detail Data */} @@ -104,7 +102,7 @@ function DetailAjukanIdeInofativDesa() { Deskripsi - + diff --git a/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/[id]/edit/page.tsx b/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/[id]/edit/page.tsx index 05c93b59..f22190da 100644 --- a/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/[id]/edit/page.tsx @@ -5,16 +5,17 @@ import desaDigitalState from '@/app/admin/(dashboard)/_state/inovasi/desa-digita import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; @@ -30,12 +31,18 @@ function EditDigitalSmartVillage() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); - + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ name: '', deskripsi: '', imageId: '', }); + const [originalData, setOriginalData] = useState({ + name: '', + deskripsi: '', + imageId: '', + imageUrl: '', + }); useEffect(() => { const loadData = async () => { @@ -50,6 +57,12 @@ function EditDigitalSmartVillage() { deskripsi: data.deskripsi || '', imageId: data.imageId || '', }); + setOriginalData({ + name: data.name || '', + deskripsi: data.deskripsi || '', + imageId: data.imageId || '', + imageUrl: data.image?.link || '', + }); if (data?.image?.link) setPreviewImage(data.image.link); } @@ -64,6 +77,7 @@ function EditDigitalSmartVillage() { const handleSubmit = async () => { try { + setIsSubmitting(true); stateDesaDigital.edit.form = { ...stateDesaDigital.edit.form, ...formData }; if (file) { @@ -80,18 +94,29 @@ function EditDigitalSmartVillage() { } catch (error) { console.error('Error updating desa digital:', error); toast.error('Terjadi kesalahan saat memperbarui data'); + } finally { + setIsSubmitting(false); } }; + const handleResetForm = () => { + setFormData({ + name: originalData.name, + deskripsi: originalData.deskripsi, + imageId: originalData.imageId, + }); + setPreviewImage(originalData.imageUrl || null); + setFile(null); + toast.info('Form dikembalikan ke data awal'); + }; + return ( {/* Header */} - - - + Edit Desa Digital Smart Village @@ -122,7 +147,7 @@ function EditDigitalSmartVillage() { }} onReject={() => toast.error('File tidak valid, gunakan format gambar')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} radius="md" p="xl" > @@ -141,25 +166,45 @@ function EditDigitalSmartVillage() { Seret gambar atau klik untuk memilih file - Maksimal 5MB, format gambar wajib + Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp {previewImage && ( - + + + {/* Tombol hapus (pojok kanan atas) */} + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -188,6 +233,17 @@ function EditDigitalSmartVillage() { {/* Tombol Simpan */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/[id]/page.tsx index c82485e0..236f2b17 100644 --- a/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/[id]/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/[id]/page.tsx @@ -8,8 +8,7 @@ import { Paper, Skeleton, Stack, - Text, - Tooltip, + Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; @@ -112,32 +111,28 @@ function DetailDesaDigital() { {/* Tombol Aksi */} - - - + - - - + diff --git a/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/create/page.tsx b/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/create/page.tsx index 5d905597..74e1bd82 100644 --- a/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/create/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/create/page.tsx @@ -2,31 +2,33 @@ import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; +import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; +import ExifOrientationImg from 'react-exif-orientation-img'; import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; import CreateEditor from '../../../_com/createEditor'; import desaDigitalState from '../../../_state/inovasi/desa-digital'; -import { Dropzone } from '@mantine/dropzone'; -import ExifOrientationImg from 'react-exif-orientation-img'; export default function CreateDesaDigital() { const stateDesaDigital = useProxy(desaDigitalState); const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { stateDesaDigital.create.form = { @@ -44,6 +46,7 @@ export default function CreateDesaDigital() { } try { + setIsSubmitting(true); const uploadRes = await ApiFetch.api.fileStorage.create.post({ file, name: file.name, @@ -64,6 +67,8 @@ export default function CreateDesaDigital() { } catch (error) { console.error('Error in handleSubmit:', error); toast.error('Terjadi kesalahan saat menyimpan data'); + } finally { + setIsSubmitting(false); } }; @@ -71,17 +76,15 @@ export default function CreateDesaDigital() { {/* Header dengan tombol kembali */} - - - + Tambah Desa Digital Smart Village @@ -104,7 +107,7 @@ export default function CreateDesaDigital() { (stateDesaDigital.create.form.name = e.target.value)} radius="md" withAsterisk @@ -138,7 +141,7 @@ export default function CreateDesaDigital() { }} onReject={() => toast.error('File tidak valid, gunakan format gambar')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} radius="md" p="xl" style={{ @@ -166,6 +169,7 @@ export default function CreateDesaDigital() { {/* Preview */} {previewImage && ( + src={previewImage} + alt="Preview" + style={{ + maxHeight: 220, + objectFit: 'cover', + border: '1px solid #e0e0e0', + borderRadius: 12, + }} + /> + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} {/* Tombol Submit */} - + + {/* Tombol Batal */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/page.tsx b/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/page.tsx index 84cb3baf..72d12073 100644 --- a/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/page.tsx @@ -16,8 +16,7 @@ import { TableThead, TableTr, Text, - Title, - Tooltip + Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react'; @@ -68,18 +67,16 @@ function ListDesaDigitalSmartVillage({ search }: { search: string }) { List Desa Digital Smart Village - - - +
diff --git a/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/[id]/edit/page.tsx b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/[id]/edit/page.tsx index e7d83362..6afa83e5 100644 --- a/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/[id]/edit/page.tsx @@ -5,16 +5,17 @@ import infoTeknoState from '@/app/admin/(dashboard)/_state/inovasi/info-tekno'; import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; @@ -30,12 +31,18 @@ function EditInfoTeknologiTepatGuna() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); - + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ name: '', deskripsi: '', imageId: '', }); + const [originalData, setOriginalData] = useState({ + name: '', + deskripsi: '', + imageId: '', + imageUrl: '', + }); // Load data pertama kali useEffect(() => { @@ -51,7 +58,12 @@ function EditInfoTeknologiTepatGuna() { deskripsi: data.deskripsi || '', imageId: data.imageId || '', }); - + setOriginalData({ + name: data.name || '', + deskripsi: data.deskripsi || '', + imageId: data.imageId || '', + imageUrl: data.image?.link || '', + }); if (data?.image?.link) setPreviewImage(data.image.link); } } catch (error) { @@ -63,9 +75,21 @@ function EditInfoTeknologiTepatGuna() { loadData(); }, [params?.id]); + const handleResetForm = () => { + setFormData({ + name: originalData.name, + deskripsi: originalData.deskripsi, + imageId: originalData.imageId, + }); + setFile(null); + setPreviewImage(originalData.imageUrl); + toast.info("Form dikembalikan ke data awal"); + }; + // Submit form const handleSubmit = async () => { try { + setIsSubmitting(true); // sync local → global pas submit stateInfoTekno.edit.form = { ...stateInfoTekno.edit.form, @@ -93,6 +117,8 @@ function EditInfoTeknologiTepatGuna() { } catch (error) { console.error('Error updating info teknologi tepat guna:', error); toast.error('Terjadi kesalahan saat memperbarui info teknologi tepat guna'); + } finally { + setIsSubmitting(false); } }; @@ -100,11 +126,9 @@ function EditInfoTeknologiTepatGuna() { {/* Tombol back + title */} - - - + Edit Info Teknologi Tepat Guna @@ -144,7 +168,7 @@ function EditInfoTeknologiTepatGuna() { }} onReject={() => toast.error('File tidak valid, gunakan format gambar')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} radius="md" p="xl" > @@ -163,25 +187,45 @@ function EditInfoTeknologiTepatGuna() { Seret gambar atau klik untuk memilih file - Maksimal 5MB, format gambar wajib + Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp {previewImage && ( - + + + {/* Tombol hapus (pojok kanan atas) */} + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -201,6 +245,17 @@ function EditInfoTeknologiTepatGuna() { {/* Tombol submit */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/[id]/page.tsx index a75b7207..1ddee92a 100644 --- a/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/[id]/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/[id]/page.tsx @@ -1,6 +1,6 @@ 'use client' import colors from '@/con/colors'; -import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -101,35 +101,31 @@ function DetailInfoTeknologiTepatGuna() { {/* Action Buttons */} - - - + - - - + diff --git a/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/create/page.tsx b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/create/page.tsx index be8b23b7..644e1e8d 100644 --- a/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/create/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/create/page.tsx @@ -2,17 +2,19 @@ import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; +import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; @@ -20,13 +22,13 @@ import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; import CreateEditor from '../../../_com/createEditor'; import infoTeknoState from '../../../_state/inovasi/info-tekno'; -import { Dropzone } from '@mantine/dropzone'; function CreateInfoTeknologiTepatGuna() { const stateInfoTekno = useProxy(infoTeknoState); const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { stateInfoTekno.create.form = { @@ -44,6 +46,7 @@ function CreateInfoTeknologiTepatGuna() { } try { + setIsSubmitting(true); const uploadRes = await ApiFetch.api.fileStorage.create.post({ file: file, name: file.name, @@ -65,6 +68,8 @@ function CreateInfoTeknologiTepatGuna() { } catch (error) { console.error('Error in handleSubmit:', error); toast.error('Terjadi kesalahan saat menyimpan data'); + } finally { + setIsSubmitting(false); } }; @@ -72,11 +77,9 @@ function CreateInfoTeknologiTepatGuna() { {/* Header */} - - - + Tambah Info Teknologi Tepat Guna @@ -94,7 +97,7 @@ function CreateInfoTeknologiTepatGuna() { {/* Nama */} { stateInfoTekno.create.form.name = val.target.value; }} @@ -131,7 +134,7 @@ function CreateInfoTeknologiTepatGuna() { }} onReject={() => toast.error('File tidak valid.')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} radius="md" p="xl" > @@ -152,7 +155,7 @@ function CreateInfoTeknologiTepatGuna() { {previewImage && ( - + + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} {/* Submit Button */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/page.tsx b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/page.tsx index 3fcb8f8a..fe766900 100644 --- a/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/page.tsx @@ -4,6 +4,7 @@ import { Box, Button, Center, + Group, Pagination, Paper, Skeleton, @@ -15,9 +16,7 @@ import { TableThead, TableTr, Text, - Title, - Group, - Tooltip, + Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react'; @@ -68,18 +67,16 @@ function ListInfoTeknologiTepatGuna({ search }: { search: string }) { Daftar Info Teknologi Tepat Guna - - - + diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/_lib/layoutTabs.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/_lib/layoutTabs.tsx index 781f1ec5..c208cdac 100644 --- a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/_lib/layoutTabs.tsx +++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/_lib/layoutTabs.tsx @@ -1,10 +1,10 @@ /* eslint-disable react-hooks/exhaustive-deps */ 'use client' import colors from '@/con/colors'; -import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core'; +import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core'; +import { IconListDetails, IconUsers } from '@tabler/icons-react'; import { usePathname, useRouter } from 'next/navigation'; import React, { useEffect, useState } from 'react'; -import { IconListDetails, IconUsers } from '@tabler/icons-react'; function LayoutTabsKolaborasi({ children }: { children: React.ReactNode }) { const router = useRouter(); @@ -15,14 +15,12 @@ function LayoutTabsKolaborasi({ children }: { children: React.ReactNode }) { label: "List Kolaborasi Inovasi", value: "listkolaborasiinovasi", href: "/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi", - tooltip: "Lihat daftar kolaborasi inovasi", icon: , }, { label: "Mitra Kolaborasi", value: "mitarakolaborasi", href: "/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi", - tooltip: "Kelola mitra kolaborasi", icon: , } ]; @@ -73,25 +71,18 @@ function LayoutTabsKolaborasi({ children }: { children: React.ReactNode }) { }} > {tabs.map((tab, i) => ( - - - {tab.label} - - + {tab.label} + ))} diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/[id]/edit/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/[id]/edit/page.tsx index cce425a1..531aca12 100644 --- a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/[id]/edit/page.tsx @@ -8,13 +8,14 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from "@mantine/core"; +import { YearPickerInput } from "@mantine/dates"; import { IconArrowBack } from "@tabler/icons-react"; import { useParams, useRouter } from "next/navigation"; import { useEffect, useState } from "react"; @@ -25,6 +26,7 @@ function EditKolaborasiInovasi() { const kolaborasiState = useProxy(kolaborasiInovasiState); const router = useRouter(); const params = useParams(); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ name: "", @@ -34,6 +36,14 @@ function EditKolaborasiInovasi() { kolaborator: "", }); + const [originalData, setOriginalData] = useState({ + name: "", + deskripsi: "", + tahun: "", + slug: "", + kolaborator: "", + }); + // Load data awal dari server useEffect(() => { const loadKolaborasi = async () => { @@ -50,6 +60,13 @@ function EditKolaborasiInovasi() { slug: data.slug ?? "", kolaborator: data.kolaborator ?? "", }); + setOriginalData({ + name: data.name ?? "", + deskripsi: data.deskripsi ?? "", + tahun: data.tahun?.toString() ?? "", + slug: data.slug ?? "", + kolaborator: data.kolaborator ?? "", + }) } } catch (error) { console.error("Error loading kolaborasi:", error); @@ -60,9 +77,21 @@ function EditKolaborasiInovasi() { loadKolaborasi(); }, [params?.id]); + const handleResetForm = () => { + setFormData({ + name: originalData.name, + deskripsi: originalData.deskripsi, + tahun: originalData.tahun?.toString(), + slug: originalData.slug, + kolaborator: originalData.kolaborator, + }); + toast.info("Form dikembalikan ke data awal"); + }; + // Handler submit → baru update global state const handleSubmit = async () => { try { + setIsSubmitting(true); kolaborasiState.update.form = { ...kolaborasiState.update.form, name: formData.name, @@ -74,10 +103,12 @@ function EditKolaborasiInovasi() { await kolaborasiState.update.submit(); toast.success("Kolaborasi inovasi berhasil diperbarui!"); - router.push("/admin/inovasi/kolaborasi-inovasi"); + router.push("/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi"); } catch (error) { console.error("Error updating kolaborasi:", error); toast.error("Terjadi kesalahan saat memperbarui data"); + } finally { + setIsSubmitting(false); } }; @@ -89,11 +120,9 @@ function EditKolaborasiInovasi() { return ( - - - + Edit Kolaborasi Inovasi @@ -124,12 +153,15 @@ function EditKolaborasiInovasi() { required /> - handleChange("tahun", e.target.value)} - required + Tahun} + placeholder="Pilih tahun" + onChange={(date) => { + const year = date ? new Date(date).getFullYear() : 0; + handleChange("tahun", year.toString()); + }} /> + {/* Tombol Batal */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/[id]/page.tsx index 0d3b0f9a..577f963f 100644 --- a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/[id]/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/[id]/page.tsx @@ -1,5 +1,5 @@ 'use client' -import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -96,33 +96,29 @@ function DetailKolaborasiInovasi() { {/* Tombol aksi */} - - - + - - - + diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/create/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/create/page.tsx index 8ed78d71..deca2518 100644 --- a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/create/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/create/page.tsx @@ -3,17 +3,18 @@ import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor'; import kolaborasiInovasiState from '@/app/admin/(dashboard)/_state/inovasi/kolaborasi-inovasi'; import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Loader, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { YearPickerInput } from '@mantine/dates'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; function CreateProgramKreatifDesa() { const stateCreate = useProxy(kolaborasiInovasiState); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { stateCreate.create.form = { @@ -40,6 +41,7 @@ function CreateProgramKreatifDesa() { const handleSubmit = async () => { try { + setIsSubmitting(true); await stateCreate.create.create(); resetForm(); router.push("/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi"); @@ -47,6 +49,8 @@ function CreateProgramKreatifDesa() { } catch (error) { console.error("Error creating kolaborasi inovasi:", error); toast.error("Terjadi kesalahan saat menyimpan data"); + } finally { + setIsSubmitting(false); } }; @@ -54,11 +58,9 @@ function CreateProgramKreatifDesa() { {/* Back Button */} - - - + Tambah Kolaborasi Inovasi @@ -77,7 +79,7 @@ function CreateProgramKreatifDesa() { Nama Kolaborasi Inovasi} placeholder="Masukkan nama kolaborasi inovasi" - defaultValue={stateCreate.create.form.name || ''} + value={stateCreate.create.form.name || ''} onChange={(val) => stateCreate.create.form.name = val.target.value} required /> @@ -106,11 +108,22 @@ function CreateProgramKreatifDesa() { Kolaborator} placeholder="Masukkan kolaborator" - defaultValue={stateCreate.create.form.kolaborator || ''} + value={stateCreate.create.form.kolaborator || ''} onChange={(e) => stateCreate.create.form.kolaborator = e.currentTarget.value} /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/page.tsx index f0899d36..576ea0c4 100644 --- a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/page.tsx @@ -17,15 +17,14 @@ import { TableThead, TableTr, Text, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react'; -import HeaderSearch from '../../../_com/header'; import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; -import kolaborasiInovasiState from '../../../_state/inovasi/kolaborasi-inovasi'; import { useProxy } from 'valtio/utils'; +import HeaderSearch from '../../../_com/header'; +import kolaborasiInovasiState from '../../../_state/inovasi/kolaborasi-inovasi'; function KolaborasiInovasi() { const [search, setSearch] = useState(''); @@ -68,16 +67,14 @@ function ListKolaborasiInovasi({ search }: { search: string }) { Daftar Kolaborasi Inovasi - - - +
diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/[id]/page.tsx index a1762db1..ce4516a7 100644 --- a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/[id]/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/[id]/page.tsx @@ -4,17 +4,18 @@ import mitraKolaborasi from '@/app/admin/(dashboard)/_state/inovasi/mitra-kolabo import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Center, Group, Image, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { @@ -36,6 +37,7 @@ function EditMitraKolaborasi() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); // Local form state (controlled) const [formData, setFormData] = useState({ @@ -43,6 +45,12 @@ function EditMitraKolaborasi() { imageId: '', }); + const [originalData, setOriginalData] = useState({ + name: '', + imageId: '', + imageUrl: '', + }); + // Load data ke state lokal sekali saja useEffect(() => { const loadData = async () => { @@ -56,6 +64,11 @@ function EditMitraKolaborasi() { name: data.name || '', imageId: data.imageId || '', }); + setOriginalData({ + name: data.name || '', + imageId: data.imageId || '', + imageUrl: data.image?.link || '', + }); if (data?.image?.link) { setPreviewImage(data.image.link); @@ -77,8 +90,19 @@ function EditMitraKolaborasi() { })); }; + const handleResetForm = () => { + setFormData({ + name: originalData.name, + imageId: originalData.imageId, + }); + setPreviewImage(originalData.imageUrl || null); + setFile(null); + toast.info('Form dikembalikan ke data awal'); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); // upload file jika ada let imageId = formData.imageId; if (file) { @@ -106,6 +130,8 @@ function EditMitraKolaborasi() { } catch (error) { console.error('Error updating mitra:', error); toast.error('Terjadi kesalahan saat memperbarui mitra'); + } finally { + setIsSubmitting(false); } }; @@ -113,11 +139,9 @@ function EditMitraKolaborasi() { {/* Header */} - - - + Edit Mitra @@ -157,7 +181,7 @@ function EditMitraKolaborasi() { }} onReject={() => toast.error('File tidak valid, gunakan format gambar')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} radius="md" p="xl" > @@ -176,7 +200,7 @@ function EditMitraKolaborasi() { Seret gambar atau klik untuk memilih file - Maksimal 5MB, format gambar wajib + Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp @@ -184,7 +208,7 @@ function EditMitraKolaborasi() { {/* Preview Foto */} {previewImage ? ( - + + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + ) : (
@@ -206,6 +248,17 @@ function EditMitraKolaborasi() { {/* Submit */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/create/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/create/page.tsx index d7da8e8b..195c1ab2 100644 --- a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/create/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/create/page.tsx @@ -3,16 +3,17 @@ import mitraKolaborasi from '@/app/admin/(dashboard)/_state/inovasi/mitra-kolabo import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; @@ -26,6 +27,7 @@ function CreateMitraKolaborasi() { const router = useRouter(); const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { state.create.form = { @@ -37,35 +39,41 @@ function CreateMitraKolaborasi() { }; const handleSubmit = async () => { - if (!file) { - return toast.warn('Silakan pilih file gambar terlebih dahulu'); + try { + setIsSubmitting(true); + if (!file) { + return toast.warn('Silakan pilih file gambar terlebih dahulu'); + } + + const res = await ApiFetch.api.fileStorage.create.post({ + file, + name: file.name, + }); + + const uploaded = res.data?.data; + if (!uploaded?.id) { + return toast.error('Gagal mengunggah gambar, silakan coba lagi'); + } + + state.create.form.imageId = uploaded.id; + await state.create.create(); + resetForm(); + router.push('/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi'); + } catch (error) { + console.error("Error creating mitra kolaborasi:", error); + toast.error("Terjadi kesalahan saat menambahkan mitra kolaborasi"); + } finally { + setIsSubmitting(false); } - - const res = await ApiFetch.api.fileStorage.create.post({ - file, - name: file.name, - }); - - const uploaded = res.data?.data; - if (!uploaded?.id) { - return toast.error('Gagal mengunggah gambar, silakan coba lagi'); - } - - state.create.form.imageId = uploaded.id; - await state.create.create(); - resetForm(); - router.push('/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi'); }; return ( {/* Back Button + Title */} - - - + Tambah Mitra Kolaborasi @@ -85,7 +93,7 @@ function CreateMitraKolaborasi() { (state.create.form.name = e.target.value)} required /> @@ -105,7 +113,7 @@ function CreateMitraKolaborasi() { }} onReject={() => toast.error('File tidak valid, gunakan format gambar')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} radius="md" p="xl" > @@ -126,7 +134,7 @@ function CreateMitraKolaborasi() { {previewImage && ( - + + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} {/* Submit Button */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/page.tsx index a2d6cbbb..9fde4fb1 100644 --- a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/page.tsx @@ -18,16 +18,15 @@ import { TableThead, TableTr, Text, - Title, - Tooltip, + Title } from '@mantine/core'; -import { IconEdit, IconSearch, IconX, IconPlus } from '@tabler/icons-react'; +import { IconEdit, IconPlus, IconSearch, IconX } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; import { useProxy } from 'valtio/utils'; import HeaderSearch from '../../../_com/header'; -import mitraKolaborasi from '../../../_state/inovasi/mitra-kolaborasi'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; +import mitraKolaborasi from '../../../_state/inovasi/mitra-kolaborasi'; function MitraKolaborasi() { const [search, setSearch] = useState(''); @@ -81,20 +80,18 @@ function ListMitraKolaborasi({ search }: { search: string }) { Daftar Mitra Kolaborasi - - - +
@@ -135,39 +132,35 @@ function ListMitraKolaborasi({ search }: { search: string }) { - - - + - - - + )) diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/_lib/layoutTabs.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/_lib/layoutTabs.tsx index 55a8cdb2..b12c50be 100644 --- a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/_lib/layoutTabs.tsx +++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/_lib/layoutTabs.tsx @@ -8,17 +8,16 @@ import { TabsList, TabsPanel, TabsTab, - Title, - Tooltip + Title } from '@mantine/core'; -import { usePathname, useRouter } from 'next/navigation'; -import React, { useEffect, useState } from 'react'; import { + IconAlertCircle, IconFileText, IconListDetails, - IconMessage, - IconAlertCircle + IconMessage } from '@tabler/icons-react'; +import { usePathname, useRouter } from 'next/navigation'; +import React, { useEffect, useState } from 'react'; function LayoutTabsLayananOnlineDesa({ children }: { children: React.ReactNode }) { const router = useRouter(); @@ -30,29 +29,25 @@ function LayoutTabsLayananOnlineDesa({ children }: { children: React.ReactNode } label: "Administrasi Online", value: "administrasionline", href: "/admin/inovasi/layanan-online-desa/administrasi-online", - icon: , - tooltip: "Kelola administrasi online desa" + icon: }, { label: "Jenis Layanan", value: "jenislayanan", href: "/admin/inovasi/layanan-online-desa/jenis-layanan", - icon: , - tooltip: "Daftar jenis layanan desa" + icon: }, { label: "Pengaduan Masyarakat", value: "pengaduanmasyarakat", href: "/admin/inovasi/layanan-online-desa/pengaduan-masyarakat", - icon: , - tooltip: "Laporan pengaduan masyarakat" + icon: }, { label: "Jenis Pengaduan", value: "jenispengaduan", href: "/admin/inovasi/layanan-online-desa/jenis-pengaduan", - icon: , - tooltip: "Kategori/jenis pengaduan masyarakat" + icon: } ]; @@ -103,25 +98,18 @@ function LayoutTabsLayananOnlineDesa({ children }: { children: React.ReactNode } }} > {tabs.map((tab, i) => ( - - - {tab.label} - - + {tab.label} + ))} diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/administrasi-online/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/administrasi-online/[id]/page.tsx index b08d0582..72f2ddd8 100644 --- a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/administrasi-online/[id]/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/administrasi-online/[id]/page.tsx @@ -1,6 +1,6 @@ 'use client' -import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -53,20 +53,18 @@ function DetailAdministrasiOnline() { Kembali - - - + {/* Konten Detail */} { + setFormData({ + nama: originalData.nama, + deskripsi: originalData.deskripsi, + }); + toast.info('Form dikembalikan ke data awal'); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); state.edit.form = { ...state.edit.form, ...formData, @@ -65,6 +84,8 @@ function EditJenisLayanan() { } catch (error) { console.error('Error updating jenis layanan:', error); toast.error('Terjadi kesalahan saat memperbarui jenis layanan'); + } finally { + setIsSubmitting(false); } }; @@ -72,16 +93,14 @@ function EditJenisLayanan() { {/* Header */} - - - + Edit Jenis Layanan @@ -126,6 +145,17 @@ function EditJenisLayanan() { {/* Tombol Submit */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/[id]/page.tsx index 41990ab2..babc24f3 100644 --- a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/[id]/page.tsx @@ -2,7 +2,7 @@ import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa'; import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -85,33 +85,29 @@ function DetailJenisLayanan() { {/* Tombol aksi */} - - - + - - - + diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/create/page.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/create/page.tsx index b1b5a107..4dc93703 100644 --- a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/create/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/create/page.tsx @@ -6,21 +6,23 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; function CreateJenisLayanan() { const router = useRouter(); const statePasar = useProxy(layananonlineDesa.jenisLayanan); + const [isSubmitting, setIsSubmitting] = useState(false); useEffect(() => { statePasar.findMany.load(); @@ -34,25 +36,31 @@ function CreateJenisLayanan() { }; const handleSubmit = async () => { - await statePasar.create.create(); - resetForm(); - router.push('/admin/inovasi/layanan-online-desa/jenis-layanan'); + try { + setIsSubmitting(true); + await statePasar.create.create(); + resetForm(); + router.push('/admin/inovasi/layanan-online-desa/jenis-layanan'); + } catch (error) { + console.error('Error creating jenis layanan:', error); + toast.error('Terjadi kesalahan saat menambahkan jenis layanan'); + } finally { + setIsSubmitting(false); + } }; return ( {/* Header dengan tombol back */} - - - + Tambah Jenis Layanan @@ -69,7 +77,7 @@ function CreateJenisLayanan() { > { statePasar.create.form.nama = val.target.value; }} @@ -78,7 +86,7 @@ function CreateJenisLayanan() { required /> { statePasar.create.form.deskripsi = val.target.value; }} @@ -88,6 +96,17 @@ function CreateJenisLayanan() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/page.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/page.tsx index ba176cd6..a1ea1da1 100644 --- a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/page.tsx @@ -16,8 +16,7 @@ import { TableThead, TableTr, Text, - Title, - Tooltip + Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react'; @@ -68,20 +67,18 @@ function ListJenisLayanan({ search }: { search: string }) { Daftar Jenis Layanan - - - +
diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/[id]/page.tsx index c64bb102..5331bc9e 100644 --- a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/[id]/page.tsx @@ -6,11 +6,11 @@ import { Box, Button, Group, + Loader, Paper, Stack, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -23,11 +23,16 @@ function EditJenisPengaduan() { const params = useParams(); const id = params?.id as string; const state = useProxy(layananonlineDesa.jenisPengaduan); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ nama: '', }); + const [originalData, setOriginalData] = useState({ + nama: '', + }); + // Load data sekali aja useEffect(() => { const loadJenisPengaduan = async () => { @@ -41,6 +46,9 @@ function EditJenisPengaduan() { setFormData({ nama: data.nama || '', }); + setOriginalData({ + nama: data.nama || '', + }); } } catch (error) { console.error('Error loading jenis pengaduan:', error); @@ -58,6 +66,13 @@ function EditJenisPengaduan() { })); }; + const handleResetForm = () => { + setFormData({ + nama: originalData.nama, + }); + toast.info('Form dikembalikan ke data awal'); + }; + const handleSubmit = async () => { if (!formData.nama.trim()) { toast.error('Nama jenis pengaduan tidak boleh kosong'); @@ -75,6 +90,7 @@ function EditJenisPengaduan() { } try { + setIsSubmitting(true); const success = await state.edit.update(); if (success) { @@ -83,6 +99,8 @@ function EditJenisPengaduan() { } catch (error) { console.error('Error updating jenis pengaduan:', error); // toast ditangani di dalam state.update + } finally { + setIsSubmitting(false); } }; @@ -90,16 +108,14 @@ function EditJenisPengaduan() { {/* Header */} - - - + Edit Jenis Pengaduan @@ -124,6 +140,17 @@ function EditJenisPengaduan() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/create/page.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/create/page.tsx index bb0ae5b9..9167a525 100644 --- a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/create/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/create/page.tsx @@ -6,21 +6,22 @@ import { Box, Button, Group, + Loader, Paper, Stack, TextInput, - Title, - Tooltip + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; function CreateJenisPengaduan() { const router = useRouter(); const state = useProxy(layananonlineDesa.jenisPengaduan); - + const [isSubmitting, setIsSubmitting] = useState(false); useEffect(() => { state.findMany.load(); }, []); @@ -32,20 +33,26 @@ function CreateJenisPengaduan() { }; const handleSubmit = async () => { - await state.create.create(); - resetForm(); - router.push('/admin/inovasi/layanan-online-desa/jenis-pengaduan'); + try { + setIsSubmitting(true); + await state.create.create(); + resetForm(); + router.push('/admin/inovasi/layanan-online-desa/jenis-pengaduan'); + } catch (error) { + console.error('Error creating jenis pengaduan:', error); + toast.error('Terjadi kesalahan saat menambahkan jenis pengaduan'); + } finally { + setIsSubmitting(false); + } }; return ( {/* Header */} - - - + Tambah Jenis Pengaduan @@ -64,12 +71,23 @@ function CreateJenisPengaduan() { (state.create.form.nama = e.target.value)} required /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/page.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/page.tsx index 4bbee3b6..ecab5a98 100644 --- a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/page.tsx @@ -16,8 +16,7 @@ import { TableThead, TableTr, Text, - Title, - Tooltip + Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; @@ -79,20 +78,18 @@ function ListJenisPengaduan({ search }: { search: string }) { Daftar Jenis Pengaduan - - - + diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/pengaduan-masyarakat/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/pengaduan-masyarakat/[id]/page.tsx index a9a9ad3e..9ab1109e 100644 --- a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/pengaduan-masyarakat/[id]/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/pengaduan-masyarakat/[id]/page.tsx @@ -1,10 +1,10 @@ 'use client' -import { useProxy } from 'valtio/utils'; -import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useState } from 'react'; +import { useProxy } from 'valtio/utils'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa'; @@ -129,21 +129,19 @@ function DetailPengaduanMasyarakat() { {/* Action Button */} - - - + diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/pengaduan-masyarakat/page.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/pengaduan-masyarakat/page.tsx index 11140fff..429bc44f 100644 --- a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/pengaduan-masyarakat/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/pengaduan-masyarakat/page.tsx @@ -16,8 +16,7 @@ import { TableThead, TableTr, Text, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; @@ -99,18 +98,16 @@ function ListPengaduanMasyarakat({ search }: { search: string }) { {item.nomorTelepon} - - - + )) diff --git a/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/[id]/edit/page.tsx index 477e0988..294d25b0 100644 --- a/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/[id]/edit/page.tsx @@ -8,6 +8,7 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, @@ -32,6 +33,7 @@ function EditProgramKreatifDesa() { const stateProgramKreatif = useProxy(programKreatifState); const params = useParams(); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ name: '', @@ -120,8 +122,22 @@ function EditProgramKreatifDesa() { } }; + const handleResetForm = () => { + setFormData({ + name: originalData.name, + slug: originalData.slug, + deskripsi: originalData.deskripsi, + icon: originalData.icon, + }); + setOriginalData({ + ...originalData, + }); + toast.info("Form dikembalikan ke data awal"); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); stateProgramKreatif.update.form = { name: formData.name.trim(), deskripsi: formData.deskripsi.trim(), @@ -138,6 +154,8 @@ function EditProgramKreatifDesa() { } catch (error) { console.error('Error updating program kreatif:', error); toast.error('Gagal menyimpan program kreatif'); + } finally { + setIsSubmitting(false); } }; @@ -203,6 +221,17 @@ function EditProgramKreatifDesa() { + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/[id]/page.tsx index e402bf15..f681a429 100644 --- a/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/[id]/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/[id]/page.tsx @@ -1,6 +1,6 @@ 'use client' import colors from '@/con/colors'; -import { Box, Button, Flex, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -95,40 +95,36 @@ function DetailProgramKreatifDesa() { - - - + - - - + diff --git a/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/create/page.tsx b/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/create/page.tsx index 4d318f33..b1505836 100644 --- a/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/create/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/create/page.tsx @@ -4,23 +4,26 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useProxy } from 'valtio/utils'; import CreateEditor from '../../../_com/createEditor'; -import programKreatifState from '../../../_state/inovasi/program-kreatif'; import SelectIconProgram from '../../../_com/selectIcon'; +import programKreatifState from '../../../_state/inovasi/program-kreatif'; +import { useState } from 'react'; +import { toast } from 'react-toastify'; function CreateProgramKreatifDesa() { const stateCreate = useProxy(programKreatifState); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { stateCreate.create.form = { @@ -32,24 +35,29 @@ function CreateProgramKreatifDesa() { }; const handleSubmit = async () => { - const success = await stateCreate.create.create(); - - if (success) { - resetForm(); - router.push("/admin/inovasi/program-kreatif-desa"); + try { + const success = await stateCreate.create.create(); + + if (success) { + resetForm(); + router.push("/admin/inovasi/program-kreatif-desa"); + } + } catch (error) { + console.error("Error creating program kreatif desa:", error); + toast.error("Gagal menambahkan program kreatif desa"); + } finally { + setIsSubmitting(false); } }; - + return ( {/* Tombol kembali */} - - - + Tambah Program Kreatif Desa @@ -68,7 +76,7 @@ function CreateProgramKreatifDesa() { Nama Program Kreatif Desa} placeholder="Masukkan nama program kreatif desa" - defaultValue={stateCreate.create.form.name || ""} + value={stateCreate.create.form.name || ""} onChange={(e) => (stateCreate.create.form.name = e.currentTarget.value)} required /> @@ -85,7 +93,7 @@ function CreateProgramKreatifDesa() { Deskripsi Singkat Program Kreatif Desa} placeholder="Masukkan deskripsi singkat program kreatif desa" - defaultValue={stateCreate.create.form.slug || ""} + value={stateCreate.create.form.slug || ""} onChange={(e) => (stateCreate.create.form.slug = e.currentTarget.value)} required /> @@ -104,6 +112,17 @@ function CreateProgramKreatifDesa() { {/* Tombol Submit */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/page.tsx b/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/page.tsx index 5ead6694..7b7a647b 100644 --- a/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/page.tsx @@ -18,8 +18,7 @@ import { TableThead, TableTr, Text, - Title, - Tooltip + Title } from '@mantine/core'; import { IconCash, @@ -117,20 +116,18 @@ function ListProgramKreatifDesa({ search }: { search: string }) { Daftar Program Kreatif Desa - - - +
@@ -164,20 +161,18 @@ function ListProgramKreatifDesa({ search }: { search: string }) { > Daftar Program Kreatif Desa - - - +
diff --git a/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/[id]/edit/page.tsx b/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/[id]/edit/page.tsx index af3209fd..898470c5 100644 --- a/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/[id]/edit/page.tsx @@ -1,11 +1,13 @@ "use client"; import { + ActionIcon, Box, Button, Center, Group, Image, + Loader, Paper, Stack, Text, @@ -37,12 +39,20 @@ function EditKeamananLingkungan() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ name: "", deskripsi: "", imageId: "", }); + const [originalData, setOriginalData] = useState({ + name: "", + deskripsi: "", + imageId: "", + imageUrl: "", + }); + // Load data sekali pas mount useEffect(() => { const loadData = async () => { @@ -57,6 +67,12 @@ function EditKeamananLingkungan() { deskripsi: data.deskripsi || "", imageId: data.imageId || "", }); + setOriginalData({ + name: data.name || "", + deskripsi: data.deskripsi || "", + imageId: data.imageId || "", + imageUrl: data.image?.link || "", + }); if (data?.image?.link) { setPreviewImage(data.image.link); @@ -76,8 +92,20 @@ function EditKeamananLingkungan() { setFormData((prev) => ({ ...prev, [field]: value })); }; + const handleResetForm = () => { + setFormData({ + name: originalData.name, + deskripsi: originalData.deskripsi, + imageId: originalData.imageId, + }); + setPreviewImage(originalData.imageUrl || null); + setFile(null); + toast.info("Form dikembalikan ke data awal"); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); let imageId = formData.imageId; if (file) { @@ -108,6 +136,8 @@ function EditKeamananLingkungan() { } catch (error) { console.error("Error updating keamananLingkungan:", error); toast.error("Terjadi kesalahan saat memperbarui keamananLingkungan"); + } finally { + setIsSubmitting(false); } }; @@ -190,7 +220,39 @@ function EditKeamananLingkungan() { {previewImage ? ( - + + + + {/* Tombol hapus (pojok kanan atas) */} + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + + ) : (
@@ -216,17 +278,29 @@ function EditKeamananLingkungan() { + {/* Tombol Batal */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/create/page.tsx b/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/create/page.tsx index e5d799d7..bdf82527 100644 --- a/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/create/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/create/page.tsx @@ -2,10 +2,12 @@ import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, @@ -31,6 +33,7 @@ function CreateKeamananLingkungan() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false) const resetForm = () => { keamananState.create.form = { @@ -43,27 +46,35 @@ function CreateKeamananLingkungan() { }; const handleSubmit = async () => { - if (!file) { - return toast.warn('Pilih file gambar terlebih dahulu'); + try { + setIsSubmitting(true) + if (!file) { + return toast.warn('Pilih file gambar terlebih dahulu'); + } + + const res = await ApiFetch.api.fileStorage.create.post({ + file, + name: file.name, + }); + + const uploaded = res.data?.data; + + if (!uploaded?.id) { + return toast.error('Gagal mengupload file'); + } + + keamananState.create.form.imageId = uploaded.id; + + await keamananState.create.create(); + + resetForm(); + router.push('/admin/keamanan/keamanan-lingkungan-pecalang-patwal'); + } catch (error) { + console.error(error); + toast.error('Gagal menambahkan data keamanan lingkungan'); + } finally { + setIsSubmitting(false) } - - const res = await ApiFetch.api.fileStorage.create.post({ - file, - name: file.name, - }); - - const uploaded = res.data?.data; - - if (!uploaded?.id) { - return toast.error('Gagal mengupload file'); - } - - keamananState.create.form.imageId = uploaded.id; - - await keamananState.create.create(); - - resetForm(); - router.push('/admin/keamanan/keamanan-lingkungan-pecalang-patwal'); }; return ( @@ -108,7 +119,7 @@ function CreateKeamananLingkungan() { }} onReject={() => toast.error('File tidak valid.')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} > {previewImage && ( - + + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} {/* Input Nama */} { keamananState.create.form.name = val.target.value; }} @@ -193,6 +222,17 @@ function CreateKeamananLingkungan() { {/* Tombol Submit */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/[id]/edit/page.tsx b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/[id]/edit/page.tsx index 79f0da80..b348d8ab 100644 --- a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/[id]/edit/page.tsx @@ -8,6 +8,7 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, @@ -24,6 +25,7 @@ function EditKontakItem() { const router = useRouter(); const kontakState = useProxy(kontakDarurat.kontakDaruratItem); const params = useParams(); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ name: '', @@ -31,6 +33,12 @@ function EditKontakItem() { icon: '', }); + const [originalData, setOriginalData] = useState({ + name: '', + nomorTelepon: '', + icon: '', + }); + // Load data sekali dari global state useEffect(() => { const loadKontakDarurat = async () => { @@ -45,6 +53,11 @@ function EditKontakItem() { nomorTelepon: data.nomorTelepon || '', icon: data.icon || '', }); + setOriginalData({ + name: data.nama || '', + nomorTelepon: data.nomorTelepon || '', + icon: data.icon || '', + }); } } catch (error) { console.error('Error loading kontak darurat:', error); @@ -62,8 +75,18 @@ function EditKontakItem() { })); }; + const handleResetForm = () => { + setFormData({ + name: originalData.name, + nomorTelepon: originalData.nomorTelepon, + icon: originalData.icon, + }); + toast.info('Form dikembalikan ke data awal'); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); // Update global state sekali pas submit kontakState.update.form = { ...kontakState.update.form, @@ -78,6 +101,8 @@ function EditKontakItem() { } catch (error) { console.error('Error updating kontak darurat:', error); toast.error('Terjadi kesalahan saat memperbarui kontak darurat'); + } finally { + setIsSubmitting(false); } }; @@ -130,6 +155,17 @@ function EditKontakItem() { + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/create/page.tsx b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/create/page.tsx index b4cc93f5..aea32c66 100644 --- a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/create/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/create/page.tsx @@ -6,6 +6,7 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, @@ -14,11 +15,14 @@ import { } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; function CreateKontakItem() { const kontakState = useProxy(kontakDarurat.kontakDaruratItem); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { kontakState.create.form = { nama: '', @@ -28,9 +32,17 @@ function CreateKontakItem() { }; const handleSubmit = async () => { - await kontakState.create.create(); - resetForm(); - router.push('/admin/keamanan/kontak-darurat/kontak-darurat-item'); + try { + setIsSubmitting(true); + await kontakState.create.create(); + resetForm(); + router.push('/admin/keamanan/kontak-darurat/kontak-darurat-item'); + } catch (error) { + console.error("Error creating kontak darurat item:", error); + toast.error("Gagal menambahkan kontak darurat item"); + } finally { + setIsSubmitting(false); + } }; return ( @@ -62,7 +74,7 @@ function CreateKontakItem() { {/* Input Nama Kategori */} { kontakState.create.form.nama = val.target.value; }} @@ -74,7 +86,7 @@ function CreateKontakItem() { Nomor Telepon Kontak} placeholder="Masukkan nomor telepon" - defaultValue={kontakState.create.form.nomorTelepon} + value={kontakState.create.form.nomorTelepon} onChange={(val) => { kontakState.create.form.nomorTelepon = val.target.value; }} @@ -88,6 +100,17 @@ function CreateKontakItem() { {/* Tombol Submit */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/[id]/edit/page.tsx index a522c3c7..0e0180e5 100644 --- a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/[id]/edit/page.tsx @@ -1,72 +1,125 @@ /* eslint-disable react-hooks/exhaustive-deps */ -"use client"; +'use client'; -import { IconKey } from "@/app/admin/(dashboard)/_com/iconMap"; -import SelectIconProgramEdit from "@/app/admin/(dashboard)/_com/selectIconEdit"; -import kontakDarurat from "@/app/admin/(dashboard)/_state/keamanan/kontak-darurat-keamanan"; -import colors from "@/con/colors"; +import { IconKey } from '@/app/admin/(dashboard)/_com/iconMap'; +import SelectIconProgramEdit from '@/app/admin/(dashboard)/_com/selectIconEdit'; +import kontakDarurat from '@/app/admin/(dashboard)/_state/keamanan/kontak-darurat-keamanan'; +import colors from '@/con/colors'; import { Box, Button, Group, + Loader, MultiSelect, Paper, Stack, Text, TextInput, - Title -} from "@mantine/core"; -import { IconArrowBack } 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"; + Title, +} from '@mantine/core'; +import { IconArrowBack } 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'; -function EditKontakDaruratKeamanan() { +type FormData = { + name: string; + icon: IconKey | ''; + kategoriId: string[]; +}; + +export default function EditKontakDaruratKeamanan() { const router = useRouter(); - const params = useParams(); + const params = useParams<{ id: string }>(); const kontakState = useProxy(kontakDarurat.kontakDaruratKeamananState); - const [isLoading, setIsLoading] = useState(true); - // Remove the dependency on data in the initial state - const [formData, setFormData] = useState({ - name: "", - icon: "" as IconKey | "", - kategoriId: [] as string[], // Initialize as empty array + const [isSubmitting, setIsSubmitting] = useState(false); + const [formData, setFormData] = useState({ + name: '', + icon: '', + kategoriId: [], + }); + const [originalData, setOriginalData] = useState({ + name: '', + icon: '', + kategoriId: [], }); - // Load data dari backend + // 🔁 Load data saat ID berubah useEffect(() => { - const loadData = async () => { + const loadInitialData = async () => { + const id = params?.id; + if (!id) { + toast.error('ID tidak valid'); + router.push('/admin/keamanan/kontak-darurat'); + return; + } + try { - setIsLoading(true); + // Load dropdown kategori await kontakDarurat.kontakDaruratItem.findMany.load(); - const id = params?.id as string; - if (id) { - const data = await kontakState.update.load(id); - if (data) { - setFormData({ - name: data.nama || "", - icon: (data.icon as IconKey) || "", - kategoriId: Array.isArray(data.kategoriId) ? data.kategoriId : [], - }); - } + // Load data kontak darurat + const data = await kontakState.update.load(id); + if (!data) { + toast.error('Data tidak ditemukan'); + router.push('/admin/keamanan/kontak-darurat'); + return; } - } catch (error) { - console.error("Error loading data:", error); - toast.error("Gagal memuat data"); - } finally { - setIsLoading(false); + + const initial: FormData = { + name: data.nama || '', + icon: (data.icon as IconKey) || '', + kategoriId: Array.isArray(data.kategoriId) ? data.kategoriId : [], + }; + + setFormData(initial); + setOriginalData(initial); + } catch (err) { + console.error('Gagal memuat data:', err); + toast.error('Gagal memuat data kontak darurat'); + router.push('/admin/keamanan/kontak-darurat'); } }; - loadData(); + loadInitialData(); }, [params?.id]); - // Handle submit + // Debug: Log the kontakDaruratItem data + useEffect(() => { + if (kontakDarurat.kontakDaruratItem.findMany.data) { + console.log('Kontak Item Data:', kontakDarurat.kontakDaruratItem.findMany.data); + } + }, [kontakDarurat.kontakDaruratItem.findMany.data]); + + // 📝 Update field + const updateField = (field: K, value: FormData[K]) => { + setFormData((prev) => ({ ...prev, [field]: value })); + }; + + // ↩️ Reset ke data awal + const handleResetForm = () => { + setFormData(originalData); + toast.info('Form dikembalikan ke data awal'); + }; + + // 💾 Simpan const handleSubmit = async () => { + if (!formData.name.trim()) { + toast.error('Nama kontak darurat wajib diisi'); + return; + } + + if (formData.kategoriId.length === 0) { + toast.error('Pilih minimal satu kontak item'); + return; + } + try { + setIsSubmitting(true); + + // Sinkronisasi ke Valtio state (jika digunakan di `update`) kontakState.update.form = { ...kontakState.update.form, nama: formData.name, @@ -74,112 +127,103 @@ function EditKontakDaruratKeamanan() { kategoriId: formData.kategoriId, }; - await kontakState.update.update(); - toast.success("Kontak Darurat berhasil diperbarui!"); - router.push("/admin/keamanan/kontak-darurat"); + const success = await kontakState.update.update(); + if (success) { + toast.success('Kontak Darurat berhasil diperbarui!'); + router.push('/admin/keamanan/kontak-darurat'); + } } catch (error) { - console.error("Error updating kontak darurat:", error); - toast.error("Terjadi kesalahan saat memperbarui kontak darurat"); + console.error('Error saat menyimpan:', error); + toast.error('Terjadi kesalahan saat menyimpan data'); + } finally { + setIsSubmitting(false); } }; + // 📋 Daftar opsi kategori untuk MultiSelect + const kategoriOptions = (kontakDarurat.kontakDaruratItem.findMany.data || []).map((item: { id: string; nama: string }) => ({ + value: item.id, + label: item.nama, + })); + return ( - - {/* Header */} + - Edit Kontak Darurat Keamanan - {/* Form */} - {/* Nama Kontak */} - setFormData((prev) => ({ ...prev, name: e.target.value })) - } - label="Nama Kontak Darurat" + label={Nama Kontak Darurat} placeholder="Masukkan nama kontak darurat" + value={formData.name} + onChange={(e) => updateField('name', e.target.value)} required /> - {/* MultiSelect */} Kontak Item} + placeholder="Pilih kontak item" + data={kategoriOptions} value={formData.kategoriId} - onChange={(val) => - setFormData((prev) => ({ ...prev, kategoriId: val })) - } - label={Kontak Item} - placeholder={isLoading ? "Memuat data..." : "Pilih kontak item"} - data={ - Array.isArray(kontakDarurat.kontakDaruratItem.findMany.data) - ? kontakDarurat.kontakDaruratItem.findMany.data.map((v) => ({ - value: v.id, - label: v.nama, - })) - : [] - } + onChange={(values) => updateField('kategoriId', values)} clearable searchable required - error={ - !formData.kategoriId.length - ? "Pilih minimal satu kategori" - : undefined - } - disabled={isLoading} + error={!formData.kategoriId.length ? 'Pilih minimal satu kategori' : undefined} /> - {/* Icon Select */} - + Ikon Program Kreatif Desa - setFormData((prev) => ({ ...prev, icon: value })) - } + value={formData.icon} + onChange={(icon) => updateField('icon', icon)} /> - {/* Submit */} + {/* Tombol Batal */} + + + {/* Tombol Simpan */} ); -} - -export default EditKontakDaruratKeamanan; +} \ No newline at end of file diff --git a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/create/page.tsx b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/create/page.tsx index 3c4bc1ad..330fa818 100644 --- a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/create/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/create/page.tsx @@ -6,6 +6,7 @@ import { Box, Button, Group, + Loader, MultiSelect, Paper, Stack, @@ -16,11 +17,14 @@ import { import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; function CreateKontakDaruratKeamanan() { const kontakState = useProxy(kontakDarurat.kontakDaruratKeamananState); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); useShallowEffect(() => { kontakDarurat.kontakDaruratItem.findMany.load(); @@ -35,9 +39,17 @@ function CreateKontakDaruratKeamanan() { }; const handleSubmit = async () => { - await kontakState.create.create(); - resetForm(); - router.push('/admin/keamanan/kontak-darurat/kontak-darurat-keamanan'); + try { + setIsSubmitting(true); + await kontakState.create.create(); + resetForm(); + router.push('/admin/keamanan/kontak-darurat/kontak-darurat-keamanan'); + } catch (error) { + console.error('Error creating kontak darurat:', error); + toast.error('Terjadi kesalahan saat menambah kontak darurat'); + } finally { + setIsSubmitting(false); + } }; return ( @@ -69,7 +81,7 @@ function CreateKontakDaruratKeamanan() { {/* Input Nama Kategori */} { kontakState.create.form.nama = val.target.value; }} @@ -100,6 +112,17 @@ function CreateKontakDaruratKeamanan() { {/* Tombol Submit */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/keamanan/laporan-publik/[id]/edit/page.tsx b/src/app/admin/(dashboard)/keamanan/laporan-publik/[id]/edit/page.tsx index c2772a32..7aecff43 100644 --- a/src/app/admin/(dashboard)/keamanan/laporan-publik/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/laporan-publik/[id]/edit/page.tsx @@ -7,6 +7,7 @@ import { Box, Button, Group, + Loader, Paper, Select, Stack, @@ -27,6 +28,7 @@ function EditLaporanPublik() { const stateLaporan = useProxy(laporanPublikState); const router = useRouter(); const params = useParams(); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState<{ judul: string; @@ -44,6 +46,15 @@ function EditLaporanPublik() { kronologi: '', }); + const [originalData, setOriginalData] = useState({ + judul: '', + lokasi: '', + tanggalWaktu: '', + status: 'Proses', // Default status + penanganan: '', + kronologi: '', + }); + useEffect(() => { const loadLaporanPublik = async () => { const id = params?.id as string; @@ -60,6 +71,14 @@ function EditLaporanPublik() { penanganan: data.penanganan?.[0]?.deskripsi ?? '', kronologi: data.kronologi ?? '', }); + setOriginalData({ + judul: data.judul ?? '', + lokasi: data.lokasi ?? '', + tanggalWaktu: data.tanggalWaktu ?? '', + status: (data.status as Status) ?? 'Proses', + penanganan: data.penanganan?.[0]?.deskripsi ?? '', + kronologi: data.kronologi ?? '', + }); } } catch (error) { console.error("Error loading laporan publik:", error); @@ -76,8 +95,21 @@ function EditLaporanPublik() { setFormData((prev) => ({ ...prev, [field]: value })); }; + const handleResetForm = () => { + setFormData({ + judul: originalData.judul, + lokasi: originalData.lokasi, + tanggalWaktu: originalData.tanggalWaktu, + status: (originalData.status as Status), + penanganan: originalData.penanganan, + kronologi: originalData.kronologi, + }); + toast.info("Form dikembalikan ke data awal"); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); stateLaporan.edit.form = { ...stateLaporan.edit.form, ...formData, @@ -89,6 +121,8 @@ function EditLaporanPublik() { } catch (error) { console.error("Error updating laporan publik:", error); toast.error("Terjadi kesalahan saat memperbarui laporan publik"); + } finally { + setIsSubmitting(false); } }; @@ -174,6 +208,17 @@ function EditLaporanPublik() { + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/keamanan/laporan-publik/create/page.tsx b/src/app/admin/(dashboard)/keamanan/laporan-publik/create/page.tsx index bbe1037a..ffb072f6 100644 --- a/src/app/admin/(dashboard)/keamanan/laporan-publik/create/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/laporan-publik/create/page.tsx @@ -4,6 +4,7 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, @@ -15,12 +16,15 @@ import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useProxy } from 'valtio/utils'; import laporanPublikState from '../../../_state/keamanan/laporan-publik'; +import { useState } from 'react'; +import { toast } from 'react-toastify'; export type Status = 'Selesai' | 'Proses' | 'Gagal'; function CreateLaporanPublik() { const stateLaporan = useProxy(laporanPublikState); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { stateLaporan.create.form = { @@ -32,9 +36,17 @@ function CreateLaporanPublik() { }; const handleSubmit = async () => { - await stateLaporan.create.create(); - resetForm(); - router.push('/admin/keamanan/laporan-publik'); + try { + setIsSubmitting(true); + await stateLaporan.create.create(); + resetForm(); + router.push('/admin/keamanan/laporan-publik'); + } catch (error) { + console.error('Error creating laporan publik:', error); + toast.error('Terjadi kesalahan saat membuat laporan publik'); + } finally { + setIsSubmitting(false); + } }; return ( @@ -60,7 +72,7 @@ function CreateLaporanPublik() { > (stateLaporan.create.form.judul = e.target.value)} label={Judul Laporan Publik} placeholder="Masukkan judul laporan publik" @@ -68,7 +80,7 @@ function CreateLaporanPublik() { /> (stateLaporan.create.form.lokasi = e.target.value)} label={Lokasi Laporan Publik} placeholder="Masukkan lokasi laporan publik" @@ -88,7 +100,7 @@ function CreateLaporanPublik() { /> (stateLaporan.create.form.kronologi = e.target.value)} label={Kronologi Laporan Publik} placeholder="Masukkan kronologi laporan publik" @@ -96,6 +108,17 @@ function CreateLaporanPublik() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/[id]/edit/page.tsx b/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/[id]/edit/page.tsx index 44e9f527..cd50b6d9 100644 --- a/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/[id]/edit/page.tsx @@ -8,6 +8,7 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, @@ -24,6 +25,7 @@ function EditPencegahanKriminalitas() { const router = useRouter(); const params = useParams(); const kriminalitasState = useProxy(pencegahanKriminalitasState); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ judul: '', @@ -32,6 +34,13 @@ function EditPencegahanKriminalitas() { linkVideo: '', }); + const [originalData, setOriginalData] = useState({ + judul: '', + deskripsi: '', + deskripsiSingkat: '', + linkVideo: '', + }); + // load data hanya sekali pas id berubah useEffect(() => { const loadKriminalitas = async () => { @@ -47,6 +56,12 @@ function EditPencegahanKriminalitas() { deskripsiSingkat: data.deskripsiSingkat ?? '', linkVideo: data.linkVideo ?? '', }); + setOriginalData({ + judul: data.judul ?? '', + deskripsi: data.deskripsi ?? '', + deskripsiSingkat: data.deskripsiSingkat ?? '', + linkVideo: data.linkVideo ?? '', + }); } } catch (error) { console.error('Error loading pencegahan kriminalitas:', error); @@ -65,6 +80,16 @@ function EditPencegahanKriminalitas() { setFormData((prev) => ({ ...prev, [field]: e.target.value })); }; + const handleResetForm = () => { + setFormData({ + judul: originalData.judul, + deskripsi: originalData.deskripsi, + deskripsiSingkat: originalData.deskripsiSingkat, + linkVideo: originalData.linkVideo, + }); + toast.info("Form dikembalikan ke data awal"); + }; + const handleSubmit = async () => { const converted = convertYoutubeUrlToEmbed(formData.linkVideo); if (!converted) { @@ -73,6 +98,7 @@ function EditPencegahanKriminalitas() { } try { + setIsSubmitting(true); // update global state saat submit kriminalitasState.update.form = { judul: formData.judul, @@ -88,6 +114,8 @@ function EditPencegahanKriminalitas() { } catch (error) { console.error('Error updating pencegahan kriminalitas:', error); toast.error('Terjadi kesalahan saat memperbarui data'); + } finally { + setIsSubmitting(false); } }; @@ -177,6 +205,17 @@ function EditPencegahanKriminalitas() { {/* Action button */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/create/page.tsx b/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/create/page.tsx index 39711915..50d14fa0 100644 --- a/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/create/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/create/page.tsx @@ -5,6 +5,7 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, @@ -25,6 +26,7 @@ function CreatePencegahanKriminalitas() { const kriminalitasState = useProxy(pencegahanKriminalitasState); const [link, setLink] = useState(''); const embedLink = convertYoutubeUrlToEmbed(link); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { kriminalitasState.create.form = { @@ -37,15 +39,23 @@ function CreatePencegahanKriminalitas() { }; const handleSubmit = async () => { - if (!embedLink) { - toast.error('Link YouTube tidak valid. Pastikan formatnya benar.'); - return; + try { + setIsSubmitting(true); + if (!embedLink) { + toast.error('Link YouTube tidak valid. Pastikan formatnya benar.'); + return; + } + + kriminalitasState.create.form.linkVideo = embedLink; + await kriminalitasState.create.create(); + resetForm(); + router.push('/admin/keamanan/pencegahan-kriminalitas'); + } catch (error) { + console.error('Gagal menambahkan pencegahan kriminalitas:', error); + toast.error('Gagal menambahkan pencegahan kriminalitas'); + } finally { + setIsSubmitting(false); } - - kriminalitasState.create.form.linkVideo = embedLink; - await kriminalitasState.create.create(); - resetForm(); - router.push('/admin/keamanan/pencegahan-kriminalitas'); }; return ( @@ -79,7 +89,7 @@ function CreatePencegahanKriminalitas() { { kriminalitasState.create.form.judul = e.currentTarget.value; }} @@ -116,7 +126,7 @@ function CreatePencegahanKriminalitas() { setLink(e.currentTarget.value)} required /> @@ -140,6 +150,17 @@ function CreatePencegahanKriminalitas() { {/* Button Submit */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/keamanan/polsek-terdekat/[id]/edit/page.tsx b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/[id]/edit/page.tsx index a07fbb11..736c9e4e 100644 --- a/src/app/admin/(dashboard)/keamanan/polsek-terdekat/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/[id]/edit/page.tsx @@ -9,6 +9,7 @@ import { Button, Card, Group, + Loader, Modal, Paper, Select, @@ -38,6 +39,7 @@ function EditPolsekTerdekat() { null ); const [namaLayananUpdate, setNamaLayananUpdate] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ nama: "", jarakKeDesa: "", @@ -51,6 +53,19 @@ function EditPolsekTerdekat() { layananPolsekId: "", }); + const [originalData, setOriginalData] = useState({ + nama: "", + jarakKeDesa: "", + alamat: "", + nomorTelepon: "", + jamOperasional: "", + embedMapUrl: "", + namaTempatMaps: "", + alamatMaps: "", + linkPetunjukArah: "", + layananPolsekId: "", + }); + // load data untuk form edit useEffect(() => { const loadPolsekTerdekat = async () => { @@ -72,6 +87,19 @@ function EditPolsekTerdekat() { linkPetunjukArah: data.linkPetunjukArah || "", layananPolsekId: data.layananPolsekId || "", }); + + setOriginalData({ + nama: data.nama || "", + jarakKeDesa: data.jarakKeDesa || "", + alamat: data.alamat || "", + nomorTelepon: data.nomorTelepon || "", + jamOperasional: data.jamOperasional || "", + embedMapUrl: data.embedMapUrl || "", + namaTempatMaps: data.namaTempatMaps || "", + alamatMaps: data.alamatMaps || "", + linkPetunjukArah: data.linkPetunjukArah || "", + layananPolsekId: data.layananPolsekId || "", + }); } } catch (error) { console.error("Error loading polsek terdekat:", error); @@ -187,8 +215,25 @@ function EditPolsekTerdekat() { setFormData(prev => ({ ...prev, [field]: value })); }; + const handleResetForm = () => { + setFormData({ + nama: originalData.nama, + jarakKeDesa: originalData.jarakKeDesa, + alamat: originalData.alamat, + nomorTelepon: originalData.nomorTelepon, + jamOperasional: originalData.jamOperasional, + embedMapUrl: originalData.embedMapUrl, + namaTempatMaps: originalData.namaTempatMaps, + alamatMaps: originalData.alamatMaps, + linkPetunjukArah: originalData.linkPetunjukArah, + layananPolsekId: originalData.layananPolsekId, + }); + toast.info("Form dikembalikan ke data awal"); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); polsekState.edit.form = { ...formData }; // update global state hanya di sini await polsekState.edit.update(); toast.success("Polsek terdekat berhasil diperbarui!"); @@ -196,6 +241,8 @@ function EditPolsekTerdekat() { } catch (error) { console.error("Error updating polsek terdekat:", error); toast.error("Gagal memperbarui data polsek terdekat"); + } finally { + setIsSubmitting(false); } }; @@ -212,7 +259,7 @@ function EditPolsekTerdekat() { setNamaLayananBaru(e.currentTarget.value)} /> @@ -230,7 +277,7 @@ function EditPolsekTerdekat() { setNamaLayananUpdate(e.currentTarget.value)} /> + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/keamanan/polsek-terdekat/create/page.tsx b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/create/page.tsx index 7ffcab93..49dc48b6 100644 --- a/src/app/admin/(dashboard)/keamanan/polsek-terdekat/create/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/create/page.tsx @@ -5,6 +5,7 @@ import { Box, Button, Group, + Loader, Modal, Paper, Select, @@ -26,6 +27,7 @@ function CreatePolsekTerdekat() { const [layananOptions, setLayananOptions] = useState<{ value: string; label: string }[]>([]); const [modalOpen, setModalOpen] = useState(false); const [namaLayananBaru, setNamaLayananBaru] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { polsekState.create.form = { @@ -43,9 +45,17 @@ function CreatePolsekTerdekat() { }; const handleSubmit = async () => { - await polsekState.create.create(); - resetForm(); - router.push("/admin/keamanan/polsek-terdekat"); + try { + setIsSubmitting(true); + await polsekState.create.create(); + resetForm(); + router.push("/admin/keamanan/polsek-terdekat"); + } catch (error) { + console.error(error) + toast.error("Gagal menambah polsek terdekat"); + } finally { + setIsSubmitting(false); + } }; const fetchLayanan = async () => { @@ -112,7 +122,7 @@ function CreatePolsekTerdekat() { setNamaLayananBaru(e.currentTarget.value)} /> @@ -145,71 +155,84 @@ function CreatePolsekTerdekat() { > (polsekState.create.form.nama = val.target.value)} label={Nama Polsek Terdekat} placeholder="Masukkan nama Polsek Terdekat" required /> (polsekState.create.form.jarakKeDesa = val.target.value)} label={Jarak Polsek Terdekat} placeholder="Masukkan jarak Polsek Terdekat" required /> (polsekState.create.form.alamat = val.target.value)} label={Alamat Polsek Terdekat} placeholder="Masukkan alamat Polsek Terdekat" required /> (polsekState.create.form.nomorTelepon = val.target.value)} label={Nomor Telepon Polsek Terdekat} placeholder="Masukkan nomor telepon Polsek Terdekat" required /> (polsekState.create.form.jamOperasional = val.target.value)} label={Jam Operasional Polsek Terdekat} placeholder="Masukkan jam operasional Polsek Terdekat" /> (polsekState.create.form.embedMapUrl = val.target.value)} label={Embed Map URL} placeholder="Masukkan embed map url" /> (polsekState.create.form.namaTempatMaps = val.target.value)} label={Nama Tempat Maps} placeholder="Masukkan nama tempat maps" /> (polsekState.create.form.alamatMaps = val.target.value)} label={Alamat Maps} placeholder="Masukkan alamat maps" /> (polsekState.create.form.linkPetunjukArah = val.target.value)} label={Link Petunjuk Arah} placeholder="Masukkan link petunjuk arah" /> - - {/* Dropdown Select */}
- Nama APBDes - Jumlah + Nama APBDes + Jumlah Dokumen - Aksi + Aksi {filteredData.length > 0 ? ( filteredData.map((item) => ( - + {item.name} - + Rp. {item.jumlah} - - {item.file?.link ? ( - - ) : ( - Tidak ada dokumen - )} + + + {item.file?.link ? ( + + ) : ( + Tidak ada dokumen + )} + - - + )) @@ -134,7 +138,7 @@ function ListAPBDes({ search }: { search: string }) {
- +
{ if (!id) return; const loadKategori = async () => { - setIsLoading(true); try { const data = await stateKategori.edit.load(id); if (data) { stateKategori.edit.id = id; - setFormData({ name: data.name || '' }); + const newForm = { name: data.name || '' }; + setFormData(newForm); + setOriginalData(newForm); } } catch (err) { console.error(err); toast.error('Gagal memuat data kategori desa anti korupsi'); - } finally { - setIsLoading(false); } }; loadKategori(); }, [id]); - // handler controlled input - const handleChange = useCallback( - (field: keyof typeof formData, value: string) => { - setFormData(prev => ({ ...prev, [field]: value })); - }, - [] - ); + // ✍️ ubah value input form + const handleChange = (field: keyof typeof formData, value: string) => { + setFormData((prev) => ({ ...prev, [field]: value })); + }; - // submit form + // 🔁 reset ke data awal + const handleResetForm = () => { + setFormData({ ...originalData }); + toast.info('Form dikembalikan ke data awal'); + }; + + // 💾 submit update const handleSubmit = useCallback(async () => { if (!formData.name.trim()) return toast.error('Nama kategori tidak boleh kosong'); + setIsSubmitting(true); try { - setIsLoading(true); - - // update global state hanya saat submit + // isi form global dari local state stateKategori.edit.form = { name: formData.name.trim() }; - if (!stateKategori.edit.id) stateKategori.edit.id = id; + stateKategori.edit.id = id; await stateKategori.edit.update(); + toast.success('Kategori berhasil diperbarui'); router.push('/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi'); } catch (err) { console.error(err); toast.error(err instanceof Error ? err.message : 'Gagal memperbarui kategori'); } finally { - setIsLoading(false); + setIsSubmitting(false); } - }, [formData.name, id, router, stateKategori.edit]); + }, [formData.name, id, router]); + // 🧩 UI return ( - + Edit Kategori Desa Anti Korupsi @@ -96,25 +104,36 @@ export default function EditKategoriDesaAntiKorupsi() { handleChange('name', e.currentTarget.value)} required - disabled={isLoading} /> - + + {/* Tombol Batal */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/create/page.tsx b/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/create/page.tsx index bb816f12..117f5f19 100644 --- a/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/create/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/create/page.tsx @@ -1,16 +1,18 @@ /* eslint-disable react-hooks/exhaustive-deps */ 'use client' import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Stack, TextInput, Title } from '@mantine/core'; +import { Box, Button, Group, Paper, Stack, TextInput, Title, Loader } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { useProxy } from 'valtio/utils'; import korupsiState from '../../../../_state/landing-page/desa-anti-korupsi'; +import { toast } from 'react-toastify'; export default function CreateKategoriDesaAntiKorupsi() { const router = useRouter(); const stateKategori = useProxy(korupsiState.kategoriDesaAntiKorupsi); + const [isSubmitting, setIsSubmitting] = useState(false); useEffect(() => { stateKategori.findMany.load(); @@ -23,21 +25,29 @@ export default function CreateKategoriDesaAntiKorupsi() { }; const handleSubmit = async () => { - if (!stateKategori.create.form.name) { - return alert('Nama kategori harus diisi'); + setIsSubmitting(true); + try { + if (!stateKategori.create.form.name) { + return alert('Nama kategori harus diisi'); + } + + await stateKategori.create.create(); + resetForm(); + router.push("/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi"); + } catch (error) { + console.error("Error creating kategori desa anti korupsi:", error); + toast.error("Gagal menambahkan kategori desa anti korupsi"); + } finally { + setIsSubmitting(false); } - - await stateKategori.create.create(); - resetForm(); - router.push("/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi"); }; return ( - + Tambah Kategori Desa Anti Korupsi @@ -55,12 +65,24 @@ export default function CreateKategoriDesaAntiKorupsi() { (stateKategori.create.form.name = e.target.value)} required /> - + + {/* Tombol Batal */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/[id]/edit/page.tsx b/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/[id]/edit/page.tsx index dfb4b0e6..eaf0c853 100644 --- a/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/[id]/edit/page.tsx @@ -1,7 +1,7 @@ /* eslint-disable react-hooks/exhaustive-deps */ 'use client'; -import { Box, Button, Group, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core'; +import { Loader, ActionIcon, Box, Button, Group, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconFile, IconUpload, IconX } from '@tabler/icons-react'; @@ -34,9 +34,18 @@ export default function EditDesaAntiKorupsi() { fileId: '', }); + const [isSubmitting, setIsSubmitting] = useState(false); + + const [originalData, setOriginalData] = useState({ + name: "", + deskripsi: "", + kategoriId: "", + fileId: "", + fileUrl: "" + }); + const [previewFile, setPreviewFile] = useState(null); const [file, setFile] = useState(null); - const [isLoading, setIsLoading] = useState(false); // Load kategori useShallowEffect(() => { @@ -63,6 +72,14 @@ export default function EditDesaAntiKorupsi() { fileId: data.fileId, }); + setOriginalData({ + name: data.name, + deskripsi: data.deskripsi, + kategoriId: data.kategoriId, + fileId: data.fileId, + fileUrl: data.file?.link || "", + }); + if (data.file?.link) setPreviewFile(data.file.link); } catch (err) { console.error(err); @@ -91,12 +108,24 @@ export default function EditDesaAntiKorupsi() { setPreviewFile(URL.createObjectURL(selectedFile)); }; + const handleResetForm = () => { + setFormData({ + name: originalData.name, + deskripsi: originalData.deskripsi, + kategoriId: originalData.kategoriId, + fileId: originalData.fileId, + }); + setPreviewFile(originalData.fileUrl || null); + setFile(null); + toast.info("Form dikembalikan ke data awal"); + }; + const handleSubmit = async () => { if (!formData.name) return toast.warn('Masukkan judul dokumen'); if (!formData.kategoriId) return toast.warn('Pilih kategori dokumen'); - setIsLoading(true); try { + setIsSubmitting(true); // Update global state desaAntiKorupsiState.edit.form = { ...desaAntiKorupsiState.edit.form, ...formData }; @@ -116,16 +145,16 @@ export default function EditDesaAntiKorupsi() { console.error(err); toast.error('Terjadi kesalahan saat memperbarui data'); } finally { - setIsLoading(false); + setIsSubmitting(false); } }; return ( - + Edit Desa Anti Korupsi @@ -204,7 +233,7 @@ export default function EditDesaAntiKorupsi() { {previewFile && ( - + Pratinjau Dokumen @@ -219,23 +248,52 @@ export default function EditDesaAntiKorupsi() { >