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/apbdesa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/[id]/edit/page.tsx index 4e266661..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,19 +1,21 @@ /* 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, Stack, Text, TextInput, - Title + Title, } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack } from '@tabler/icons-react'; @@ -22,67 +24,120 @@ 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 */} @@ -114,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 54f43121..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 @@ -80,7 +80,7 @@ function DetailAPBDesa() { Detail APB Desa - + 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 db01743c..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,6 +6,7 @@ import { Box, Button, Group, + Loader, MultiSelect, Paper, Skeleton, @@ -17,11 +18,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 CreateAPBDesa() { const apbDesaState = useProxy(PendapatanAsliDesa.ApbDesa); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { apbDesaState.create.form = { @@ -33,9 +37,17 @@ 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 ( @@ -62,7 +74,7 @@ function CreateAPBDesa() { { apbDesaState.create.form.tahun = Number(val.target.value); }} @@ -94,6 +106,17 @@ function CreateAPBDesa() { {/* Action */} + + + {/* Tombol Simpan */} 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 b4b21fdd..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,6 +7,7 @@ import { Box, Button, Group, + Loader, Paper, Stack, TextInput, @@ -22,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 = @@ -58,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); @@ -70,6 +81,7 @@ function EditBelanja() { const handleSubmit = async () => { try { + setIsSubmitting(true); belanjaState.update.form = { ...belanjaState.update.form, name: formData.name, @@ -82,9 +94,19 @@ 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 */} @@ -135,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 50b6f1cd..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,6 +6,7 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, @@ -14,12 +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 CreateBelanja() { const belanjaState = useProxy(PendapatanAsliDesa.belanja); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const formatRupiah = (value: number | string) => { const number = @@ -47,9 +50,17 @@ 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 ( @@ -82,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 /> @@ -91,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); @@ -100,6 +111,17 @@ function CreateBelanja() { /> + + + {/* Tombol Simpan */} 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 6a4ad0cf..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,6 +6,7 @@ import { Box, Button, Group, + Loader, Paper, Stack, TextInput, @@ -21,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' @@ -55,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); @@ -65,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, @@ -79,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); } }; @@ -132,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 549ad7e6..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 @@ -5,6 +5,7 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, @@ -13,12 +14,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 CreatePembiayaan() { const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const formatRupiah = (value: number | string) => { const number = @@ -42,13 +45,21 @@ 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 ( @@ -81,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; }} @@ -92,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); @@ -101,6 +112,17 @@ function CreatePembiayaan() { /> + + + {/* Tombol Simpan */} 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 265f8386..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,6 +6,7 @@ import { Box, Button, Group, + Loader, Paper, Stack, TextInput, @@ -21,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: '', @@ -55,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); @@ -72,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, @@ -86,7 +107,10 @@ function EditPendapatan() { } catch (error) { console.error('Error updating jenis pendapatan:', error); toast.error('Terjadi kesalahan saat memperbarui jenis pendapatan'); + } finally { + setIsSubmitting(false); } + }; return ( @@ -137,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 2fa43d74..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,6 +5,7 @@ import { Box, Button, Group, + Loader, Paper, Stack, TextInput, @@ -12,11 +13,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 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, '')); @@ -39,9 +43,17 @@ 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 ( @@ -72,7 +84,7 @@ function CreatePendapatan() { > { pendapatanState.create.form.name = val.target.value; }} @@ -83,7 +95,7 @@ function CreatePendapatan() { { const raw = val.currentTarget.value; const cleanValue = unformatRupiah(raw); @@ -95,6 +107,17 @@ function CreatePendapatan() { /> + + + {/* Tombol Simpan */} 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 4a1b672b..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,6 +6,7 @@ import { Box, Button, Group, + Loader, Paper, Stack, TextInput, @@ -28,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(() => { @@ -41,6 +47,7 @@ export default function EditDemografiPekerjaan() { const loadData = async () => { try { + setIsSubmitting(true); stateDemografi.update.id = id; await stateDemografi.findUnique.load(id); @@ -51,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); } }; @@ -75,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 }; @@ -88,6 +112,8 @@ export default function EditDemografiPekerjaan() { } catch (error) { console.error('Error updating data:', error); toast.error('Gagal memperbarui data'); + } finally { + setIsSubmitting(false); } }; @@ -145,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 fec578be..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,6 +7,7 @@ import { Box, Button, Group, + Loader, Paper, Stack, TextInput, @@ -17,11 +18,13 @@ 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 = { @@ -32,16 +35,23 @@ 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 ( @@ -74,7 +84,7 @@ function CreateDemografiPekerjaan() { { stateDemografi.create.form.pekerjaan = val.currentTarget.value; @@ -84,7 +94,7 @@ function CreateDemografiPekerjaan() { { stateDemografi.create.form.lakiLaki = Number(val.currentTarget.value); @@ -94,7 +104,7 @@ function CreateDemografiPekerjaan() { { stateDemografi.create.form.perempuan = Number(val.currentTarget.value); @@ -103,6 +113,17 @@ function CreateDemografiPekerjaan() { /> + + + {/* Tombol Simpan */} 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 a49738c5..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 @@ -7,6 +7,7 @@ import { Box, Button, Group, + Loader, Paper, Stack, TextInput, @@ -22,7 +23,7 @@ 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 @@ -31,6 +32,11 @@ function EditJumlahPendudukMiskin() { totalPoorPopulation: 0, }); + const [originalData, setOriginalData] = useState({ + year: 0, + totalPoorPopulation: 0, + }); + // 🔹 Load data awal dari backend useEffect(() => { if (!id) return; @@ -44,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); @@ -62,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 }; @@ -75,6 +94,8 @@ function EditJumlahPendudukMiskin() { } catch (error) { console.error('Gagal menyimpan data:', error); toast.error('Terjadi kesalahan saat menyimpan data'); + } finally { + setIsSubmitting(false); } }; @@ -124,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 4bad22be..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 @@ -2,17 +2,19 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ 'use client'; import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Stack, TextInput, Title } 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 { useProxy } from 'valtio/utils'; 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,16 +24,24 @@ 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 ( @@ -59,7 +69,7 @@ export default function CreateJumlahPendudukMiskin() { { const value = e.currentTarget.value; @@ -71,7 +81,7 @@ export default function CreateJumlahPendudukMiskin() { { stateJPM.create.form.totalPoorPopulation = Number(e.currentTarget.value); @@ -80,6 +90,17 @@ export default function CreateJumlahPendudukMiskin() { /> + + + {/* Tombol Simpan */} 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 7677e027..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 } 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,26 +45,51 @@ 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 ( @@ -83,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 5c890bc0..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 @@ -3,16 +3,18 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur'; import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Stack, TextInput, Title } 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 = { @@ -26,18 +28,26 @@ 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 ( @@ -64,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 /> @@ -72,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 /> @@ -80,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 /> @@ -88,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 /> @@ -96,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 5ef9d16a..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 @@ -217,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 ab99accc..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 @@ -6,6 +6,7 @@ import { Box, Button, Group, + Loader, Paper, Stack, TextInput, @@ -25,6 +26,8 @@ function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() { ); const id = params.id; + const [isSubmitting, setIsSubmitting] = useState(false); + // ✅ state lokal, controlled const [formData, setFormData] = useState({ usia18_25: '', @@ -33,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) { @@ -45,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 || '', + }); + } }); } @@ -57,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 }; @@ -72,6 +100,8 @@ function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() { } catch (error) { console.error(error); toast.error('Terjadi kesalahan saat memperbarui data grafik'); + } finally { + setIsSubmitting(false); } }; @@ -134,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 3051f3f1..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 @@ -4,16 +4,18 @@ import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur'; import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Stack, TextInput, Title } 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,16 +28,24 @@ 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 ( @@ -64,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 /> @@ -72,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 /> @@ -80,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 /> @@ -88,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/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/create/page.tsx index c16fce6d..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,6 +7,7 @@ import { Box, Button, Group, + Loader, NumberInput, Paper, Select, @@ -17,12 +18,14 @@ import { 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', @@ -72,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); } }; @@ -176,7 +187,19 @@ function CreateJumlahPengangguran() {
{/* Action Button */} - + + {/* Tombol Batal */} + + + {/* Tombol Simpan */} 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 cc7371db..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,6 +7,7 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, @@ -23,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: '', @@ -35,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 () => { @@ -54,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); @@ -70,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 }; @@ -82,6 +119,8 @@ function EditLowonganKerja() { } catch (error) { console.error("Error updating lowongan kerja:", error); toast.error("Terjadi kesalahan saat memperbarui lowongan kerja"); + } finally { + setIsSubmitting(false); } }; @@ -175,6 +214,17 @@ function EditLowonganKerja() {
+ + + {/* Tombol Simpan */} 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 aac19f6d..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,6 +4,7 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, @@ -15,10 +16,13 @@ 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 = { @@ -34,9 +38,19 @@ 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 ( @@ -66,7 +80,7 @@ function CreateLowonganKerja() { > (lowonganState.create.form.posisi = val.target.value) } @@ -75,7 +89,7 @@ function CreateLowonganKerja() { required /> (lowonganState.create.form.namaPerusahaan = val.target.value) } @@ -84,7 +98,7 @@ function CreateLowonganKerja() { required /> (lowonganState.create.form.notelp = val.target.value) } @@ -93,7 +107,7 @@ function CreateLowonganKerja() { required /> (lowonganState.create.form.lokasi = val.target.value) } @@ -102,7 +116,7 @@ function CreateLowonganKerja() { required /> (lowonganState.create.form.tipePekerjaan = val.target.value) } @@ -111,7 +125,7 @@ function CreateLowonganKerja() { required /> (lowonganState.create.form.gaji = val.target.value) } @@ -146,6 +160,17 @@ function CreateLowonganKerja() { {/* Tombol Simpan */} + + + {/* Tombol Simpan */} 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 e62f85a4..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 @@ -5,6 +5,7 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, @@ -23,9 +24,9 @@ function EditKategoriProduk() { 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 () => { @@ -40,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); } }; @@ -59,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; @@ -81,13 +89,11 @@ 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 */} @@ -125,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 e0e6caa1..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,6 +5,7 @@ import { Box, Button, Group, + Loader, Paper, Stack, TextInput, @@ -12,7 +13,7 @@ import { } 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'; @@ -20,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(); @@ -32,27 +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 @@ -71,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/produk-pasar-desa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/[id]/edit/page.tsx index b87e474b..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,10 +5,12 @@ 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, @@ -40,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, @@ -50,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(); @@ -70,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) { @@ -87,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) { @@ -110,6 +151,8 @@ function EditPasarDesa() { } catch (error) { console.error('Error updating pasar desa:', error); toast.error('Terjadi kesalahan saat memperbarui pasar desa'); + } finally { + setIsSubmitting(false); } }; @@ -148,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" > @@ -167,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 && ( - + Preview Gambar + + {/* Tombol hapus (pojok kanan atas) */} + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -254,6 +317,17 @@ function EditPasarDesa() { /> + + + {/* Tombol Simpan */} 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 b611f44a..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,10 +3,12 @@ import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, MultiSelect, Paper, Stack, @@ -27,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(); @@ -47,25 +50,33 @@ 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 ( @@ -105,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" > @@ -126,7 +137,7 @@ export default function CreatePasarDesa() { {previewImage && ( - + Preview Gambar + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -142,7 +171,7 @@ export default function CreatePasarDesa() { (statePasar.pasarDesa.create.form.nama = e.target.value)} required /> @@ -152,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 /> @@ -165,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) { @@ -178,7 +207,7 @@ export default function CreatePasarDesa() { (statePasar.pasarDesa.create.form.alamatUsaha = e.target.value)} /> @@ -187,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)} /> @@ -207,6 +236,17 @@ export default function CreatePasarDesa() { {/* Tombol Submit */} + + + {/* Tombol Simpan */} 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 b6c0324e..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,6 +9,7 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, @@ -49,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(() => { @@ -68,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); @@ -99,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(); @@ -110,6 +136,8 @@ function EditProgramKemiskinan() { } catch (error) { console.error('Error update program:', error); toast.error('Terjadi kesalahan saat memperbarui program'); + } finally { + setIsSubmitting(false); } }; @@ -192,6 +220,17 @@ function EditProgramKemiskinan() { + + + {/* Tombol Simpan */} 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 d82b3ace..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,6 +7,7 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, @@ -27,6 +28,7 @@ function CreateProgramKemiskinan() { const router = useRouter(); const [lineChart, setLineChart] = useState([]); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { programState.create.form = { nama: '', @@ -40,24 +42,32 @@ 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 ( @@ -90,7 +100,7 @@ function CreateProgramKemiskinan() { (programState.create.form.nama = val.target.value)} required /> @@ -125,7 +135,7 @@ function CreateProgramKemiskinan() { (programState.create.form.statistik.jumlah = val.target.value) } @@ -135,7 +145,7 @@ function CreateProgramKemiskinan() { /> (programState.create.form.statistik.tahun = val.target.value) } @@ -147,6 +157,17 @@ function CreateProgramKemiskinan() { {/* Tombol Submit */} + + + {/* Tombol Simpan */} 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 b7525bf3..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 @@ -8,6 +8,7 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, @@ -24,7 +25,7 @@ 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 @@ -34,6 +35,12 @@ function EditSektorUnggulanDesa() { value: 0, }); + const [originalData, setOriginalData] = useState({ + name: '', + description: '', + value: 0, + }); + // Load data saat komponen mount useEffect(() => { if (id) { @@ -47,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) => { @@ -65,6 +77,7 @@ function EditSektorUnggulanDesa() { const handleSubmit = async () => { try { + setIsSubmitting(true); stateGrafik.update.id = id; stateGrafik.update.form = { ...formData }; // update global pas submit await stateGrafik.update.submit(); @@ -73,9 +86,20 @@ 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 ( @@ -129,6 +153,17 @@ function EditSektorUnggulanDesa() { /> + + + {/* Tombol Simpan */} 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 db72d661..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,6 +6,7 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, @@ -18,11 +19,13 @@ import { useState } from 'react'; import { useProxy } from 'valtio/utils'; 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 = { @@ -33,16 +36,24 @@ 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 ( @@ -75,7 +86,7 @@ function CreateSektorUnggulanDesa() { { stateGrafik.create.form.name = e.currentTarget.value; }} @@ -98,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); }} @@ -106,6 +117,17 @@ function CreateSektorUnggulanDesa() { /> + + + {/* Tombol Simpan */} 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 d125418d..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,10 +5,12 @@ 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, @@ -27,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: '', @@ -39,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); @@ -67,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); } @@ -79,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'); } @@ -103,6 +147,8 @@ 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); } }; @@ -164,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 && ( - + Preview Gambar + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -244,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/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/create/page.tsx index 137fb7fc..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 } 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,6 +76,8 @@ function CreatePegawaiBumDes() { } catch (error) { console.error("Error creating pegawai:", error); toast.error("Terjadi kesalahan saat menambahkan pegawai"); + } finally { + setIsSubmitting(false); } }; @@ -96,7 +105,7 @@ function CreatePegawaiBumDes() { (stateOrganisasi.create.form.namaLengkap = e.currentTarget.value)} required /> @@ -105,7 +114,7 @@ function CreatePegawaiBumDes() { (stateOrganisasi.create.form.gelarAkademik = e.currentTarget.value)} /> @@ -163,7 +172,7 @@ function CreatePegawaiBumDes() { {previewImage && ( - + Preview Gambar @@ -180,6 +189,24 @@ function CreatePegawaiBumDes() { }} loading='lazy' /> + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -188,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)} />
@@ -198,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)} />
@@ -207,7 +234,7 @@ function CreatePegawaiBumDes() { (stateOrganisasi.create.form.telepon = e.currentTarget.value)} />
@@ -216,7 +243,7 @@ function CreatePegawaiBumDes() { (stateOrganisasi.create.form.alamat = e.currentTarget.value)} /> @@ -242,6 +269,17 @@ function CreatePegawaiBumDes() { + + + {/* Tombol Simpan */} 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 972a710c..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 } 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,6 +100,8 @@ function EditPosisiOrganisasiBumDes() { } catch (err) { console.error('Error updating posisi organisasi:', err); // toast error biasanya sudah ada di update + } finally { + setIsSubmitting(false); } }; @@ -132,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 1241e1c2..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,37 +3,32 @@ 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 } 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'); @@ -45,6 +40,8 @@ function CreatePosisiOrganisasiBumDes() { } catch (error) { toast.error('Gagal menambahkan posisi organisasi'); console.error('Error:', error); + } finally { + setIsSubmitting(false); } }; @@ -71,7 +68,7 @@ function CreatePosisiOrganisasiBumDes() { (stateOrganisasi.create.form.nama = e.target.value)} required /> @@ -93,7 +90,7 @@ function CreatePosisiOrganisasiBumDes() { type="number" min={0} placeholder="Contoh: 1 (Angka semakin kecil, posisi semakin tinggi)" - defaultValue={stateOrganisasi.create.form.hierarki || ''} + value={stateOrganisasi.create.form.hierarki || ''} onChange={(e) => { const value = parseInt(e.target.value, 10); stateOrganisasi.create.form.hierarki = isNaN(value) ? 0 : value; @@ -101,10 +98,21 @@ function CreatePosisiOrganisasiBumDes() { required /> - + + {/* Tombol Batal */} + + + {/* Tombol Simpan */} 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 ad7f2184..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,10 +5,12 @@ 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, @@ -29,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 () => { @@ -49,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); } @@ -63,6 +77,7 @@ function EditDigitalSmartVillage() { const handleSubmit = async () => { try { + setIsSubmitting(true); stateDesaDigital.edit.form = { ...stateDesaDigital.edit.form, ...formData }; if (file) { @@ -79,9 +94,22 @@ 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 */} @@ -119,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" > @@ -138,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 && ( - + Preview Gambar + + {/* Tombol hapus (pojok kanan atas) */} + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -185,6 +233,17 @@ function EditDigitalSmartVillage() { {/* Tombol Simpan */} + + + {/* Tombol Simpan */} 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 cb0f810f..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,9 +2,11 @@ import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, + Loader, Paper, Stack, Text, @@ -26,6 +28,7 @@ export default function CreateDesaDigital() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { stateDesaDigital.create.form = { @@ -43,6 +46,7 @@ export default function CreateDesaDigital() { } try { + setIsSubmitting(true); const uploadRes = await ApiFetch.api.fileStorage.create.post({ file, name: file.name, @@ -63,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); } }; @@ -101,7 +107,7 @@ export default function CreateDesaDigital() { (stateDesaDigital.create.form.name = e.target.value)} radius="md" withAsterisk @@ -135,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={{ @@ -163,6 +169,7 @@ export default function CreateDesaDigital() { {/* Preview */} {previewImage && ( + { + 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/info-teknologi-tepat-guna/[id]/edit/page.tsx b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/[id]/edit/page.tsx index 248c8a72..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,10 +5,12 @@ 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, @@ -29,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(() => { @@ -50,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) { @@ -62,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, @@ -92,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); } }; @@ -141,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" > @@ -160,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 && ( - + Preview Gambar + + {/* Tombol hapus (pojok kanan atas) */} + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -198,6 +245,17 @@ function EditInfoTeknologiTepatGuna() { {/* Tombol submit */} + + + {/* Tombol Simpan */} 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 2021d79b..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,10 +2,12 @@ import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, @@ -26,6 +28,7 @@ function CreateInfoTeknologiTepatGuna() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { stateInfoTekno.create.form = { @@ -43,6 +46,7 @@ function CreateInfoTeknologiTepatGuna() { } try { + setIsSubmitting(true); const uploadRes = await ApiFetch.api.fileStorage.create.post({ file: file, name: file.name, @@ -64,6 +68,8 @@ function CreateInfoTeknologiTepatGuna() { } catch (error) { console.error('Error in handleSubmit:', error); toast.error('Terjadi kesalahan saat menyimpan data'); + } finally { + setIsSubmitting(false); } }; @@ -91,7 +97,7 @@ function CreateInfoTeknologiTepatGuna() { {/* Nama */} { stateInfoTekno.create.form.name = val.target.value; }} @@ -128,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" > @@ -149,7 +155,7 @@ function CreateInfoTeknologiTepatGuna() { {previewImage && ( - + Preview + { + 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/list-kolaborasi-inovasi/[id]/edit/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/[id]/edit/page.tsx index ffe7c536..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,12 +8,14 @@ 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 { useParams, useRouter } from "next/navigation"; import { useEffect, useState } from "react"; @@ -24,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: "", @@ -33,6 +36,14 @@ function EditKolaborasiInovasi() { kolaborator: "", }); + const [originalData, setOriginalData] = useState({ + name: "", + deskripsi: "", + tahun: "", + slug: "", + kolaborator: "", + }); + // Load data awal dari server useEffect(() => { const loadKolaborasi = async () => { @@ -49,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); @@ -59,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, @@ -73,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); } }; @@ -121,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/create/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/create/page.tsx index 0b63e248..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 } 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); } }; @@ -75,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 /> @@ -104,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/mitra-kolaborasi/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/[id]/page.tsx index 74c361e1..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,11 +4,13 @@ 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, @@ -35,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({ @@ -42,6 +45,12 @@ function EditMitraKolaborasi() { imageId: '', }); + const [originalData, setOriginalData] = useState({ + name: '', + imageId: '', + imageUrl: '', + }); + // Load data ke state lokal sekali saja useEffect(() => { const loadData = async () => { @@ -55,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); @@ -76,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) { @@ -105,6 +130,8 @@ function EditMitraKolaborasi() { } catch (error) { console.error('Error updating mitra:', error); toast.error('Terjadi kesalahan saat memperbarui mitra'); + } finally { + setIsSubmitting(false); } }; @@ -154,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" > @@ -173,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 @@ -181,7 +208,7 @@ function EditMitraKolaborasi() { {/* Preview Foto */} {previewImage ? ( - + Preview Gambar + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + ) : (
@@ -203,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 aa5ac040..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,10 +3,12 @@ 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, @@ -25,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 = { @@ -36,24 +39,32 @@ 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 ( @@ -82,7 +93,7 @@ function CreateMitraKolaborasi() { (state.create.form.name = e.target.value)} required /> @@ -102,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" > @@ -123,7 +134,7 @@ function CreateMitraKolaborasi() { {previewImage && ( - + Preview Gambar + { + 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/layanan-online-desa/jenis-layanan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/[id]/edit/page.tsx index 7bf76e10..780a9265 100644 --- a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/[id]/edit/page.tsx @@ -7,6 +7,7 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, @@ -23,6 +24,12 @@ function EditJenisLayanan() { const state = useProxy(layananonlineDesa.jenisLayanan); const router = useRouter(); const params = useParams(); + const [isSubmitting, setIsSubmitting] = useState(false); + + const [originalData, setOriginalData] = useState({ + nama: '', + deskripsi: '', + }); const [formData, setFormData] = useState({ nama: '', @@ -41,6 +48,10 @@ function EditJenisLayanan() { nama: data.nama ?? '', deskripsi: data.deskripsi ?? '', }); + setOriginalData({ + nama: data.nama ?? '', + deskripsi: data.deskripsi ?? '', + }); } } catch (error) { console.error('Error loading jenis layanan:', error); @@ -51,8 +62,17 @@ function EditJenisLayanan() { loadJenisLayanan(); }, [params?.id]); + const handleResetForm = () => { + 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, @@ -64,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); } }; @@ -123,6 +145,17 @@ function EditJenisLayanan() { {/* Tombol Submit */} + + + {/* Tombol Simpan */} 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 30aae0e6..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,6 +6,7 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, @@ -14,12 +15,14 @@ import { } 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(); @@ -33,9 +36,17 @@ 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 ( @@ -66,7 +77,7 @@ function CreateJenisLayanan() { > { statePasar.create.form.nama = val.target.value; }} @@ -75,7 +86,7 @@ function CreateJenisLayanan() { required /> { statePasar.create.form.deskripsi = val.target.value; }} @@ -85,6 +96,17 @@ function CreateJenisLayanan() { /> + + + {/* Tombol Simpan */} 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 de4c6c24..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,6 +6,7 @@ import { Box, Button, Group, + Loader, Paper, Stack, TextInput, @@ -22,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 () => { @@ -40,6 +46,9 @@ function EditJenisPengaduan() { setFormData({ nama: data.nama || '', }); + setOriginalData({ + nama: data.nama || '', + }); } } catch (error) { console.error('Error loading jenis pengaduan:', error); @@ -57,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'); @@ -74,6 +90,7 @@ function EditJenisPengaduan() { } try { + setIsSubmitting(true); const success = await state.edit.update(); if (success) { @@ -82,6 +99,8 @@ function EditJenisPengaduan() { } catch (error) { console.error('Error updating jenis pengaduan:', error); // toast ditangani di dalam state.update + } finally { + setIsSubmitting(false); } }; @@ -121,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 31999381..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,6 +6,7 @@ import { Box, Button, Group, + Loader, Paper, Stack, TextInput, @@ -13,13 +14,14 @@ import { } 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(); }, []); @@ -31,9 +33,17 @@ 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 ( @@ -61,12 +71,23 @@ function CreateJenisPengaduan() { (state.create.form.nama = e.target.value)} required /> + + + {/* Tombol Simpan */} 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/create/page.tsx b/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/create/page.tsx index 2609b036..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,6 +4,7 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, @@ -16,10 +17,13 @@ import { useProxy } from 'valtio/utils'; import CreateEditor from '../../../_com/createEditor'; 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 = { @@ -31,11 +35,18 @@ function CreateProgramKreatifDesa() { }; const handleSubmit = async () => { - const success = await stateCreate.create.create(); + try { + const success = await stateCreate.create.create(); - if (success) { - resetForm(); - router.push("/admin/inovasi/program-kreatif-desa"); + 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); } }; @@ -65,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 /> @@ -82,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 /> @@ -101,6 +112,17 @@ function CreateProgramKreatifDesa() { {/* Tombol Submit */} + + + {/* Tombol Simpan */} 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 ? ( - + + Preview Gambar + + {/* 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 && ( - + Preview + { + 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 */} (stateKorupsi.create.form.kategoriId = val || '')} - data={ - korupsiState.kategoriDesaAntiKorupsi.findMany.data?.map((v) => ({ - value: v.id, - label: v.name, - })) || [] - } - required + data={korupsiState.kategoriDesaAntiKorupsi.findMany.data?.map((item) => ({ + label: item.name, + value: item.id, + })) || []} + value={stateKorupsi.create.form.kategoriId || null} + onChange={(val: string | null) => { + if (val) { + const selected = korupsiState.kategoriDesaAntiKorupsi.findMany.data?.find( + (item) => item.id === val + ); + if (selected) { + stateKorupsi.create.form.kategoriId = selected.id; + } + } else { + stateKorupsi.create.form.kategoriId = ''; + } + }} searchable clearable + nothingFoundMessage="Tidak ditemukan" + required /> - + + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/responden/[id]/edit/page.tsx b/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/responden/[id]/edit/page.tsx index f203df16..6e123982 100644 --- a/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/responden/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/responden/[id]/edit/page.tsx @@ -1,19 +1,21 @@ -'use client' /* eslint-disable react-hooks/exhaustive-deps */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +'use client'; import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan'; import colors from '@/con/colors'; import { Box, Button, Group, + Loader, Paper, Select, Stack, Text, TextInput, - Title + Title, } from '@mantine/core'; -import { IconArrowBack, IconDeviceFloppy } from '@tabler/icons-react'; +import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useCallback, useEffect, useState } from 'react'; import { toast } from 'react-toastify'; @@ -30,9 +32,13 @@ interface FormResponden { function EditResponden() { const router = useRouter(); const params = useParams() as { id: string }; - const state = useProxy(indeksKepuasanState.responden); const id = params.id; + // ✅ proxy asli untuk mutasi + const state = indeksKepuasanState.responden; + // ✅ snapshot untuk re-render (read-only) + const snapshot = useProxy(indeksKepuasanState.responden); + const [formData, setFormData] = useState({ name: '', tanggal: '', @@ -41,31 +47,43 @@ function EditResponden() { kelompokUmurId: '', }); - // Helper untuk load pilihan select + const [originalData, setOriginalData] = useState({ + name: '', + tanggal: '', + jenisKelaminId: '', + ratingId: '', + kelompokUmurId: '', + }); + + const [isSubmitting, setIsSubmitting] = useState(false); + + // 🔹 Load data pilihan select const loadSelectOptions = useCallback(() => { indeksKepuasanState.jenisKelaminResponden.findMany.load(); indeksKepuasanState.pilihanRatingResponden.findMany.load(); indeksKepuasanState.kelompokUmurResponden.findMany.load(); }, []); - // Load data responden + // 🔹 Load data responden by ID const loadResponden = useCallback(async () => { if (!id) return; - try { const data = await state.update.load(id); if (!data) return; - setFormData({ - name: data.name, - tanggal: data.tanggal, - jenisKelaminId: data.jenisKelaminId, - ratingId: data.ratingId, - kelompokUmurId: data.kelompokUmurId, - }); + const newForm = { + name: data.name || '', + tanggal: data.tanggal || '', + jenisKelaminId: data.jenisKelaminId || '', + ratingId: data.ratingId || '', + kelompokUmurId: data.kelompokUmurId || '', + }; + + setFormData(newForm); + setOriginalData(newForm); } catch (error) { - console.error("Error loading responden:", error); - toast.error("Gagal memuat data responden"); + console.error('Error loading responden:', error); + toast.error('Gagal memuat data responden'); } }, [id]); @@ -74,14 +92,30 @@ function EditResponden() { loadResponden(); }, [loadSelectOptions, loadResponden]); + // 🔹 Submit data const handleSubmit = async () => { - state.update.id = id; - state.update.form = { ...formData }; // sinkronisasi manual - await state.update.submit(); - router.push('/admin/landing-page/indeks-kepuasan-masyarakat/responden'); + try { + setIsSubmitting(true); + state.update.id = id; + state.update.form = { ...formData }; // mutasi proxy asli ✅ + await state.update.submit(); + toast.success('Responden berhasil diperbarui!'); + router.push('/admin/landing-page/indeks-kepuasan-masyarakat/responden'); + } catch (error) { + console.error('Error updating responden:', error); + toast.error('Gagal memperbarui responden'); + } finally { + setIsSubmitting(false); + } }; - // Reusable Select component + // 🔹 Reset form ke data awal + const handleResetForm = () => { + setFormData({ ...originalData }); + toast.info('Form dikembalikan ke data awal'); + }; + + // 🔹 Reusable Select component const ControlledSelect = ({ label, value, @@ -98,30 +132,28 @@ function EditResponden() { error?: string; placeholder?: string; loading?: boolean; - }) => { - return ( - {label}} + value={value} + onChange={(val) => onChange(val || '')} + data={options} + placeholder={placeholder} + disabled={loading} + clearable + searchable + required + radius="md" + error={error} + /> + ); return ( - + Edit Responden @@ -144,6 +176,7 @@ function EditResponden() { radius="md" required /> + setFormData({ ...formData, jenisKelaminId: val })} options={(indeksKepuasanState.jenisKelaminResponden.findMany.data || []) - .filter(Boolean) .map((v) => ({ value: v.id || '', label: v.name || 'Tanpa Nama' }))} loading={indeksKepuasanState.jenisKelaminResponden.findMany.loading} error={!formData.jenisKelaminId ? 'Pilih jenis kelamin' : undefined} @@ -169,7 +201,6 @@ function EditResponden() { value={formData.ratingId} onChange={(val) => setFormData({ ...formData, ratingId: val })} options={(indeksKepuasanState.pilihanRatingResponden.findMany.data || []) - .filter(Boolean) .map((v) => ({ value: v.id || '', label: v.name || 'Tanpa Nama' }))} loading={indeksKepuasanState.pilihanRatingResponden.findMany.loading} error={!formData.ratingId ? 'Pilih rating' : undefined} @@ -180,23 +211,33 @@ function EditResponden() { value={formData.kelompokUmurId} onChange={(val) => setFormData({ ...formData, kelompokUmurId: val })} options={(indeksKepuasanState.kelompokUmurResponden.findMany.data || []) - .filter(Boolean) .map((v) => ({ value: v.id || '', label: v.name || 'Tanpa Nama' }))} loading={indeksKepuasanState.kelompokUmurResponden.findMany.loading} error={!formData.kelompokUmurId ? 'Pilih kelompok umur' : undefined} /> - - + diff --git a/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/responden/create/page.tsx b/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/responden/create/page.tsx index 7056c869..6c8c149e 100644 --- a/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/responden/create/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/responden/create/page.tsx @@ -1,21 +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 grafikBerdasarkanJenisKelamin from '@/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanJenisKelamin'; -import { useProxy } from 'valtio/utils'; -import { useState } from 'react'; -import colors from '@/con/colors'; -import { Box, Button, Paper, Stack, Title, TextInput, Select, Text } from '@mantine/core'; -import { IconArrowBack } from '@tabler/icons-react'; import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan'; +import colors from '@/con/colors'; +import { Box, Button, Group, Loader, Paper, Select, Stack, TextInput, 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 { useProxy } from 'valtio/utils'; function RespondenCreate() { const router = useRouter(); const stategrafikBerdasarkanResponden = useProxy(indeksKepuasanState.responden) const [donutData, setDonutData] = useState([]); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { stategrafikBerdasarkanResponden.create.form = { @@ -35,6 +34,7 @@ function RespondenCreate() { }) const handleSubmit = async () => { + setIsSubmitting(true); try { const id = await stategrafikBerdasarkanResponden.create.create(); if (typeof id !== 'undefined') { @@ -45,9 +45,11 @@ function RespondenCreate() { } } resetForm(); - router.push("/admin/ppid/ikm-desa-darmasaba/responden"); + router.push("/admin/landing-page/indeks-kepuasan-masyarakat/responden"); } catch (error) { console.error('Error submitting form:', error); + } finally { + setIsSubmitting(false); } } return ( @@ -64,7 +66,7 @@ function RespondenCreate() { label="Nama" type='text' placeholder="masukkan nama" - defaultValue={stategrafikBerdasarkanResponden.create.form.name} + value={stategrafikBerdasarkanResponden.create.form.name} onChange={(val) => { stategrafikBerdasarkanResponden.create.form.name = val.currentTarget.value; }} @@ -73,7 +75,7 @@ function RespondenCreate() { label="Tanggal" type="date" placeholder="masukkan tanggal" - defaultValue={stategrafikBerdasarkanResponden.create.form.tanggal} + value={stategrafikBerdasarkanResponden.create.form.tanggal} onChange={(val) => { stategrafikBerdasarkanResponden.create.form.tanggal = val.currentTarget.value; }} @@ -96,24 +98,24 @@ function RespondenCreate() { } disabled={indeksKepuasanState.jenisKelaminResponden.findMany.loading} /> - - + + {/* Tombol Batal */} + + + {/* Tombol Simpan */} + + diff --git a/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/responden/page.tsx b/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/responden/page.tsx index eadaee3d..ea36e1d3 100644 --- a/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/responden/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/responden/page.tsx @@ -97,11 +97,11 @@ function ListResponden({ search }: ListRespondenProps) { > - No - Nama - Tanggal - Jenis Kelamin - Aksi + No + Nama + Tanggal + Jenis Kelamin + Aksi @@ -116,9 +116,9 @@ function ListResponden({ search }: ListRespondenProps) { ) : ( filteredData.map((item, index) => ( - {index + 1} - {item.name} - + {index + 1} + {item.name} + {item.tanggal ? new Date(item.tanggal).toLocaleDateString('id-ID', { @@ -129,12 +129,12 @@ function ListResponden({ search }: ListRespondenProps) { : '-'} - + {item.jenisKelamin.name} - + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/landing-page/prestasi-desa/kategori-prestasi-desa/create/page.tsx b/src/app/admin/(dashboard)/landing-page/prestasi-desa/kategori-prestasi-desa/create/page.tsx index 3d3a09d3..ebfd82cc 100644 --- a/src/app/admin/(dashboard)/landing-page/prestasi-desa/kategori-prestasi-desa/create/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/prestasi-desa/kategori-prestasi-desa/create/page.tsx @@ -2,16 +2,18 @@ 'use client' import prestasiState from '@/app/admin/(dashboard)/_state/landing-page/prestasi-desa'; 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 { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; function CreateKategoriPrestasi() { const router = useRouter(); const stateKategori = useProxy(prestasiState.kategoriPrestasi) + const [isSubmitting, setIsSubmitting] = useState(false); useEffect(() => { stateKategori.findMany.load(); @@ -24,9 +26,17 @@ function CreateKategoriPrestasi() { } const handleSubmit = async () => { - await stateKategori.create.create(); - resetForm(); - router.push("/admin/landing-page/prestasi-desa/kategori-prestasi-desa") + try { + setIsSubmitting(true); + await stateKategori.create.create(); + resetForm(); + router.push("/admin/landing-page/prestasi-desa/kategori-prestasi-desa") + } catch (error) { + console.error("Error creating kategori prestasi:", error); + toast.error("Terjadi kesalahan saat menambahkan kategori prestasi"); + } finally { + setIsSubmitting(false); + } } return ( @@ -52,12 +62,24 @@ function CreateKategoriPrestasi() { (stateKategori.create.form.name = val.target.value)} required /> - + + {/* Tombol Batal */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/landing-page/prestasi-desa/list-prestasi-desa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/landing-page/prestasi-desa/list-prestasi-desa/[id]/edit/page.tsx index b10fea7a..085d347a 100644 --- a/src/app/admin/(dashboard)/landing-page/prestasi-desa/list-prestasi-desa/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/prestasi-desa/list-prestasi-desa/[id]/edit/page.tsx @@ -1,6 +1,6 @@ /* eslint-disable react-hooks/exhaustive-deps */ 'use client'; -import { Box, Button, Group, Image, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core'; +import { ActionIcon, Box, Button, Group, Image, Paper, Select, Stack, Text, TextInput, Title, Loader } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -28,6 +28,17 @@ export default function EditPrestasiDesa() { kategoriId: '', imageId: '', }); + + const [isSubmitting, setIsSubmitting] = useState(false); + + const [originalData, setOriginalData] = useState({ + name: "", + deskripsi: "", + kategoriId: "", + imageId: "", + imageUrl: "", + }); + const [previewFile, setPreviewFile] = useState(null); const [file, setFile] = useState(null); const params = useParams(); @@ -59,6 +70,14 @@ export default function EditPrestasiDesa() { imageId: data.imageId, }); + setOriginalData({ + name: data.name, + deskripsi: data.deskripsi, + kategoriId: data.kategoriId, + imageId: data.imageId, + imageUrl: data.image?.link || "", + }); + if (data.image?.link) setPreviewFile(data.image.link); } } catch (error) { @@ -70,9 +89,22 @@ export default function EditPrestasiDesa() { loadPrestasi(); }, [params?.id]); + const handleResetForm = () => { + setFormData({ + name: originalData.name, + deskripsi: originalData.deskripsi, + kategoriId: originalData.kategoriId, + imageId: originalData.imageId, + }); + setPreviewFile(originalData.imageUrl || null); + setFile(null); + toast.info("Form dikembalikan ke data awal"); + }; + + const handleSubmit = async () => { try { - // Jika ada file baru, upload dulu + setIsSubmitting(true); let imageId = formData.imageId; if (file) { const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name }); @@ -90,15 +122,17 @@ export default function EditPrestasiDesa() { } catch (error) { console.error('Error updating prestasi desa:', error); toast.error('Terjadi kesalahan saat memperbarui prestasi desa'); + } finally { + setIsSubmitting(false); } }; return ( - + Edit Prestasi Desa @@ -118,12 +152,29 @@ export default function EditPrestasiDesa() { (stateCreate.create.form.kategoriId = val ?? '')} - data={ - prestasiState.kategoriPrestasi.findMany.data?.map((v) => ({ - value: v.id, - label: v.name, - })) || [] - } + data={prestasiState.kategoriPrestasi.findMany.data?.map((item) => ({ + label: item.name, + value: item.id, + })) || []} + value={snapPrestasi.create.form.kategoriId || null} + onChange={(val: string | null) => { + if (val) { + const selected = prestasiState.kategoriPrestasi.findMany.data?.find( + (item) => item.id === val + ); + if (selected) { + prestasi.create.form.kategoriId = selected.id; + } + } else { + prestasi.create.form.kategoriId = ''; + } + }} + searchable + clearable + nothingFoundMessage="Tidak ditemukan" required /> - + {/* ======= Tombol Aksi ======= */} + + {/* Tombol Batal */} + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/landing-page/prestasi-desa/list-prestasi-desa/page.tsx b/src/app/admin/(dashboard)/landing-page/prestasi-desa/list-prestasi-desa/page.tsx index 1059a1f7..6a9b88bf 100644 --- a/src/app/admin/(dashboard)/landing-page/prestasi-desa/list-prestasi-desa/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/prestasi-desa/list-prestasi-desa/page.tsx @@ -1,10 +1,10 @@ -/* eslint-disable react-hooks/exhaustive-deps */ 'use client' import colors from '@/con/colors'; 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 { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { useProxy } from 'valtio/utils'; import HeaderSearch from '../../../_com/header'; import prestasiState from '../../../_state/landing-page/prestasi-desa'; @@ -37,23 +37,9 @@ function ListPrestasi({ search }: { search: string }) { load, } = listState.findMany - // Debug log - console.log('ListPrestasi state:', { - loading, - data: data?.length, - page, - totalPages, - search - }); - - useEffect(() => { - console.log('Loading data...', { page, search }); - load(page, 10, search).then(() => { - console.log('Data loaded:', listState.findMany.data); - }).catch(error => { - console.error('Error loading data:', error); - }); - }, [page, search]) + useShallowEffect(() => { + load(page, 10, search); + }, [page, search]); const filteredData = data || [] @@ -70,14 +56,14 @@ function ListPrestasi({ search }: { search: string }) { Daftar Prestasi Desa - + @@ -107,16 +93,16 @@ function ListPrestasi({ search }: { search: string }) { - + )) diff --git a/src/app/admin/(dashboard)/landing-page/profile/_lib/layoutTabs.tsx b/src/app/admin/(dashboard)/landing-page/profil/_lib/layoutTabs.tsx similarity index 95% rename from src/app/admin/(dashboard)/landing-page/profile/_lib/layoutTabs.tsx rename to src/app/admin/(dashboard)/landing-page/profil/_lib/layoutTabs.tsx index c5e029cb..6d889a1f 100644 --- a/src/app/admin/(dashboard)/landing-page/profile/_lib/layoutTabs.tsx +++ b/src/app/admin/(dashboard)/landing-page/profil/_lib/layoutTabs.tsx @@ -22,19 +22,19 @@ function LayoutTabs({ children }: { children: React.ReactNode }) { { label: "Program Inovasi", value: "program-inovasi", - href: "/admin/landing-page/profile/program-inovasi", + href: "/admin/landing-page/profil/program-inovasi", icon: , }, { label: "Pejabat Desa", value: "pejabat-desa", - href: "/admin/landing-page/profile/pejabat-desa", + href: "/admin/landing-page/profil/pejabat-desa", icon: , }, { label: "Media Sosial", value: "media-sosial", - href: "/admin/landing-page/profile/media-sosial", + href: "/admin/landing-page/profil/media-sosial", icon: , }, ]; diff --git a/src/app/admin/(dashboard)/landing-page/profile/layout.tsx b/src/app/admin/(dashboard)/landing-page/profil/layout.tsx similarity index 100% rename from src/app/admin/(dashboard)/landing-page/profile/layout.tsx rename to src/app/admin/(dashboard)/landing-page/profil/layout.tsx diff --git a/src/app/admin/(dashboard)/landing-page/profile/media-sosial/[id]/edit/page.tsx b/src/app/admin/(dashboard)/landing-page/profil/media-sosial/[id]/edit/page.tsx similarity index 69% rename from src/app/admin/(dashboard)/landing-page/profile/media-sosial/[id]/edit/page.tsx rename to src/app/admin/(dashboard)/landing-page/profil/media-sosial/[id]/edit/page.tsx index dd41c2a9..2cb77a63 100644 --- a/src/app/admin/(dashboard)/landing-page/profile/media-sosial/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/profil/media-sosial/[id]/edit/page.tsx @@ -4,6 +4,7 @@ import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page 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'; @@ -34,6 +36,15 @@ function EditMediaSosial() { imageId: '', }); + const [isSubmitting, setIsSubmitting] = useState(false); + + const [originalData, setOriginalData] = useState({ + name: "", + iconUrl: "", + imageId: "", + imageUrl: "", + }); + // Load data by ID useEffect(() => { const id = params?.id as string; @@ -44,12 +55,21 @@ function EditMediaSosial() { const data = await stateMediaSosial.update.load(id); if (data) { - setFormData({ - name: data.name || '', - iconUrl: data.iconUrl || '', - imageId: data.imageId || '', + // isi form awal + const newForm = { + name: data.name || "", + iconUrl: data.iconUrl || "", + imageId: data.imageId || "", + }; + setFormData(newForm); + + // simpan juga versi original + setOriginalData({ + ...newForm, + imageUrl: data.image?.link || "", }); - if (data.image?.link) setPreviewImage(data.image.link); + + setPreviewImage(data.image?.link || null); } } catch (error) { console.error('Error loading media sosial:', error); @@ -67,6 +87,7 @@ function EditMediaSosial() { }; const handleSubmit = async () => { + setIsSubmitting(true); try { // update global state hanya saat submit stateMediaSosial.update.form = { ...stateMediaSosial.update.form, ...formData }; @@ -82,13 +103,27 @@ function EditMediaSosial() { await stateMediaSosial.update.update(); toast.success('Media sosial berhasil diperbarui!'); - router.push('/admin/landing-page/profile/media-sosial'); + router.push('/admin/landing-page/profil/media-sosial'); } catch (error) { console.error('Error updating media sosial:', error); toast.error('Terjadi kesalahan saat memperbarui media sosial'); + } finally { + setIsSubmitting(false); } }; + // ✅ Tombol Batal → balikin ke data original + const handleResetForm = () => { + setFormData({ + name: originalData.name, + iconUrl: originalData.iconUrl, + imageId: originalData.imageId, + }); + setPreviewImage(originalData.imageUrl || null); + setFile(null); + toast.info("Form dikembalikan ke data awal"); + }; + return ( - Gambar Media Sosial + Gambar Program Inovasi { @@ -127,7 +162,7 @@ function EditMediaSosial() { }} 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" > @@ -146,25 +181,46 @@ function EditMediaSosial() { Seret gambar atau klik untuk memilih file - Maksimal 5MB, format gambar wajib + Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp + {/* ✅ Preview gambar + tombol X */} {previewImage && ( - + + + {/* Tombol hapus (pojok kanan atas) */} + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -187,7 +243,19 @@ function EditMediaSosial() { required /> - + + {/* Tombol Batal */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/landing-page/profile/media-sosial/[id]/page.tsx b/src/app/admin/(dashboard)/landing-page/profil/media-sosial/[id]/page.tsx similarity index 97% rename from src/app/admin/(dashboard)/landing-page/profile/media-sosial/[id]/page.tsx rename to src/app/admin/(dashboard)/landing-page/profil/media-sosial/[id]/page.tsx index d6aecffd..d84a78a6 100644 --- a/src/app/admin/(dashboard)/landing-page/profile/media-sosial/[id]/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/profil/media-sosial/[id]/page.tsx @@ -25,7 +25,7 @@ function DetailMediaSosial() { stateMediaSosial.delete.byId(selectedId); setModalHapus(false); setSelectedId(null); - router.push("/admin/landing-page/profile/media-sosial"); + router.push("/admin/landing-page/profil/media-sosial"); } }; @@ -110,7 +110,7 @@ function DetailMediaSosial() { diff --git a/src/app/admin/(dashboard)/landing-page/profile/media-sosial/page.tsx b/src/app/admin/(dashboard)/landing-page/profil/media-sosial/page.tsx similarity index 98% rename from src/app/admin/(dashboard)/landing-page/profile/media-sosial/page.tsx rename to src/app/admin/(dashboard)/landing-page/profil/media-sosial/page.tsx index 587590b3..e1d8c56b 100644 --- a/src/app/admin/(dashboard)/landing-page/profile/media-sosial/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/profil/media-sosial/page.tsx @@ -56,7 +56,7 @@ function ListMediaSosial({ search }: { search: string }) { Daftar Media Sosial - @@ -100,7 +100,7 @@ function ListMediaSosial({ search }: { search: string }) { variant="light" color="blue" leftSection={} - onClick={() => router.push(`/admin/landing-page/profile/media-sosial/${item.id}`)} + onClick={() => router.push(`/admin/landing-page/profil/media-sosial/${item.id}`)} > Detail diff --git a/src/app/admin/(dashboard)/landing-page/profile/pejabat-desa/[id]/page.tsx b/src/app/admin/(dashboard)/landing-page/profil/pejabat-desa/[id]/page.tsx similarity index 64% rename from src/app/admin/(dashboard)/landing-page/profile/pejabat-desa/[id]/page.tsx rename to src/app/admin/(dashboard)/landing-page/profil/pejabat-desa/[id]/page.tsx index 25599d5b..0c7e40ef 100644 --- a/src/app/admin/(dashboard)/landing-page/profile/pejabat-desa/[id]/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/profil/pejabat-desa/[id]/page.tsx @@ -2,8 +2,9 @@ import colors from '@/con/colors'; import { + ActionIcon, Alert, Box, Button, Center, Group, Image, - Paper, Stack, Text, TextInput, Title + Paper, Stack, Text, TextInput, Title, Loader } from '@mantine/core'; import { useEffect, useState } from 'react'; import { useProxy } from 'valtio/utils'; @@ -24,7 +25,14 @@ function EditPejabatDesa() { const [formData, setFormData] = useState({ name: '', position: '', - imageId: null as string | null, + imageId: '' + }); + + const [originalData, setOriginalData] = useState({ + name: '', + position: '', + imageId: "", + imageUrl: "", }); // UI states @@ -38,7 +46,7 @@ function EditPejabatDesa() { const id = params?.id as string; if (!id) { toast.error("ID tidak valid"); - router.push("/admin/landing-page/profile/pejabat-desa"); + router.push("/admin/landing-page/profil/pejabat-desa"); return; } @@ -46,26 +54,27 @@ function EditPejabatDesa() { const profileData = await profileLandingPageState.pejabatDesa.findUnique.load(id); if (profileData) { - // Initialize form data - setFormData({ - name: profileData.name || '', - position: profileData.position || '', - imageId: profileData.imageId || null, + // isi form awal + const newForm = { + name: profileData.name || "", + position: profileData.position || "", + imageId: profileData.imageId || "", + }; + setFormData(newForm); + + // simpan juga versi original + setOriginalData({ + ...newForm, + imageUrl: profileData.image?.link || "", }); - // Initialize edit state with profile data - profileLandingPageState.pejabatDesa.edit.initialize({ - ...profileData, - imageId: profileData.imageId || '' - }); - - if (profileData.image?.link) { - setPreviewImage(profileData.image.link); - } + setPreviewImage(profileData.image?.link || null); } } catch (error) { - console.error("Error loading profile:", error); - toast.error("Gagal memuat data profile"); + console.error("Error loading program inovasi:", error); + toast.error( + error instanceof Error ? error.message : "Gagal mengambil data program inovasi" + ); } }; @@ -78,22 +87,6 @@ function EditPejabatDesa() { setFormData(prev => ({ ...prev, [field]: value })); }; - // Handle file change - const handleFileChange = (newFile: File | null) => { - if (!newFile) { - setFile(null); - return; - } - - setFile(newFile); - - const reader = new FileReader(); - reader.onload = (event) => { - setPreviewImage(event.target?.result as string); - }; - reader.readAsDataURL(newFile); - }; - // Submit form const handleSubmit = async () => { if (isSubmitting || !formData.name.trim()) { @@ -133,7 +126,7 @@ function EditPejabatDesa() { const success = await profileLandingPageState.pejabatDesa.edit.submit(); if (success) { toast.success("Berhasil menyimpan perubahan"); - router.push("/admin/landing-page/profile/pejabat-desa"); + router.push("/admin/landing-page/profil/pejabat-desa"); } } catch (error) { console.error("Error submitting form:", error); @@ -143,6 +136,17 @@ function EditPejabatDesa() { } }; + const handleResetForm = () => { + setFormData({ + name: originalData.name, + position: originalData.position, + imageId: originalData.imageId, + }); + setPreviewImage(originalData.imageUrl || null); + setFile(null); + toast.info("Form dikembalikan ke data awal"); + }; + const handleBack = () => router.back(); // Loading @@ -216,49 +220,78 @@ function EditPejabatDesa() { {/* File Upload */} - Gambar + + Gambar Program Inovasi + handleFileChange(files[0])} - onReject={() => toast.error('File tidak valid.')} + onDrop={(files) => { + const selectedFile = files[0]; + if (selectedFile) { + setFile(selectedFile); + setPreviewImage(URL.createObjectURL(selectedFile)); + } + }} + onReject={() => toast.error('File tidak valid, gunakan format gambar')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} + radius="md" + p="xl" > - + - + - + - + - -
- - Drag gambar ke sini atau klik untuk pilih file + + + Seret gambar atau klik untuk memilih file - - Maksimal 5MB dan harus format gambar + + Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp -
+
+ {/* ✅ Preview gambar + tombol X */} {previewImage && ( - + + + {/* Tombol hapus (pojok kanan atas) */} + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -267,7 +300,7 @@ function EditPejabatDesa() { Preview Gambar {previewImage ? ( - + ) : (
@@ -279,23 +312,33 @@ function EditPejabatDesa() { {/* Submit */} - - - + + + + diff --git a/src/app/admin/(dashboard)/landing-page/profile/pejabat-desa/page.tsx b/src/app/admin/(dashboard)/landing-page/profil/pejabat-desa/page.tsx similarity index 98% rename from src/app/admin/(dashboard)/landing-page/profile/pejabat-desa/page.tsx rename to src/app/admin/(dashboard)/landing-page/profil/pejabat-desa/page.tsx index a425140a..f9f37707 100644 --- a/src/app/admin/(dashboard)/landing-page/profile/pejabat-desa/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/profil/pejabat-desa/page.tsx @@ -40,7 +40,7 @@ function Page() { variant="light" leftSection={} radius="md" - onClick={() => router.push(`/admin/landing-page/profile/pejabat-desa/${allList.findUnique.data?.id}`)} + onClick={() => router.push(`/admin/landing-page/profil/pejabat-desa/${allList.findUnique.data?.id}`)} > Edit diff --git a/src/app/admin/(dashboard)/landing-page/profile/program-inovasi/[id]/edit/page.tsx b/src/app/admin/(dashboard)/landing-page/profil/program-inovasi/[id]/edit/page.tsx similarity index 71% rename from src/app/admin/(dashboard)/landing-page/profile/program-inovasi/[id]/edit/page.tsx rename to src/app/admin/(dashboard)/landing-page/profil/program-inovasi/[id]/edit/page.tsx index b3892f26..9d41f051 100644 --- a/src/app/admin/(dashboard)/landing-page/profile/program-inovasi/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/profil/program-inovasi/[id]/edit/page.tsx @@ -5,6 +5,7 @@ import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, @@ -13,7 +14,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,15 @@ function EditProgramInovasi() { imageId: "", link: "", }) + const [isSubmitting, setIsSubmitting] = useState(false); + + const [originalData, setOriginalData] = useState({ + name: "", + description: "", + imageId: "", + link: "", + imageUrl: "", + }); useEffect(() => { const id = params?.id as string; @@ -44,19 +55,22 @@ function EditProgramInovasi() { try { const data = await stateProgramInovasi.update.load(id); if (data) { - setFormData({ + // isi form awal + const newForm = { name: data.name || "", description: data.description || "", imageId: data.imageId || "", - link: data.link || "" + link: data.link || "", + }; + setFormData(newForm); + + // simpan juga versi original + setOriginalData({ + ...newForm, + imageUrl: data.image?.link || "", }); - // Preview image - if (data.image?.link) { - setPreviewImage(data.image.link); - } else { - setPreviewImage(null); - } + setPreviewImage(data.image?.link || null); } } catch (error) { console.error("Error loading program inovasi:", error); @@ -64,13 +78,26 @@ function EditProgramInovasi() { error instanceof Error ? error.message : "Gagal mengambil data program inovasi" ); } - } - + }; loadProgramInovasi(); }, [params?.id]); + // ✅ Tombol Batal → balikin ke data original + const handleResetForm = () => { + setFormData({ + name: originalData.name, + description: originalData.description, + imageId: originalData.imageId, + link: originalData.link, + }); + setPreviewImage(originalData.imageUrl || null); + setFile(null); + toast.info("Form dikembalikan ke data awal"); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); // Upload file kalau ada file baru let imageId = formData.imageId; if (file) { @@ -93,10 +120,12 @@ function EditProgramInovasi() { await stateProgramInovasi.update.update(); toast.success("Program Inovasi berhasil diperbarui!"); - router.push("/admin/landing-page/profile/program-inovasi"); + router.push("/admin/landing-page/profil/program-inovasi"); } catch (error) { console.error("Error updating program inovasi:", error); toast.error("Terjadi kesalahan saat memperbarui program inovasi"); + } finally { + setIsSubmitting(false); } }; @@ -134,7 +163,7 @@ function EditProgramInovasi() { }} 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" > @@ -153,21 +182,46 @@ function EditProgramInovasi() { Seret gambar atau klik untuk memilih file - Maksimal 5MB, format gambar wajib + Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp + {/* ✅ Preview gambar + tombol X */} {previewImage && ( - + + + {/* Tombol hapus (pojok kanan atas) */} + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -199,7 +253,20 @@ function EditProgramInovasi() { onChange={(e) => setFormData({ ...formData, link: e.target.value })} /> + {/* ======= Tombol Aksi ======= */} + {/* Tombol Batal */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/landing-page/profile/program-inovasi/[id]/page.tsx b/src/app/admin/(dashboard)/landing-page/profil/program-inovasi/[id]/page.tsx similarity index 97% rename from src/app/admin/(dashboard)/landing-page/profile/program-inovasi/[id]/page.tsx rename to src/app/admin/(dashboard)/landing-page/profil/program-inovasi/[id]/page.tsx index 8ae6df07..b82d958f 100644 --- a/src/app/admin/(dashboard)/landing-page/profile/program-inovasi/[id]/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/profil/program-inovasi/[id]/page.tsx @@ -25,7 +25,7 @@ function DetailProgramInovasi() { stateProgramInovasi.delete.byId(selectedId) setModalHapus(false) setSelectedId(null) - router.push("/admin/landing-page/profile/program-inovasi") + router.push("/admin/landing-page/profil/program-inovasi") } } @@ -121,7 +121,7 @@ function DetailProgramInovasi() { + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/landing-page/profile/program-inovasi/page.tsx b/src/app/admin/(dashboard)/landing-page/profil/program-inovasi/page.tsx similarity index 98% rename from src/app/admin/(dashboard)/landing-page/profile/program-inovasi/page.tsx rename to src/app/admin/(dashboard)/landing-page/profil/program-inovasi/page.tsx index ba77436b..fd744631 100644 --- a/src/app/admin/(dashboard)/landing-page/profile/program-inovasi/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/profil/program-inovasi/page.tsx @@ -56,7 +56,7 @@ function ListProgramInovasi({ search }: { search: string }) { leftSection={} variant="light" radius="md" - onClick={() => router.push('/admin/landing-page/profile/program-inovasi/create')} + onClick={() => router.push('/admin/landing-page/profil/program-inovasi/create')} > Tambah Program @@ -109,7 +109,7 @@ function ListProgramInovasi({ search }: { search: string }) { color="blue" leftSection={} onClick={() => - router.push(`/admin/landing-page/profile/program-inovasi/${item.id}`) + router.push(`/admin/landing-page/profil/program-inovasi/${item.id}`) } > Detail diff --git a/src/app/admin/(dashboard)/landing-page/sdgs-desa/create/page.tsx b/src/app/admin/(dashboard)/landing-page/sdgs-desa/create/page.tsx deleted file mode 100644 index 474ea855..00000000 --- a/src/app/admin/(dashboard)/landing-page/sdgs-desa/create/page.tsx +++ /dev/null @@ -1,191 +0,0 @@ -/* eslint-disable react-hooks/exhaustive-deps */ -'use client' -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 { Dropzone } from '@mantine/dropzone'; -import { IconArrowBack, IconDeviceFloppy, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; -import { useRouter } from 'next/navigation'; -import { useEffect, useState } from 'react'; -import { toast } from 'react-toastify'; -import { useProxy } from 'valtio/utils'; -import sdgsDesa from '../../../_state/landing-page/sdgs-desa'; - - -function CreateSDGsDesa() { - const router = useRouter(); - const stateSDGSDesa = useProxy(sdgsDesa) - const [previewImage, setPreviewImage] = useState(null); - const [file, setFile] = useState(null); - - - useEffect(() => { - stateSDGSDesa.findMany.load(); - }, []); - - const resetForm = () => { - stateSDGSDesa.create.form = { - name: "", - jumlah: "", - imageId: "", - }; - setFile(null); - setPreviewImage(null); - }; - const handleSubmit = async () => { - if (!file) { - return toast.warn("Pilih file image 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"); - } - - stateSDGSDesa.create.form.imageId = uploaded.id; - - await stateSDGSDesa.create.create(); - - resetForm(); - router.push("/admin/landing-page/sdgs-desa") - } - return ( - - - - - Tambah Sdgs Desa - - - - - - - - Gambar Sdgs Desa - - { - const selectedFile = files[0]; - if (selectedFile) { - setFile(selectedFile); - setPreviewImage(URL.createObjectURL(selectedFile)); - } - }} - onReject={() => toast.error('File tidak valid.')} - maxSize={5 * 1024 ** 2} - accept={{ - 'image/*': ['.jpeg', '.jpg', '.png', '.gif', '.webp', '.svg'], - }} - radius="md" - > - - - - - - - - - - - -
- - Drag file ke sini atau klik untuk memilih - - - Maksimal 5MB (JPEG, JPG, PNG, GIF, WEBP, SVG) - -
-
-
- - {previewImage && ( - - - Pratinjau Gambar - - - - - - )} -
- { - stateSDGSDesa.create.form.name = e.currentTarget.value; - }} - required - /> - - - Jumlah - - } - placeholder="Masukkan jumlah" - defaultValue={stateSDGSDesa.create.form.jumlah} - onChange={(val) => { - stateSDGSDesa.create.form.jumlah = val.target.value; - }} - required - min={0} - radius="md" - /> - - - - -
-
-
- ); -} - -export default CreateSDGsDesa; diff --git a/src/app/admin/(dashboard)/lingkungan/data-lingkungan-desa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/lingkungan/data-lingkungan-desa/[id]/edit/page.tsx index ec954d40..78b5771d 100644 --- a/src/app/admin/(dashboard)/lingkungan/data-lingkungan-desa/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/data-lingkungan-desa/[id]/edit/page.tsx @@ -8,6 +8,7 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, @@ -50,6 +51,7 @@ export default function EditDataLingkunganDesa() { const stateDataLingkunganDesa = useProxy(dataLingkunganDesaState); const params = useParams(); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ name: '', @@ -58,6 +60,13 @@ export default function EditDataLingkunganDesa() { icon: '', }); + const [originalData, setOriginalData] = useState({ + name: '', + deskripsi: '', + jumlah: '', + icon: '', + }); + // Load data saat komponen mount useEffect(() => { const loadData = async () => { @@ -74,6 +83,12 @@ export default function EditDataLingkunganDesa() { jumlah: data.jumlah, icon: data.icon, }); + setOriginalData({ + name: data.name, + deskripsi: data.deskripsi, + jumlah: data.jumlah, + icon: data.icon, + }); } } catch (error) { console.error('Error loading data lingkungan desa:', error); @@ -84,8 +99,19 @@ export default function EditDataLingkunganDesa() { loadData(); }, [params?.id]); + const handleResetForm = () => { + setFormData({ + name: originalData.name, + deskripsi: originalData.deskripsi, + jumlah: originalData.jumlah, + icon: originalData.icon, + }); + toast.info('Form dikembalikan ke data awal'); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); // Update global state hanya saat submit stateDataLingkunganDesa.update.form = { ...formData, @@ -100,6 +126,8 @@ export default function EditDataLingkunganDesa() { } catch (error) { console.error('Error updating data lingkungan desa:', error); toast.error('Terjadi kesalahan saat memperbarui data lingkungan desa'); + } finally { + setIsSubmitting(false); } }; @@ -168,6 +196,17 @@ export default function EditDataLingkunganDesa() { + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/lingkungan/data-lingkungan-desa/create/page.tsx b/src/app/admin/(dashboard)/lingkungan/data-lingkungan-desa/create/page.tsx index 4fb19016..ad1f00dc 100644 --- a/src/app/admin/(dashboard)/lingkungan/data-lingkungan-desa/create/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/data-lingkungan-desa/create/page.tsx @@ -4,6 +4,7 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, @@ -16,10 +17,13 @@ import { useProxy } from 'valtio/utils'; import CreateEditor from '../../../_com/createEditor'; import SelectIconProgram from '../../../_com/selectIcon'; import dataLingkunganDesaState from '../../../_state/lingkungan/data-lingkungan-desa'; +import { useState } from 'react'; +import { toast } from 'react-toastify'; function CreateDataLingkunganDesa() { const stateCreate = useProxy(dataLingkunganDesaState); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { stateCreate.create.form = { @@ -31,9 +35,17 @@ function CreateDataLingkunganDesa() { }; const handleSubmit = async () => { - await stateCreate.create.create(); - resetForm(); - router.push('/admin/lingkungan/data-lingkungan-desa'); + try { + setIsSubmitting(true); + await stateCreate.create.create(); + resetForm(); + router.push('/admin/lingkungan/data-lingkungan-desa'); + } catch (error) { + console.error("Error creating data lingkungan desa:", error); + toast.error("Terjadi kesalahan saat menambahkan data lingkungan desa"); + } finally { + setIsSubmitting(false); + } }; return ( @@ -66,7 +78,7 @@ function CreateDataLingkunganDesa() { Nama Data Lingkungan Desa} placeholder="Masukkan nama data lingkungan desa" - defaultValue={stateCreate.create.form.name || ''} + value={stateCreate.create.form.name || ''} onChange={(val) => (stateCreate.create.form.name = val.target.value)} required /> @@ -83,7 +95,7 @@ function CreateDataLingkunganDesa() { Jumlah Data Lingkungan Desa} placeholder="Masukkan jumlah data lingkungan desa" - defaultValue={stateCreate.create.form.jumlah || ''} + value={stateCreate.create.form.jumlah || ''} onChange={(e) => (stateCreate.create.form.jumlah = e.currentTarget.value)} required /> @@ -102,6 +114,17 @@ function CreateDataLingkunganDesa() { {/* Submit Button */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/contoh-kegiatan-desa-darmasaba/edit/page.tsx b/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/contoh-kegiatan-desa-darmasaba/edit/page.tsx index 78447cc6..ef782f70 100644 --- a/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/contoh-kegiatan-desa-darmasaba/edit/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/contoh-kegiatan-desa-darmasaba/edit/page.tsx @@ -2,12 +2,13 @@ import stateEdukasiLingkungan from '@/app/admin/(dashboard)/_state/lingkungan/edukasi-lingkungan'; import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Stack, Text, Title } from '@mantine/core'; +import { Box, Button, Group, Loader, Paper, Stack, Text, Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack } from '@tabler/icons-react'; import dynamic from 'next/dynamic'; import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; const EdukasiLingkunganTextEditor = dynamic( @@ -24,12 +25,19 @@ export default function EditContohKegiatanDesaDarmasaba() { stateEdukasiLingkungan.stateContohEdukasiLingkungan ); + const [isSubmitting, setIsSubmitting] = useState(false); + // state lokal untuk form const [formData, setFormData] = useState({ judul: '', deskripsi: '', }); + const [originalData, setOriginalData] = useState({ + judul: '', + deskripsi: '', + }); + // load data awal useShallowEffect(() => { if (!contohEdukasiState.findById.data) { @@ -44,6 +52,10 @@ export default function EditContohKegiatanDesaDarmasaba() { judul: contohEdukasiState.findById.data.judul ?? '', deskripsi: contohEdukasiState.findById.data.deskripsi ?? '', }); + setOriginalData({ + judul: contohEdukasiState.findById.data.judul ?? '', + deskripsi: contohEdukasiState.findById.data.deskripsi ?? '', + }); } }, [contohEdukasiState.findById.data]); @@ -52,19 +64,35 @@ export default function EditContohKegiatanDesaDarmasaba() { setFormData((prev) => ({ ...prev, [field]: value })); }; + const handleResetForm = () => { + setFormData({ + judul: originalData.judul, + deskripsi: originalData.deskripsi, + }); + toast.info("Form dikembalikan ke data awal"); + }; + // submit update const handleSubmit = () => { - if (contohEdukasiState.findById.data) { - const updatedData = { - ...contohEdukasiState.findById.data, - judul: formData.judul, - deskripsi: formData.deskripsi, - }; - contohEdukasiState.update.save(updatedData); + try { + setIsSubmitting(true); + if (contohEdukasiState.findById.data) { + const updatedData = { + ...contohEdukasiState.findById.data, + judul: formData.judul, + deskripsi: formData.deskripsi, + }; + contohEdukasiState.update.save(updatedData); + } + router.push( + '/admin/lingkungan/edukasi-lingkungan/contoh-kegiatan-desa-darmasaba' + ); + } catch (error) { + console.error("Error updating contoh kegiatan desa darmasaba:", error); + toast.error("Terjadi kesalahan saat memperbarui contoh kegiatan desa darmasaba"); + } finally { + setIsSubmitting(false); } - router.push( - '/admin/lingkungan/edukasi-lingkungan/contoh-kegiatan-desa-darmasaba' - ); }; return ( @@ -111,7 +139,19 @@ export default function EditContohKegiatanDesaDarmasaba() { /> - + + {/* Tombol Batal */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan/edit/page.tsx b/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan/edit/page.tsx index 6f363765..0eb0c956 100644 --- a/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan/edit/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan/edit/page.tsx @@ -1,12 +1,13 @@ 'use client' import stateEdukasiLingkungan from '@/app/admin/(dashboard)/_state/lingkungan/edukasi-lingkungan'; import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Stack, Text, Title } from '@mantine/core'; +import { Box, Button, Group, Loader, Paper, Stack, Text, Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack } from '@tabler/icons-react'; import dynamic from 'next/dynamic'; import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; const EdukasiLingkunganTextEditor = dynamic( @@ -17,9 +18,14 @@ const EdukasiLingkunganTextEditor = dynamic( export default function EditMateriEdukasiYangDiberikan() { const router = useRouter(); const materiEdukasiState = useProxy(stateEdukasiLingkungan.stateMateriEdukasiLingkungan); + const [isSubmitting, setIsSubmitting] = useState(false); // State lokal gabungan untuk form const [formData, setFormData] = useState({ judul: '', content: '' }); + const [originalData, setOriginalData] = useState({ + judul: '', + content: '', + }); // Initialize data kalau belum ada useShallowEffect(() => { @@ -35,6 +41,10 @@ export default function EditMateriEdukasiYangDiberikan() { judul: materiEdukasiState.findById.data.judul ?? '', content: materiEdukasiState.findById.data.deskripsi ?? '', }); + setOriginalData({ + judul: materiEdukasiState.findById.data.judul ?? '', + content: materiEdukasiState.findById.data.deskripsi ?? '', + }); } }, [materiEdukasiState.findById.data]); @@ -42,17 +52,32 @@ export default function EditMateriEdukasiYangDiberikan() { const handleChange = (field: 'judul' | 'content', value: string) => { setFormData(prev => ({ ...prev, [field]: value })); }; + const handleResetForm = () => { + setFormData({ + judul: originalData.judul, + content: originalData.content, + }); + toast.info("Form dikembalikan ke data awal"); + }; const submit = () => { - if (materiEdukasiState.findById.data) { - const updatedData = { - ...materiEdukasiState.findById.data, - judul: formData.judul, - deskripsi: formData.content, - }; - materiEdukasiState.update.save(updatedData); + try { + setIsSubmitting(true); + if (materiEdukasiState.findById.data) { + const updatedData = { + ...materiEdukasiState.findById.data, + judul: formData.judul, + deskripsi: formData.content, + }; + materiEdukasiState.update.save(updatedData); + } + router.push('/admin/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan'); + } catch (error) { + console.error("Error updating materi edukasi yang diberikan:", error); + toast.error("Terjadi kesalahan saat memperbarui materi edukasi yang diberikan"); + } finally { + setIsSubmitting(false); } - router.push('/admin/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan'); }; return ( @@ -97,7 +122,19 @@ export default function EditMateriEdukasiYangDiberikan() { /> - + + {/* Tombol Batal */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan/edit/page.tsx b/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan/edit/page.tsx index 0c60469d..527358ea 100644 --- a/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan/edit/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan/edit/page.tsx @@ -2,12 +2,13 @@ import stateEdukasiLingkungan from '@/app/admin/(dashboard)/_state/lingkungan/edukasi-lingkungan'; import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Stack, Text, Title } from '@mantine/core'; +import { Box, Button, Group, Loader, Paper, Stack, Text, Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack } from '@tabler/icons-react'; import dynamic from 'next/dynamic'; import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; const EdukasiLingkunganTextEditor = dynamic( @@ -18,9 +19,14 @@ const EdukasiLingkunganTextEditor = dynamic( export default function EditTujuanEdukasiLingkungan() { const router = useRouter(); const tujuanEdukasiState = useProxy(stateEdukasiLingkungan.stateTujuanEdukasi); + const [isSubmitting, setIsSubmitting] = useState(false); // State lokal untuk form, gak tergantung global state const [formData, setFormData] = useState({ judul: '', deskripsi: '' }); + const [originalData, setOriginalData] = useState({ + judul: '', + deskripsi: '', + }); // Initialize global state useShallowEffect(() => { @@ -36,25 +42,45 @@ export default function EditTujuanEdukasiLingkungan() { judul: tujuanEdukasiState.findById.data.judul ?? '', deskripsi: tujuanEdukasiState.findById.data.deskripsi ?? '', }); + setOriginalData({ + judul: tujuanEdukasiState.findById.data.judul ?? '', + deskripsi: tujuanEdukasiState.findById.data.deskripsi ?? '', + }); } }, [tujuanEdukasiState.findById.data]); + const handleResetForm = () => { + setFormData({ + judul: originalData.judul, + deskripsi: originalData.deskripsi, + }); + toast.info("Form dikembalikan ke data awal"); + }; + // Handler input const handleChange = (field: 'judul' | 'deskripsi', value: string) => { setFormData(prev => ({ ...prev, [field]: value })); }; const submit = () => { - if (tujuanEdukasiState.findById.data) { - // Update global state hanya saat submit - const updatedData = { - ...tujuanEdukasiState.findById.data, - judul: formData.judul, - deskripsi: formData.deskripsi, - }; - tujuanEdukasiState.update.save(updatedData); + try { + setIsSubmitting(true); + if (tujuanEdukasiState.findById.data) { + // Update global state hanya saat submit + const updatedData = { + ...tujuanEdukasiState.findById.data, + judul: formData.judul, + deskripsi: formData.deskripsi, + }; + tujuanEdukasiState.update.save(updatedData); + } + router.push('/admin/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan'); + } catch (error) { + console.error(error); + toast.error('Gagal memuat data tujuan edukasi lingkungan'); + } finally { + setIsSubmitting(false); } - router.push('/admin/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan'); }; return ( @@ -104,7 +130,19 @@ export default function EditTujuanEdukasiLingkungan() { /> - + + {/* Tombol Batal */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/lingkungan/gotong-royong/kategori-kegiatan/[id]/page.tsx b/src/app/admin/(dashboard)/lingkungan/gotong-royong/kategori-kegiatan/[id]/page.tsx index e5b6d2f6..5dcce580 100644 --- a/src/app/admin/(dashboard)/lingkungan/gotong-royong/kategori-kegiatan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/gotong-royong/kategori-kegiatan/[id]/page.tsx @@ -3,7 +3,7 @@ import gotongRoyongState from '@/app/admin/(dashboard)/_state/lingkungan/gotong-royong'; import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } 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'; @@ -15,8 +15,10 @@ function EditKategoriKegiatan() { const params = useParams(); const id = params?.id as string; const stateKategori = useProxy(gotongRoyongState.kategoriKegiatan); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ nama: '' }); + const [originalData, setOriginalData] = useState({ nama: '' }); const [loading, setLoading] = useState(true); // Load data once @@ -29,6 +31,7 @@ function EditKategoriKegiatan() { if (data) { stateKategori.edit.id = id; setFormData({ nama: data.nama || '' }); + setOriginalData({ nama: data.nama || '' }); } } catch (err) { console.error('Error loading kategori kegiatan:', err); @@ -53,6 +56,7 @@ function EditKategoriKegiatan() { } try { + setIsSubmitting(true); stateKategori.edit.form = { nama: trimmedNama }; if (!stateKategori.edit.id) stateKategori.edit.id = id; @@ -64,9 +68,18 @@ function EditKategoriKegiatan() { } catch (err) { console.error('Error updating kategori kegiatan:', err); toast.error('Gagal memperbarui kategori kegiatan'); + } finally { + setIsSubmitting(false); } }; + const handleResetForm = () => { + setFormData({ + nama: originalData.nama, + }); + toast.info('Form dikembalikan ke data awal'); + }; + if (loading) return Loading...; return ( @@ -98,6 +111,17 @@ function EditKategoriKegiatan() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/lingkungan/gotong-royong/kategori-kegiatan/create/page.tsx b/src/app/admin/(dashboard)/lingkungan/gotong-royong/kategori-kegiatan/create/page.tsx index d74cf703..b6b937b6 100644 --- a/src/app/admin/(dashboard)/lingkungan/gotong-royong/kategori-kegiatan/create/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/gotong-royong/kategori-kegiatan/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, Text, TextInput, Title } 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 { useProxy } from 'valtio/utils'; import gotongRoyongState from '../../../../_state/lingkungan/gotong-royong'; +import { toast } from 'react-toastify'; function CreateKategoriKegiatan() { const router = useRouter(); const stateKategori = useProxy(gotongRoyongState.kategoriKegiatan) + const [isSubmitting, setIsSubmitting] = useState(false); useEffect(() => { stateKategori.findMany.load(); @@ -23,9 +25,17 @@ function CreateKategoriKegiatan() { } const handleSubmit = async () => { - await stateKategori.create.create(); - resetForm(); - router.push("/admin/lingkungan/gotong-royong/kategori-kegiatan") + try { + setIsSubmitting(true); + await stateKategori.create.create(); + resetForm(); + router.push("/admin/lingkungan/gotong-royong/kategori-kegiatan") + } catch (error) { + console.error('Error creating kategori kegiatan:', error); + toast.error('Terjadi kesalahan saat menambahkan kategori kegiatan'); + } finally { + setIsSubmitting(false); + } } return ( @@ -51,7 +61,7 @@ function CreateKategoriKegiatan() { > (stateKategori.create.form.nama = val.target.value)} label={Nama Kategori Kegiatan} placeholder="Masukkan nama kategori kegiatan" @@ -59,6 +69,17 @@ function CreateKategoriKegiatan() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/lingkungan/gotong-royong/kegiatan-desa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/lingkungan/gotong-royong/kegiatan-desa/[id]/edit/page.tsx index f46191cc..2aad0154 100644 --- a/src/app/admin/(dashboard)/lingkungan/gotong-royong/kegiatan-desa/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/gotong-royong/kegiatan-desa/[id]/edit/page.tsx @@ -5,10 +5,12 @@ import gotongRoyongState from '@/app/admin/(dashboard)/_state/lingkungan/gotong- import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Select, Stack, @@ -38,6 +40,7 @@ export default function EditKegiatanDesa() { const kegiatanDesaState = useProxy(gotongRoyongState.kegiatanDesa); const params = useParams(); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ judul: '', @@ -49,6 +52,18 @@ export default function EditKegiatanDesa() { imageId: '', kategoriKegiatanId: '', }); + const [originalData, setOriginalData] = useState({ + judul: '', + deskripsiSingkat: '', + deskripsiLengkap: '', + tanggal: '', + lokasi: '', + partisipan: 0, + imageId: '', + imageUrl: '', + kategoriKegiatanId: '', + }); + const [file, setFile] = useState(null); const [previewImage, setPreviewImage] = useState(null); @@ -77,10 +92,18 @@ export default function EditKegiatanDesa() { imageId: data.imageId || '', kategoriKegiatanId: data.kategoriKegiatanId || '', }); - if (data.imageId) { - // Optional: bisa fetch URL image dari backend - setPreviewImage(`/api/file/${data.imageId}`); - } + setOriginalData({ + judul: data.judul || '', + deskripsiSingkat: data.deskripsiSingkat || '', + deskripsiLengkap: data.deskripsiLengkap || '', + tanggal: data.tanggal || '', + lokasi: data.lokasi || '', + partisipan: data.partisipan || 0, + imageId: data.imageId || '', + kategoriKegiatanId: data.kategoriKegiatanId || '', + imageUrl: data.image?.link || "", + }); + setPreviewImage(data.image?.link || null); } } catch (error) { console.error(error); @@ -90,8 +113,24 @@ export default function EditKegiatanDesa() { loadData(); }, [params?.id]); + const handleResetForm = () => { + setFormData({ + judul: originalData.judul, + deskripsiSingkat: originalData.deskripsiSingkat, + deskripsiLengkap: originalData.deskripsiLengkap, + tanggal: originalData.tanggal, + lokasi: originalData.lokasi, + partisipan: originalData.partisipan, + imageId: originalData.imageId, + kategoriKegiatanId: originalData.kategoriKegiatanId, + }); + setPreviewImage(originalData.imageUrl || null); + setFile(null); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); let imageId = formData.imageId; if (file) { const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name }); @@ -117,6 +156,8 @@ export default function EditKegiatanDesa() { } catch (error) { console.error(error); toast.error('Terjadi kesalahan saat memperbarui kegiatan desa'); + } finally { + setIsSubmitting(false); } }; @@ -203,7 +244,7 @@ export default function EditKegiatanDesa() { }} onReject={() => toast.error('File tidak valid.')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} radius="md" p="xl" > @@ -219,13 +260,13 @@ export default function EditKegiatanDesa() { Drag gambar atau klik untuk pilih 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)', + }} + > + + )} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/lingkungan/gotong-royong/kegiatan-desa/create/page.tsx b/src/app/admin/(dashboard)/lingkungan/gotong-royong/kegiatan-desa/create/page.tsx index 2644fb76..a9605a38 100644 --- a/src/app/admin/(dashboard)/lingkungan/gotong-royong/kegiatan-desa/create/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/gotong-royong/kegiatan-desa/create/page.tsx @@ -5,10 +5,12 @@ import gotongRoyongState from '@/app/admin/(dashboard)/_state/lingkungan/gotong- import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Select, Stack, @@ -34,6 +36,8 @@ function CreateKegiatanDesa() { gotongRoyongState.kategoriKegiatan.findMany.load(); }, []); + const [isSubmitting, setIsSubmitting] = useState(false); + const resetForm = () => { stateKegiatanDesa.create.form = { judul: '', @@ -50,27 +54,35 @@ function CreateKegiatanDesa() { }; 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'); + } + + stateKegiatanDesa.create.form.imageId = uploaded.id; + + await stateKegiatanDesa.create.create(); + + resetForm(); + router.push('/admin/lingkungan/gotong-royong/kegiatan-desa'); + } catch (error) { + console.error('Error creating kegiatan desa:', error); + toast.error('Gagal membuat kegiatan desa'); + } 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'); - } - - stateKegiatanDesa.create.form.imageId = uploaded.id; - - await stateKegiatanDesa.create.create(); - - resetForm(); - router.push('/admin/lingkungan/gotong-royong/kegiatan-desa'); }; return ( @@ -115,7 +127,7 @@ function CreateKegiatanDesa() { }} 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" > @@ -136,7 +148,7 @@ function CreateKegiatanDesa() { {previewImage && ( - + + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -152,7 +182,7 @@ function CreateKegiatanDesa() { (stateKegiatanDesa.create.form.judul = e.target.value)} required /> @@ -168,7 +198,7 @@ function CreateKegiatanDesa() { { const value = Number(e.target.value); if (value >= 0) { @@ -183,7 +213,7 @@ function CreateKegiatanDesa() { label="Tanggal" type="date" placeholder="Contoh: 2022-01-01" - defaultValue={ + value={ stateKegiatanDesa.create.form.tanggal ? stateKegiatanDesa.create.form.tanggal.toISOString().split('T')[0] : '' @@ -197,7 +227,7 @@ function CreateKegiatanDesa() { (stateKegiatanDesa.create.form.lokasi = e.target.value)} required /> @@ -228,6 +258,17 @@ function CreateKegiatanDesa() { {/* Submit */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/bentuk-konservasi-berdasarkan-adat/edit/page.tsx b/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/bentuk-konservasi-berdasarkan-adat/edit/page.tsx index aca07a0c..e6694a8c 100644 --- a/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/bentuk-konservasi-berdasarkan-adat/edit/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/bentuk-konservasi-berdasarkan-adat/edit/page.tsx @@ -1,12 +1,13 @@ 'use client' import stateKonservasiAdatBali from '@/app/admin/(dashboard)/_state/lingkungan/konservasi-adat-bali'; import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Stack, Text, Title } from '@mantine/core'; +import { Box, Button, Group, Loader, Paper, Stack, Text, Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack } from '@tabler/icons-react'; import dynamic from 'next/dynamic'; import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; const KonservasiAdatBaliTextEditor = dynamic( @@ -17,9 +18,14 @@ const KonservasiAdatBaliTextEditor = dynamic( function EditBentukKonservasiBerdasarkanAdat() { const router = useRouter(); const bentukKonservasiState = useProxy(stateKonservasiAdatBali.stateBentukKonservasiBerdasarkanAdat); + const [isSubmitting, setIsSubmitting] = useState(false); // Gabung semua field form jadi satu object const [formData, setFormData] = useState({ judul: '', deskripsi: '' }); + const [originalData, setOriginalData] = useState({ + judul: '', + deskripsi: '', + }); // Initialize data dari global state useShallowEffect(() => { @@ -34,6 +40,10 @@ function EditBentukKonservasiBerdasarkanAdat() { judul: bentukKonservasiState.findById.data.judul ?? '', deskripsi: bentukKonservasiState.findById.data.deskripsi ?? '', }); + setOriginalData({ + judul: bentukKonservasiState.findById.data.judul ?? '', + deskripsi: bentukKonservasiState.findById.data.deskripsi ?? '', + }); } }, [bentukKonservasiState.findById.data]); @@ -41,13 +51,29 @@ function EditBentukKonservasiBerdasarkanAdat() { setFormData(prev => ({ ...prev, [field]: value })); }; - const submit = () => { - if (bentukKonservasiState.findById.data) { - // Update global state cuma pas submit - const updatedData = { ...bentukKonservasiState.findById.data, ...formData }; - bentukKonservasiState.update.save(updatedData); + const handleResetForm = () => { + setFormData({ + judul: originalData.judul, + deskripsi: originalData.deskripsi, + }); + toast.info("Form dikembalikan ke data awal"); + }; + + const handleSubmit = () => { + try { + setIsSubmitting(true); + if (bentukKonservasiState.findById.data) { + // Update global state cuma pas submit + const updatedData = { ...bentukKonservasiState.findById.data, ...formData }; + bentukKonservasiState.update.save(updatedData); + } + router.push('/admin/lingkungan/konservasi-adat-bali/bentuk-konservasi-berdasarkan-adat'); + } catch (error) { + console.error("Error updating bentuk konservasi berdasarkan adat:", error); + toast.error("Terjadi kesalahan saat memperbarui bentuk konservasi berdasarkan adat"); + } finally { + setIsSubmitting(false); } - router.push('/admin/lingkungan/konservasi-adat-bali/bentuk-konservasi-berdasarkan-adat'); }; return ( @@ -94,9 +120,21 @@ function EditBentukKonservasiBerdasarkanAdat() { /> - + + {/* Tombol Batal */} + + {/* Tombol Simpan */} + diff --git a/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana/edit/page.tsx b/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana/edit/page.tsx index 3ed20d1e..14a4e88c 100644 --- a/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana/edit/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana/edit/page.tsx @@ -2,12 +2,13 @@ import stateKonservasiAdatBali from '@/app/admin/(dashboard)/_state/lingkungan/konservasi-adat-bali'; import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Stack, Text, Title } from '@mantine/core'; +import { Box, Button, Group, Loader, Paper, Stack, Text, Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack } from '@tabler/icons-react'; import dynamic from 'next/dynamic'; import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; const KonservasiAdatBaliTextEditor = dynamic( @@ -21,9 +22,14 @@ const KonservasiAdatBaliTextEditor = dynamic( function EditFilosofiTriHitaKarana() { const router = useRouter(); const filosofiTriHitaState = useProxy(stateKonservasiAdatBali.stateFilosofiTriHita); + const [isSubmitting, setIsSubmitting] = useState(false); // Local state form const [formData, setFormData] = useState({ judul: '', content: '' }); + const [originalData, setOriginalData] = useState({ + judul: '', + content: '', + }); // Load data dari global state kalau belum ada useShallowEffect(() => { @@ -39,6 +45,10 @@ function EditFilosofiTriHitaKarana() { judul: filosofiTriHitaState.findById.data.judul ?? '', content: filosofiTriHitaState.findById.data.deskripsi ?? '', }); + setOriginalData({ + judul: filosofiTriHitaState.findById.data.judul ?? '', + content: filosofiTriHitaState.findById.data.deskripsi ?? '', + }); } }, [filosofiTriHitaState.findById.data]); @@ -46,13 +56,29 @@ function EditFilosofiTriHitaKarana() { setFormData((prev) => ({ ...prev, [field]: value })); }; + const handleResetForm = () => { + setFormData({ + judul: originalData.judul, + content: originalData.content, + }); + toast.info("Form dikembalikan ke data awal"); + }; + const handleSubmit = () => { - if (filosofiTriHitaState.findById.data) { - filosofiTriHitaState.findById.data.judul = formData.judul; - filosofiTriHitaState.findById.data.deskripsi = formData.content; - filosofiTriHitaState.update.save(filosofiTriHitaState.findById.data); + try { + setIsSubmitting(true); + if (filosofiTriHitaState.findById.data) { + filosofiTriHitaState.findById.data.judul = formData.judul; + filosofiTriHitaState.findById.data.deskripsi = formData.content; + filosofiTriHitaState.update.save(filosofiTriHitaState.findById.data); + } + router.push('/admin/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana'); + } catch (error) { + console.error("Error updating filosofi tri hita karana:", error); + toast.error("Terjadi kesalahan saat memperbarui filosofi tri hita karana"); + } finally { + setIsSubmitting(false); } - router.push('/admin/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana'); }; return ( @@ -99,7 +125,19 @@ function EditFilosofiTriHitaKarana() { /> - + + {/* Tombol Batal */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/nilai-konservasi-adat/edit/page.tsx b/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/nilai-konservasi-adat/edit/page.tsx index 1df50f0c..ed32c9fa 100644 --- a/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/nilai-konservasi-adat/edit/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/nilai-konservasi-adat/edit/page.tsx @@ -1,12 +1,13 @@ 'use client' import stateKonservasiAdatBali from '@/app/admin/(dashboard)/_state/lingkungan/konservasi-adat-bali'; import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Stack, Text, Title } from '@mantine/core'; +import { Box, Button, Group, Loader, Paper, Stack, Text, Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack } from '@tabler/icons-react'; import dynamic from 'next/dynamic'; import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; const KonservasiAdatBaliTextEditor = dynamic( @@ -17,9 +18,11 @@ const KonservasiAdatBaliTextEditor = dynamic( function EditNilaiKonservasiAdat() { const router = useRouter(); const nilaiKonservasiState = useProxy(stateKonservasiAdatBali.stateNilaiKonservasiAdat); + const [isSubmitting, setIsSubmitting] = useState(false); // state lokal untuk form const [formData, setFormData] = useState({ judul: '', deskripsi: '' }); + const [originalData, setOriginalData] = useState({ judul: '', deskripsi: '' }); // load data awal useShallowEffect(() => { @@ -35,6 +38,10 @@ function EditNilaiKonservasiAdat() { judul: nilaiKonservasiState.findById.data.judul ?? '', deskripsi: nilaiKonservasiState.findById.data.deskripsi ?? '', }); + setOriginalData({ + judul: nilaiKonservasiState.findById.data.judul ?? '', + deskripsi: nilaiKonservasiState.findById.data.deskripsi ?? '', + }); } }, [nilaiKonservasiState.findById.data]); @@ -42,14 +49,30 @@ function EditNilaiKonservasiAdat() { setFormData(prev => ({ ...prev, [field]: value })); }; - const submit = () => { - if (nilaiKonservasiState.findById.data) { - // update global state saat submit - nilaiKonservasiState.findById.data.judul = formData.judul; - nilaiKonservasiState.findById.data.deskripsi = formData.deskripsi; - nilaiKonservasiState.update.save(nilaiKonservasiState.findById.data); + const handleResetForm = () => { + setFormData({ + judul: originalData.judul, + deskripsi: originalData.deskripsi, + }); + toast.info("Form dikembalikan ke data awal"); + }; + + const handleSubmit = () => { + try { + setIsSubmitting(true); + if (nilaiKonservasiState.findById.data) { + // update global state saat submit + nilaiKonservasiState.findById.data.judul = formData.judul; + nilaiKonservasiState.findById.data.deskripsi = formData.deskripsi; + nilaiKonservasiState.update.save(nilaiKonservasiState.findById.data); + } + router.push('/admin/lingkungan/konservasi-adat-bali/nilai-konservasi-adat'); + } catch (error) { + console.error("Error updating nilai konservasi adat:", error); + toast.error("Terjadi kesalahan saat memperbarui nilai konservasi adat"); + } finally { + setIsSubmitting(false); } - router.push('/admin/lingkungan/konservasi-adat-bali/nilai-konservasi-adat'); }; return ( @@ -96,9 +119,21 @@ function EditNilaiKonservasiAdat() { /> - + + {/* Tombol Batal */} + + {/* Tombol Simpan */} + diff --git a/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/[id]/edit/page.tsx b/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/[id]/edit/page.tsx index 72555ecf..ef0cb555 100644 --- a/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/[id]/edit/page.tsx @@ -3,7 +3,7 @@ import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah'; import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; +import { Box, Button, Group, Loader, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import dynamic from 'next/dynamic'; import { useParams, useRouter } from 'next/navigation'; @@ -18,7 +18,7 @@ function EditKeteranganBankSampahTerdekat() { const router = useRouter(); const params = useParams(); const [markerPosition, setMarkerPosition] = useState<{ lat: number; lng: number } | null>(null); - + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ name: '', alamat: '', @@ -27,6 +27,14 @@ function EditKeteranganBankSampahTerdekat() { lng: 0, }); + const [originalData, setOriginalData] = useState({ + name: '', + alamat: '', + namaTempatMaps: '', + lat: 0, + lng: 0, + }); + // Load data ketika component mount useEffect(() => { const loadKeterangan = async () => { @@ -46,6 +54,15 @@ function EditKeteranganBankSampahTerdekat() { lat: data.lat, lng: data.lng, }); + + // simpan juga versi original + setOriginalData({ + name: data.name, + alamat: data.alamat, + namaTempatMaps: data.namaTempatMaps, + lat: data.lat, + lng: data.lng, + }); setMarkerPosition({ lat: data.lat, lng: data.lng }); } } catch (error) { @@ -61,8 +78,21 @@ function EditKeteranganBankSampahTerdekat() { setFormData(prev => ({ ...prev, [field]: value })); }; + const handleResetForm = () => { + setFormData({ + name: originalData.name, + alamat: originalData.alamat, + namaTempatMaps: originalData.namaTempatMaps, + lat: originalData.lat, + lng: originalData.lng, + }); + setMarkerPosition({ lat: originalData.lat, lng: originalData.lng }); + toast.info('Form dikembalikan ke data awal'); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); if (!formData.name.trim()) return toast.error('Nama bank sampah harus diisi'); if (!formData.alamat.trim()) return toast.error('Alamat harus diisi'); if (!formData.namaTempatMaps.trim()) return toast.error('Nama tempat di Maps harus diisi'); @@ -84,6 +114,8 @@ function EditKeteranganBankSampahTerdekat() { } catch (error) { console.error(error); toast.error(error instanceof Error ? error.message : 'Gagal memperbarui data bank sampah'); + } finally { + setIsSubmitting(false); } }; @@ -150,6 +182,17 @@ function EditKeteranganBankSampahTerdekat() { + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/create/page.tsx b/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/create/page.tsx index 3bf71f3d..8b702830 100644 --- a/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/create/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/create/page.tsx @@ -2,7 +2,7 @@ import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah'; import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; +import { Box, Button, Group, Loader, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import dynamic from 'next/dynamic'; import { useRouter } from 'next/navigation'; @@ -17,6 +17,7 @@ function CreateKeteranganBankSampahTerdekat() { const router = useRouter(); const [markerPosition, setMarkerPosition] = useState<{ lat: number; lng: number } | null>(null); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { keteranganState.create.form = { @@ -30,6 +31,7 @@ function CreateKeteranganBankSampahTerdekat() { } const handleSubmit = async () => { try { + setIsSubmitting(true); if (!keteranganState.create.form.name) { return toast.error('Nama bank sampah harus diisi'); } @@ -48,6 +50,8 @@ function CreateKeteranganBankSampahTerdekat() { } catch (error) { console.error('Error creating bank sampah:', error); toast.error('Gagal menambahkan data bank sampah'); + } finally { + setIsSubmitting(false); } } @@ -74,7 +78,7 @@ function CreateKeteranganBankSampahTerdekat() { (keteranganState.create.form.name = e.target.value)} required /> @@ -82,7 +86,7 @@ function CreateKeteranganBankSampahTerdekat() { (keteranganState.create.form.alamat = e.target.value)} required /> @@ -90,7 +94,7 @@ function CreateKeteranganBankSampahTerdekat() { (keteranganState.create.form.namaTempatMaps = e.target.value)} required /> @@ -116,6 +120,17 @@ function CreateKeteranganBankSampahTerdekat() { + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/[id]/page.tsx b/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/[id]/page.tsx index f07cfb6d..c1c20352 100644 --- a/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/[id]/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/[id]/page.tsx @@ -3,7 +3,7 @@ import SelectIconProgramEdit from '@/app/admin/(dashboard)/_com/selectIconEdit'; import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah'; import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } 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'; @@ -21,6 +21,7 @@ function EditProgramKreatifDesa() { const stateSampah = useProxy(pengelolaanSampahState.pengelolaanSampah) const params = useParams() const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); // State lokal untuk form controlled const [formData, setFormData] = useState({ @@ -28,6 +29,11 @@ function EditProgramKreatifDesa() { icon: '', }); + const [originalData, setOriginalData] = useState({ + name: '', + icon: '', + }); + useEffect(() => { const loadProgramKreatif = async () => { const id = params?.id as string; @@ -41,6 +47,10 @@ function EditProgramKreatifDesa() { name: data.name, icon: data.icon, }); + setOriginalData({ + name: data.name, + icon: data.icon, + }); } } catch (error) { console.error("Error loading pengelolaan sampah:", error); @@ -51,8 +61,17 @@ function EditProgramKreatifDesa() { loadProgramKreatif(); }, [params?.id]); + const handleResetForm = () => { + setFormData({ + name: originalData.name, + icon: originalData.icon, + }); + toast.info("Form dikembalikan ke data awal"); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); // Update global state HANYA saat submit stateSampah.update.form = { name: formData.name.trim(), @@ -65,6 +84,8 @@ function EditProgramKreatifDesa() { } catch (error) { console.error("Error updating pengelolaan sampah:", error); toast.error(error instanceof Error ? error.message : "Gagal memperbarui data pengelolaan sampah"); + } finally { + setIsSubmitting(false); } }; @@ -107,6 +128,17 @@ function EditProgramKreatifDesa() { + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/create/page.tsx b/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/create/page.tsx index 365c97bd..a3b7dc61 100644 --- a/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/create/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/create/page.tsx @@ -2,17 +2,16 @@ import SelectIconProgram from '@/app/admin/(dashboard)/_com/selectIcon'; import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah'; import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } 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 { useState } from 'react'; import { useProxy } from 'valtio/utils'; - - - function CreatePengelolaanSampahBankSampah() { const stateCreate = useProxy(pengelolaanSampahState.pengelolaanSampah); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { stateCreate.create.form = { @@ -23,11 +22,14 @@ function CreatePengelolaanSampahBankSampah() { const handleSubmit = async () => { try { + setIsSubmitting(true); await stateCreate.create.create(); resetForm(); router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah"); } catch (error) { console.error('Error creating pengelolaan sampah:', error); + } finally { + setIsSubmitting(false); } }; @@ -59,7 +61,7 @@ function CreatePengelolaanSampahBankSampah() { (stateCreate.create.form.name = e.target.value)} required /> @@ -74,6 +76,17 @@ function CreatePengelolaanSampahBankSampah() { + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/lingkungan/program-penghijauan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/lingkungan/program-penghijauan/[id]/edit/page.tsx index 161aef97..3ebf74e0 100644 --- a/src/app/admin/(dashboard)/lingkungan/program-penghijauan/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/program-penghijauan/[id]/edit/page.tsx @@ -9,6 +9,7 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, @@ -47,6 +48,7 @@ function EditProgramPenghijauan() { const stateProgramPenghijauan = useProxy(programPenghijauanState); const params = useParams(); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ name: '', @@ -55,6 +57,13 @@ function EditProgramPenghijauan() { icon: '', }); + const [originalData, setOriginalData] = useState({ + name: '', + judul: '', + deskripsi: '', + icon: '', + }); + // Load data program penghijauan useEffect(() => { const loadProgram = async () => { @@ -71,6 +80,12 @@ function EditProgramPenghijauan() { deskripsi: data.deskripsi, icon: data.icon, }); + setOriginalData({ + name: data.name, + judul: data.judul, + deskripsi: data.deskripsi, + icon: data.icon, + }); } } catch (error) { console.error('Error loading program penghijauan:', error); @@ -81,9 +96,20 @@ function EditProgramPenghijauan() { loadProgram(); }, [params?.id]); + const handleResetForm = () => { + setFormData({ + name: originalData.name, + judul: originalData.judul, + deskripsi: originalData.deskripsi, + icon: originalData.icon, + }); + toast.info('Form dikembalikan ke data awal'); + }; + // Hanya update global state saat submit const handleSubmit = async () => { try { + setIsSubmitting(true); stateProgramPenghijauan.update.form = { name: formData.name.trim(), judul: formData.judul.trim(), @@ -97,6 +123,8 @@ function EditProgramPenghijauan() { } catch (error) { console.error('Error updating program penghijauan:', error); toast.error('Gagal memperbarui program penghijauan'); + } finally { + setIsSubmitting(false); } }; @@ -173,6 +201,17 @@ function EditProgramPenghijauan() { {/* Tombol simpan */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/lingkungan/program-penghijauan/[id]/page.tsx b/src/app/admin/(dashboard)/lingkungan/program-penghijauan/[id]/page.tsx index ff29a6ba..307e30af 100644 --- a/src/app/admin/(dashboard)/lingkungan/program-penghijauan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/program-penghijauan/[id]/page.tsx @@ -4,9 +4,13 @@ import colors from '@/con/colors'; import { Box, Button, Group, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { - IconArrowBack, IconChartLine, IconChristmasTreeFilled, IconClipboard, - IconEdit, IconHomeEco, IconLeaf, IconRecycle, IconScale, - IconShieldFilled, IconTent, IconTrash, IconTrendingUp, + IconAlertTriangle, + IconAmbulance, + IconArrowBack, IconBuilding, IconCash, IconChartLine, IconChristmasTreeFilled, IconClipboard, + IconDroplet, + IconEdit, IconFiretruck, IconHome, IconHomeEco, IconHospital, IconLeaf, IconRecycle, IconScale, + IconSchool, + IconShieldFilled, IconShoppingCart, IconTent, IconTrash, IconTree, IconTrendingUp, IconTrophy, IconTruck } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -24,18 +28,31 @@ function DetailProgramPenghijauan() { const iconMap: Record> = { ekowisata: IconLeaf, - kompetisi: IconTrophy, - wisata: IconTent, - ekonomi: IconChartLine, - sampah: IconRecycle, - truck: IconTruck, - scale: IconScale, - clipboard: IconClipboard, - trash: IconTrash, - lingkunganSehat: IconHomeEco, - sumberOksigen: IconChristmasTreeFilled, - ekonomiBerkelanjutan: IconTrendingUp, - mencegahBencana: IconShieldFilled, + kompetisi: IconTrophy, + wisata: IconTent, + ekonomi: IconChartLine, + sampah: IconRecycle, + truck: IconTruck, + scale: IconScale, + clipboard: IconClipboard, + trash: IconTrash, + lingkunganSehat: IconHomeEco, + sumberOksigen: IconChristmasTreeFilled, + ekonomiBerkelanjutan: IconTrendingUp, + mencegahBencana: IconShieldFilled, + rumah: IconHome, + pohon: IconTree, + air: IconDroplet, + bantuan: IconCash, + pelatihan: IconSchool, + subsidi: IconShoppingCart, + layananKesehatan: IconHospital, + polisi: IconShieldFilled, + ambulans: IconAmbulance, + pemadam: IconFiretruck, + rumahSakit: IconHospital, + bangunan: IconBuilding, + darurat: IconAlertTriangle }; useShallowEffect(() => { @@ -96,7 +113,7 @@ function DetailProgramPenghijauan() { Ikon Program - {iconMap[data?.icon] ? ( + {iconMap[data?.icon] ? ( {React.createElement(iconMap[data.icon], { size: 28, color: colors['blue-button'] })} diff --git a/src/app/admin/(dashboard)/lingkungan/program-penghijauan/create/page.tsx b/src/app/admin/(dashboard)/lingkungan/program-penghijauan/create/page.tsx index 3031d35d..e06a98d1 100644 --- a/src/app/admin/(dashboard)/lingkungan/program-penghijauan/create/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/program-penghijauan/create/page.tsx @@ -4,6 +4,7 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, @@ -16,10 +17,13 @@ import { useProxy } from 'valtio/utils'; import CreateEditor from '../../../_com/createEditor'; import SelectIconProgram from '../../../_com/selectIcon'; import programPenghijauanState from '../../../_state/lingkungan/program-penghijauan'; +import { toast } from 'react-toastify'; +import { useState } from 'react'; function CreateProgramPenghijauan() { const stateCreate = useProxy(programPenghijauanState); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { stateCreate.create.form = { @@ -31,9 +35,17 @@ function CreateProgramPenghijauan() { }; const handleSubmit = async () => { - await stateCreate.create.create(); - resetForm(); - router.push('/admin/lingkungan/program-penghijauan'); + try { + setIsSubmitting(true); + await stateCreate.create.create(); + resetForm(); + router.push('/admin/lingkungan/program-penghijauan'); + } catch (error) { + console.error(error); + toast.error(error instanceof Error ? error.message : 'Gagal menambahkan program penghijauan'); + } finally { + setIsSubmitting(false); + } }; return ( @@ -66,7 +78,7 @@ function CreateProgramPenghijauan() { Nama Program Penghijauan} placeholder="Masukkan nama program penghijauan" - defaultValue={stateCreate.create.form.name || ''} + value={stateCreate.create.form.name || ''} onChange={(e) => (stateCreate.create.form.name = e.target.value)} required /> @@ -83,7 +95,7 @@ function CreateProgramPenghijauan() { Judul Deskripsi Program Penghijauan} placeholder="Masukkan judul deskripsi program penghijauan" - defaultValue={stateCreate.create.form.judul || ''} + value={stateCreate.create.form.judul || ''} onChange={(e) => (stateCreate.create.form.judul = e.target.value)} required /> @@ -101,6 +113,17 @@ function CreateProgramPenghijauan() { + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/pendidikan/beasiswa-desa/keunggulan-program/[id]/page.tsx b/src/app/admin/(dashboard)/pendidikan/beasiswa-desa/keunggulan-program/[id]/page.tsx index 9fac6d28..18728c2c 100644 --- a/src/app/admin/(dashboard)/pendidikan/beasiswa-desa/keunggulan-program/[id]/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/beasiswa-desa/keunggulan-program/[id]/page.tsx @@ -3,7 +3,7 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import beasiswaDesaState from '@/app/admin/(dashboard)/_state/pendidikan/beasiswa-desa'; import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } 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'; @@ -14,10 +14,15 @@ function EditProgramKreatifDesa() { const state = useProxy(beasiswaDesaState.keunggulanProgram) const params = useParams() const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ judul: '', deskripsi: '', }) + const [originalData, setOriginalData] = useState({ + judul: '', + deskripsi: '', + }) useEffect(() => { const loadProgramKreatif = async () => { @@ -39,6 +44,10 @@ function EditProgramKreatifDesa() { judul: data.judul, deskripsi: data.deskripsi, }); + setOriginalData({ + judul: data.judul, + deskripsi: data.deskripsi, + }); } } catch (error) { console.error("Error loading pengelolaan sampah:", error); @@ -49,10 +58,24 @@ function EditProgramKreatifDesa() { loadProgramKreatif(); }, [params?.id]); - + const handleResetForm = () => { + const resetData = { + judul: originalData.judul, + deskripsi: originalData.deskripsi, + }; + + setFormData(resetData); + + // ⬇️ tambahkan sinkronisasi ke state global + state.update.form = { ...resetData }; + + toast.info("Form dikembalikan ke data awal"); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); state.update.form = { ...state.update.form, judul: formData.judul.trim(), @@ -65,6 +88,8 @@ function EditProgramKreatifDesa() { } catch (error) { console.error("Error updating keunggulan program:", error); toast.error(error instanceof Error ? error.message : "Gagal memperbarui data keunggulan program"); + } finally { + setIsSubmitting(false); } } return ( @@ -95,7 +120,7 @@ function EditProgramKreatifDesa() { { const value = e.target.value; setFormData(prev => ({ @@ -120,6 +145,17 @@ function EditProgramKreatifDesa() { + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/pendidikan/beasiswa-desa/keunggulan-program/create/page.tsx b/src/app/admin/(dashboard)/pendidikan/beasiswa-desa/keunggulan-program/create/page.tsx index 2206cef2..00b7bc4b 100644 --- a/src/app/admin/(dashboard)/pendidikan/beasiswa-desa/keunggulan-program/create/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/beasiswa-desa/keunggulan-program/create/page.tsx @@ -2,9 +2,10 @@ import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor'; import beasiswaDesaState from '@/app/admin/(dashboard)/_state/pendidikan/beasiswa-desa'; import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } 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 { useState } from 'react'; import { useProxy } from 'valtio/utils'; @@ -13,6 +14,7 @@ import { useProxy } from 'valtio/utils'; function CreateKeunggulanProgram() { const stateCreate = useProxy(beasiswaDesaState.keunggulanProgram); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { stateCreate.create.form = { @@ -23,11 +25,14 @@ function CreateKeunggulanProgram() { const handleSubmit = async () => { try { + setIsSubmitting(true); await stateCreate.create.create(); resetForm(); router.push("/admin/pendidikan/beasiswa-desa/keunggulan-program"); } catch (error) { console.error('Error creating keunggulan program:', error); + } finally { + setIsSubmitting(false); } }; @@ -59,7 +64,7 @@ function CreateKeunggulanProgram() { (stateCreate.create.form.judul = e.target.value)} required /> @@ -77,6 +82,17 @@ function CreateKeunggulanProgram() { + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/fasilitas-yang-disediakan/edit/page.tsx b/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/fasilitas-yang-disediakan/edit/page.tsx index 09a00b71..15beadd4 100644 --- a/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/fasilitas-yang-disediakan/edit/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/fasilitas-yang-disediakan/edit/page.tsx @@ -7,6 +7,7 @@ import { Button, Center, Group, + Loader, Paper, Stack, Text, @@ -36,6 +37,10 @@ function EditFasilitasYangDisediakan() { // === State lokal form === const [formData, setFormData] = useState({ judul: '', deskripsi: '' }); const [isSubmitting, setIsSubmitting] = useState(false); + const [originalData, setOriginalData] = useState({ + judul: '', + deskripsi: '', + }); // Load data pertama kali useShallowEffect(() => { @@ -51,6 +56,10 @@ function EditFasilitasYangDisediakan() { judul: editState.findById.data.judul ?? '', deskripsi: editState.findById.data.deskripsi ?? '' }); + setOriginalData({ + judul: editState.findById.data.judul ?? '', + deskripsi: editState.findById.data.deskripsi ?? '', + }); } }, [editState.findById.data]); @@ -58,6 +67,14 @@ function EditFasilitasYangDisediakan() { setFormData((prev) => ({ ...prev, [field]: value })); }; + const handleResetForm = () => { + setFormData({ + judul: originalData.judul, + deskripsi: originalData.deskripsi, + }); + toast.info("Form dikembalikan ke data awal"); + }; + const handleSubmit = async () => { if (!formData.judul.trim()) { toast.error('Judul wajib diisi'); @@ -146,23 +163,31 @@ function EditFasilitasYangDisediakan() { {/* Tombol Aksi */} - - - + + {/* Tombol Batal */} + + {/* Tombol Simpan */} + diff --git a/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/lokasi-dan-jadwal/edit/page.tsx b/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/lokasi-dan-jadwal/edit/page.tsx index 31ac6352..4b2a8156 100644 --- a/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/lokasi-dan-jadwal/edit/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/lokasi-dan-jadwal/edit/page.tsx @@ -7,6 +7,7 @@ import { Button, Center, Group, + Loader, Paper, Stack, Text, @@ -36,6 +37,7 @@ function EditLokasiDanJadwal() { // state lokal untuk form, tidak langsung merubah global state const [formData, setFormData] = useState({ judul: '', deskripsi: '' }); const [isSubmitting, setIsSubmitting] = useState(false); + const [originalData, setOriginalData] = useState({ judul: '', deskripsi: '' }); // Load data sekali useShallowEffect(() => { @@ -51,6 +53,10 @@ function EditLokasiDanJadwal() { judul: editState.findById.data.judul ?? '', deskripsi: editState.findById.data.deskripsi ?? '' }); + setOriginalData({ + judul: editState.findById.data.judul ?? '', + deskripsi: editState.findById.data.deskripsi ?? '' + }); } }, [editState.findById.data]); @@ -58,6 +64,14 @@ function EditLokasiDanJadwal() { setFormData((prev) => ({ ...prev, [field]: value })); }; + const handleResetForm = () => { + setFormData({ + judul: originalData.judul, + deskripsi: originalData.deskripsi, + }); + toast.info("Form dikembalikan ke data awal"); + }; + const handleSubmit = async () => { if (!formData.judul.trim()) { toast.error('Judul wajib diisi'); @@ -147,23 +161,31 @@ function EditLokasiDanJadwal() { {/* Action Button */} - - - + + {/* Tombol Batal */} + + {/* Tombol Simpan */} + diff --git a/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/tujuan-program/edit/page.tsx b/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/tujuan-program/edit/page.tsx index 3ddddbd1..be02e0d6 100644 --- a/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/tujuan-program/edit/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/tujuan-program/edit/page.tsx @@ -7,6 +7,7 @@ import { Button, Center, Group, + Loader, Paper, Stack, Text, @@ -36,6 +37,7 @@ function EditTujuanProgram() { // gabung judul & content jadi formData const [formData, setFormData] = useState({ judul: '', deskripsi: '' }); const [isSubmitting, setIsSubmitting] = useState(false); + const [originalData, setOriginalData] = useState({ judul: '', deskripsi: '' }); // load data sekali useShallowEffect(() => { @@ -49,6 +51,10 @@ function EditTujuanProgram() { judul: editState.findById.data.judul ?? '', deskripsi: editState.findById.data.deskripsi ?? '', }); + setOriginalData({ + judul: editState.findById.data.judul ?? '', + deskripsi: editState.findById.data.deskripsi ?? '', + }); } }, [editState.findById.data]); @@ -56,6 +62,14 @@ function EditTujuanProgram() { setFormData((prev) => ({ ...prev, [field]: value })); }; + const handleResetForm = () => { + setFormData({ + judul: originalData.judul, + deskripsi: originalData.deskripsi, + }); + toast.info("Form dikembalikan ke data awal"); + }; + const handleSubmit = async () => { if (!formData.judul.trim()) { toast.error('Judul wajib diisi'); @@ -139,23 +153,31 @@ function EditTujuanProgram() { {/* Submit & Cancel */} - - - + + {/* Tombol Batal */} + + {/* Tombol Simpan */} + diff --git a/src/app/admin/(dashboard)/pendidikan/data-pendidikan/[id]/page.tsx b/src/app/admin/(dashboard)/pendidikan/data-pendidikan/[id]/page.tsx index ff3d1e37..731b4f22 100644 --- a/src/app/admin/(dashboard)/pendidikan/data-pendidikan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/data-pendidikan/[id]/page.tsx @@ -2,18 +2,20 @@ 'use client' import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Stack, TextInput, Title } from '@mantine/core'; +import { Box, Button, Loader, Group, 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 { useProxy } from 'valtio/utils'; import dataPendidikan from '../../../_state/pendidikan/data-pendidikan'; +import { toast } from 'react-toastify'; export default function EditDataPendidikan() { const router = useRouter(); const params = useParams() as { id: string }; const stateDPM = useProxy(dataPendidikan); const id = params.id; + const [isSubmitting, setIsSubmitting] = useState(false); // state lokal untuk form const [formData, setFormData] = useState({ @@ -21,6 +23,11 @@ export default function EditDataPendidikan() { jumlah: '', }); + const [originalData, setOriginalData] = useState({ + name: '', + jumlah: '', + }); + // Load data saat mount useEffect(() => { if (id) { @@ -31,21 +38,41 @@ export default function EditDataPendidikan() { name: data.name || '', jumlah: data.jumlah || '', }); + setOriginalData({ + name: data.name || '', + jumlah: data.jumlah || '', + }); } }); } }, [id]); + const handleResetForm = () => { + setFormData({ + name: originalData.name, + jumlah: originalData.jumlah, + }); + toast.info("Form dikembalikan ke data awal"); + }; + const handleChange = (field: 'name' | 'jumlah', value: string) => { setFormData((prev) => ({ ...prev, [field]: value })); }; const handleSubmit = async () => { - // update global state hanya saat submit + try { + setIsSubmitting(true); + // update global state hanya saat submit stateDPM.update.id = id; stateDPM.update.form = { ...formData }; await stateDPM.update.submit(); router.push('/admin/pendidikan/data-pendidikan'); + } catch (error) { + console.error("Error updating data pendidikan:", error); + toast.error("Terjadi kesalahan saat memperbarui data pendidikan"); + } finally { + setIsSubmitting(false); + } }; return ( @@ -83,19 +110,30 @@ export default function EditDataPendidikan() { radius="md" required /> - + + {/* Tombol Batal */} + + {/* Tombol Simpan */} + diff --git a/src/app/admin/(dashboard)/pendidikan/data-pendidikan/create/page.tsx b/src/app/admin/(dashboard)/pendidikan/data-pendidikan/create/page.tsx index 6cd346a8..dcfb0a49 100644 --- a/src/app/admin/(dashboard)/pendidikan/data-pendidikan/create/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/data-pendidikan/create/page.tsx @@ -1,31 +1,41 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ 'use client' import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } 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 { useState } from 'react'; import { useProxy } from 'valtio/utils'; import dataPendidikan from '../../../_state/pendidikan/data-pendidikan'; +import { toast } from 'react-toastify'; export default function CreateDataPendidikan() { const stateDPM = useProxy(dataPendidikan); const [chartData, setChartData] = useState([]); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { stateDPM.create.form = { name: '', jumlah: '' }; }; const handleSubmit = async () => { - const id = await stateDPM.create.create(); - if (id) { - const idStr = String(id); - await stateDPM.findUnique.load(idStr); - if (stateDPM.findUnique.data) setChartData([stateDPM.findUnique.data]); + try { + setIsSubmitting(true); + const id = await stateDPM.create.create(); + if (id) { + const idStr = String(id); + await stateDPM.findUnique.load(idStr); + if (stateDPM.findUnique.data) setChartData([stateDPM.findUnique.data]); + } + resetForm(); + router.push('/admin/pendidikan/data-pendidikan'); + } catch (error) { + console.error("Error creating data pendidikan:", error); + toast.error("Terjadi kesalahan saat menambahkan data pendidikan"); + } finally { + setIsSubmitting(false); } - resetForm(); - router.push('/admin/pendidikan/data-pendidikan'); }; return ( @@ -49,7 +59,7 @@ export default function CreateDataPendidikan() { (stateDPM.create.form.name = e.currentTarget.value)} radius="md" required @@ -57,25 +67,36 @@ export default function CreateDataPendidikan() { (stateDPM.create.form.jumlah = e.currentTarget.value)} radius="md" type="number" required /> - + + {/* Tombol Batal */} + + {/* Tombol Simpan */} + diff --git a/src/app/admin/(dashboard)/pendidikan/info-sekolah/jenjang-pendidikan/[id]/page.tsx b/src/app/admin/(dashboard)/pendidikan/info-sekolah/jenjang-pendidikan/[id]/page.tsx index 5e9395c6..252eed16 100644 --- a/src/app/admin/(dashboard)/pendidikan/info-sekolah/jenjang-pendidikan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/info-sekolah/jenjang-pendidikan/[id]/page.tsx @@ -7,6 +7,7 @@ import { Box, Button, Group, + Loader, Paper, Stack, TextInput, @@ -26,6 +27,8 @@ function EditJenjangPendidikan() { const stateJenjang = useProxy(infoSekolahPaud.jenjangPendidikan); const [formData, setFormData] = useState({ nama: '' }); + const [originalData, setOriginalData] = useState({ nama: '' }); + const [isSubmitting, setIsSubmitting] = useState(false); const [loading, setLoading] = useState(true); // Load data sekali saat component mount @@ -38,6 +41,7 @@ function EditJenjangPendidikan() { if (data) { stateJenjang.edit.id = id; // tetap simpan di global state setFormData({ nama: data.nama || '' }); + setOriginalData({ nama: data.nama || '' }); } } catch (error) { console.error('Error loading jenjang pendidikan:', error); @@ -61,6 +65,7 @@ function EditJenjangPendidikan() { } try { + setIsSubmitting(true); stateJenjang.edit.form = { nama: formData.nama.trim() }; if (!stateJenjang.edit.id) stateJenjang.edit.id = id; @@ -72,9 +77,18 @@ function EditJenjangPendidikan() { } catch (error) { console.error('Error updating jenjang pendidikan:', error); toast.error('Terjadi kesalahan saat memperbarui jenjang pendidikan'); + } finally { + setIsSubmitting(false); } }; + const handleResetForm = () => { + setFormData({ + nama: originalData.nama, + }); + toast.info("Form dikembalikan ke data awal"); + }; + return ( {/* Header */} @@ -107,6 +121,17 @@ function EditJenjangPendidikan() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/pendidikan/info-sekolah/jenjang-pendidikan/create/page.tsx b/src/app/admin/(dashboard)/pendidikan/info-sekolah/jenjang-pendidikan/create/page.tsx index e66fa92f..dacee6d5 100644 --- a/src/app/admin/(dashboard)/pendidikan/info-sekolah/jenjang-pendidikan/create/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/info-sekolah/jenjang-pendidikan/create/page.tsx @@ -5,6 +5,7 @@ import { Box, Button, Group, + Loader, Paper, Stack, TextInput, @@ -12,7 +13,7 @@ import { } 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 infoSekolahPaud from '../../../../_state/pendidikan/info-sekolah-paud'; @@ -20,6 +21,7 @@ import infoSekolahPaud from '../../../../_state/pendidikan/info-sekolah-paud'; function CreateJenjangPendidikan() { const router = useRouter(); const stateJenjang = useProxy(infoSekolahPaud.jenjangPendidikan); + const [isSubmitting, setIsSubmitting] = useState(false); useEffect(() => { stateJenjang.findMany.load(); @@ -32,13 +34,20 @@ function CreateJenjangPendidikan() { }; const handleSubmit = async () => { - if (!stateJenjang.create.form.nama) { - return toast.warn('Nama jenjang pendidikan tidak boleh kosong'); - } + try { + if (!stateJenjang.create.form.nama) { + return toast.warn('Nama jenjang pendidikan tidak boleh kosong'); + } - await stateJenjang.create.create(); - resetForm(); - router.push('/admin/pendidikan/info-sekolah/jenjang-pendidikan'); + setIsSubmitting(true); + await stateJenjang.create.create(); + resetForm(); + router.push('/admin/pendidikan/info-sekolah/jenjang-pendidikan'); + } catch (error) { + console.error(error); + } finally { + setIsSubmitting(false); + } }; return ( @@ -71,12 +80,23 @@ function CreateJenjangPendidikan() { (stateJenjang.create.form.nama = e.target.value)} required /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/pendidikan/info-sekolah/lembaga/[id]/edit/page.tsx b/src/app/admin/(dashboard)/pendidikan/info-sekolah/lembaga/[id]/edit/page.tsx index d5b322a1..3b5107f8 100644 --- a/src/app/admin/(dashboard)/pendidikan/info-sekolah/lembaga/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/info-sekolah/lembaga/[id]/edit/page.tsx @@ -7,6 +7,7 @@ import { Box, Button, Group, + Loader, Paper, Select, Stack, @@ -24,12 +25,18 @@ export default function EditLembaga() { const { id } = useParams<{ id: string }>(); const state = useProxy(infoSekolahPaud.lembagaPendidikan); const jenjangPendidikanList = infoSekolahPaud.jenjangPendidikan.findMany.data; + const [isSubmitting, setIsSubmitting] = useState(false); const [form, setForm] = useState({ nama: '', jenjangId: '', }); + const [originalData, setOriginalData] = useState({ + nama: '', + jenjangId: '', + }); + // Load jenjang pendidikan dan data lembaga useEffect(() => { infoSekolahPaud.jenjangPendidikan.findMany.load(); @@ -41,6 +48,10 @@ export default function EditLembaga() { nama: data.nama, jenjangId: data.jenjangId, }); + setOriginalData({ + nama: data.nama, + jenjangId: data.jenjangId, + }); } }); } @@ -50,21 +61,37 @@ export default function EditLembaga() { setForm((prev) => ({ ...prev, [field]: value })); }; + const handleResetForm = () => { + setForm({ + nama: originalData.nama, + jenjangId: originalData.jenjangId, + }); + toast.info('Form dikembalikan ke data awal'); + }; + const handleSubmit = async () => { - if (!form.nama || !form.jenjangId) { - toast.warn('Nama dan jenjang pendidikan harus diisi'); - return; - } + try { + setIsSubmitting(true); + if (!form.nama || !form.jenjangId) { + toast.warn('Nama dan jenjang pendidikan harus diisi'); + return; + } - // Update global state hanya saat submit - state.edit.id = id; - state.edit.form = form; + // Update global state hanya saat submit + state.edit.id = id; + state.edit.form = form; - const result = await state.edit.update(); + const result = await state.edit.update(); - if (result) { - toast.success('Data berhasil diperbarui'); - router.push('/admin/pendidikan/info-sekolah/lembaga'); + if (result) { + toast.success('Data berhasil diperbarui'); + router.push('/admin/pendidikan/info-sekolah/lembaga'); + } + } catch (error) { + console.error('Error updating lembaga:', error); + toast.error('Gagal memperbarui lembaga'); + } finally { + setIsSubmitting(false); } }; @@ -119,6 +146,17 @@ export default function EditLembaga() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/pendidikan/info-sekolah/lembaga/create/page.tsx b/src/app/admin/(dashboard)/pendidikan/info-sekolah/lembaga/create/page.tsx index bfbd79fc..447e6938 100644 --- a/src/app/admin/(dashboard)/pendidikan/info-sekolah/lembaga/create/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/info-sekolah/lembaga/create/page.tsx @@ -5,6 +5,7 @@ import { Box, Button, Group, + Loader, Paper, Select, Stack, @@ -14,13 +15,15 @@ import { } 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 infoSekolahPaud from '../../../../_state/pendidikan/info-sekolah-paud'; +import { toast } from 'react-toastify'; function CreateLembaga() { const router = useRouter(); const stateLembaga = useProxy(infoSekolahPaud.lembagaPendidikan); + const [isSubmitting, setIsSubmitting] = useState(false); useEffect(() => { stateLembaga.findMany.load(); @@ -35,9 +38,17 @@ function CreateLembaga() { }; const handleSubmit = async () => { - await stateLembaga.create.create(); - resetForm(); - router.push('/admin/pendidikan/info-sekolah/lembaga'); + try { + setIsSubmitting(true); + await stateLembaga.create.create(); + resetForm(); + router.push('/admin/pendidikan/info-sekolah/lembaga'); + } catch (error) { + console.error('Error creating lembaga:', error); + toast.error('Gagal menambahkan lembaga'); + } finally { + setIsSubmitting(false); + } }; return ( @@ -63,7 +74,7 @@ function CreateLembaga() { > { stateLembaga.create.form.nama = val.target.value; }} @@ -90,6 +101,17 @@ function CreateLembaga() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/pendidikan/info-sekolah/pengajar/[id]/edit/page.tsx b/src/app/admin/(dashboard)/pendidikan/info-sekolah/pengajar/[id]/edit/page.tsx index 6d2a91bb..4df0541d 100644 --- a/src/app/admin/(dashboard)/pendidikan/info-sekolah/pengajar/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/info-sekolah/pengajar/[id]/edit/page.tsx @@ -7,6 +7,7 @@ import { Box, Button, Group, + Loader, Paper, Select, Stack, @@ -29,10 +30,15 @@ function EditPengajar() { const params = useParams(); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ nama: '', lembagaId: '' }); + const [originalData, setOriginalData] = useState({ + nama: '', + lembagaId: '' + }); useEffect(() => { const loadData = async () => { @@ -47,6 +53,10 @@ function EditPengajar() { nama: data.nama || '', lembagaId: data.lembagaId || '', }); + setOriginalData({ + nama: data.nama || '', + lembagaId: data.lembagaId || '', + }); } } catch (err) { console.error(err); @@ -57,12 +67,21 @@ function EditPengajar() { loadData(); }, [params?.id]); + const handleResetForm = () => { + setFormData({ + nama: originalData.nama, + lembagaId: originalData.lembagaId, + }); + toast.info('Form dikembalikan ke data awal'); + }; + const handleChange = (field: keyof FormPengajar, value: string) => { setFormData(prev => ({ ...prev, [field]: value })); }; const handleSubmit = async () => { try { + setIsSubmitting(true); // update global state hanya saat submit pengajarState.edit.form = { ...pengajarState.edit.form, @@ -75,6 +94,8 @@ function EditPengajar() { } catch (err) { console.error(err); toast.error("Terjadi kesalahan saat memperbarui pengajar"); + } finally { + setIsSubmitting(false); } }; @@ -121,6 +142,17 @@ function EditPengajar() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/pendidikan/info-sekolah/pengajar/create/page.tsx b/src/app/admin/(dashboard)/pendidikan/info-sekolah/pengajar/create/page.tsx index 3049368b..a0395acc 100644 --- a/src/app/admin/(dashboard)/pendidikan/info-sekolah/pengajar/create/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/info-sekolah/pengajar/create/page.tsx @@ -6,6 +6,7 @@ import { Box, Button, Group, + Loader, Paper, Select, Stack, @@ -15,13 +16,14 @@ import { } 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 CreatePengajar() { const router = useRouter(); const stateCreate = useProxy(infoSekolahPaud.pengajar); + const [isSubmitting, setIsSubmitting] = useState(false); useEffect(() => { stateCreate.findMany.load(); @@ -36,14 +38,22 @@ function CreatePengajar() { }; const handleSubmit = async () => { - if (!stateCreate.create.form.nama || !stateCreate.create.form.lembagaId) { - return toast.warn('Nama dan Lembaga wajib diisi!'); + try { + setIsSubmitting(true); + if (!stateCreate.create.form.nama || !stateCreate.create.form.lembagaId) { + return toast.warn('Nama dan Lembaga wajib diisi!'); + } + + await stateCreate.create.create(); + + resetForm(); + router.push('/admin/pendidikan/info-sekolah/pengajar'); + } catch (error) { + console.error('Error creating pengajar:', error); + toast.error('Terjadi kesalahan saat menambahkan pengajar'); + } finally { + setIsSubmitting(false); } - - await stateCreate.create.create(); - - resetForm(); - router.push('/admin/pendidikan/info-sekolah/pengajar'); }; return ( @@ -71,7 +81,7 @@ function CreatePengajar() { Nama} placeholder="Masukkan nama pengajar" - defaultValue={stateCreate.create.form.nama} + value={stateCreate.create.form.nama} onChange={(e) => (stateCreate.create.form.nama = e.target.value)} required /> @@ -91,6 +101,17 @@ function CreatePengajar() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/pendidikan/info-sekolah/siswa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/pendidikan/info-sekolah/siswa/[id]/edit/page.tsx index c22b8792..6d283d2e 100644 --- a/src/app/admin/(dashboard)/pendidikan/info-sekolah/siswa/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/info-sekolah/siswa/[id]/edit/page.tsx @@ -7,6 +7,7 @@ import { Box, Button, Group, + Loader, Paper, Select, Stack, @@ -29,6 +30,12 @@ function EditSiswa() { const siswaState = useProxy(infoSekolahPaud.siswa); const params = useParams(); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); + + const [originalData, setOriginalData] = useState({ + nama: "", + lembagaId: "", + }); const [formData, setFormData] = useState({ nama: '', @@ -48,6 +55,10 @@ function EditSiswa() { nama: data.nama || '', lembagaId: data.lembagaId || '', }); + setOriginalData({ + nama: data.nama || '', + lembagaId: data.lembagaId || '', + }); } } catch (error) { console.error('Error loading siswa:', error); @@ -67,9 +78,18 @@ function EditSiswa() { })); }; + const handleResetForm = () => { + setFormData({ + nama: originalData.nama, + lembagaId: originalData.lembagaId, + }); + toast.info('Form dikembalikan ke data awal'); + }; + // Submit ke global state const handleSubmit = async () => { try { + setIsSubmitting(true); siswaState.edit.form = { ...siswaState.edit.form, nama: formData.nama.trim(), @@ -81,6 +101,8 @@ function EditSiswa() { } catch (error) { console.error('Error updating siswa:', error); toast.error('Terjadi kesalahan saat memperbarui siswa'); + } finally { + setIsSubmitting(false); } }; @@ -129,6 +151,17 @@ function EditSiswa() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/pendidikan/info-sekolah/siswa/create/page.tsx b/src/app/admin/(dashboard)/pendidikan/info-sekolah/siswa/create/page.tsx index 34ef5442..0cd59c97 100644 --- a/src/app/admin/(dashboard)/pendidikan/info-sekolah/siswa/create/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/info-sekolah/siswa/create/page.tsx @@ -6,6 +6,7 @@ import { Box, Button, Group, + Loader, Paper, Select, Stack, @@ -15,12 +16,14 @@ import { } 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 CreateSiswa() { const router = useRouter(); const stateCreate = useProxy(infoSekolahPaud.siswa); + const [isSubmitting, setIsSubmitting] = useState(false); useEffect(() => { stateCreate.findMany.load(); @@ -35,10 +38,17 @@ function CreateSiswa() { }; const handleSubmit = async () => { - await stateCreate.create.create(); - - resetForm(); - router.push('/admin/pendidikan/info-sekolah/siswa'); + try { + setIsSubmitting(true); + await stateCreate.create.create(); + resetForm(); + router.push('/admin/pendidikan/info-sekolah/siswa'); + } catch (error) { + console.error('Error creating siswa:', error); + toast.error('Gagal menambahkan siswa'); + } finally { + setIsSubmitting(false); + } }; return ( @@ -64,7 +74,7 @@ function CreateSiswa() { > { stateCreate.create.form.nama = val.target.value; }} @@ -90,6 +100,17 @@ function CreateSiswa() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/jenis-program-yang-diselenggarakan/edit/page.tsx b/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/jenis-program-yang-diselenggarakan/edit/page.tsx index 5c459e6e..30117ce0 100644 --- a/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/jenis-program-yang-diselenggarakan/edit/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/jenis-program-yang-diselenggarakan/edit/page.tsx @@ -6,6 +6,7 @@ import { Button, Center, Group, + Loader, Paper, Stack, Text, @@ -34,6 +35,7 @@ function EditJenisProgramYangDiselenggarakan() { const [formData, setFormData] = useState({ judul: '', content: '' }); const [isSubmitting, setIsSubmitting] = useState(false); + const [originalData, setOriginalData] = useState({ judul: '', content: '' }); // Load data pertama kali useShallowEffect(() => { @@ -49,6 +51,10 @@ function EditJenisProgramYangDiselenggarakan() { judul: editState.findById.data.judul ?? '', content: editState.findById.data.deskripsi ?? '', }); + setOriginalData({ + judul: editState.findById.data.judul ?? '', + content: editState.findById.data.deskripsi ?? '', + }); } }, [editState.findById.data]); @@ -56,6 +62,14 @@ function EditJenisProgramYangDiselenggarakan() { setFormData((prev) => ({ ...prev, [field]: value })); }; + const handleResetForm = () => { + setFormData({ + judul: originalData.judul, + content: originalData.content, + }); + toast.info("Form dikembalikan ke data awal"); + }; + const handleSubmit = async () => { if (!formData.judul.trim()) { toast.error('Judul wajib diisi'); @@ -137,24 +151,32 @@ function EditJenisProgramYangDiselenggarakan() { /> - - + + {/* Tombol Batal */} + - - + {/* Tombol Simpan */} + + diff --git a/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/tempat-kegiatan/edit/page.tsx b/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/tempat-kegiatan/edit/page.tsx index f4a46c19..699e9839 100644 --- a/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/tempat-kegiatan/edit/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/tempat-kegiatan/edit/page.tsx @@ -7,6 +7,7 @@ import { Button, Center, Group, + Loader, Paper, Stack, Text, @@ -39,6 +40,10 @@ function EditTempatKegiatan() { deskripsi: '', }); const [isSubmitting, setIsSubmitting] = useState(false); + const [originalData, setOriginalData] = useState({ + judul: '', + deskripsi: '', + }); // load data pertama kali useShallowEffect(() => { @@ -54,6 +59,10 @@ function EditTempatKegiatan() { judul: editState.findById.data.judul ?? '', deskripsi: editState.findById.data.deskripsi ?? '', }); + setOriginalData({ + judul: editState.findById.data.judul ?? '', + deskripsi: editState.findById.data.deskripsi ?? '', + }); } }, [editState.findById.data]); @@ -61,6 +70,14 @@ function EditTempatKegiatan() { setFormData((prev) => ({ ...prev, [field]: value })); }; + const handleResetForm = () => { + setFormData({ + judul: originalData.judul, + deskripsi: originalData.deskripsi, + }); + toast.info("Form dikembalikan ke data awal"); + }; + const handleSubmit = async () => { if (!formData.judul.trim()) { toast.error('Judul wajib diisi'); @@ -143,23 +160,31 @@ function EditTempatKegiatan() { /> - - - + + {/* Tombol Batal */} + + {/* Tombol Simpan */} + diff --git a/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/tujuan-program/edit/page.tsx b/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/tujuan-program/edit/page.tsx index 48d3d1db..111cd506 100644 --- a/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/tujuan-program/edit/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/tujuan-program/edit/page.tsx @@ -8,6 +8,7 @@ import { Center, Group, Paper, + Loader, Stack, Text, TextInput, @@ -32,6 +33,10 @@ function EditTujuanProgram() { const [formData, setFormData] = useState({ judul: '', deskripsi: '' }); const [isSubmitting, setIsSubmitting] = useState(false); + const [originalData, setOriginalData] = useState({ + judul: '', + deskripsi: '', + }); // Load data pertama kali useShallowEffect(() => { @@ -47,6 +52,10 @@ function EditTujuanProgram() { judul: editState.findById.data.judul ?? '', deskripsi: editState.findById.data.deskripsi ?? '' }); + setOriginalData({ + judul: editState.findById.data.judul ?? '', + deskripsi: editState.findById.data.deskripsi ?? '', + }); } }, [editState.findById.data]); @@ -54,6 +63,14 @@ function EditTujuanProgram() { setFormData(prev => ({ ...prev, [field]: value })); }; + const handleResetForm = () => { + setFormData({ + judul: originalData.judul, + deskripsi: originalData.deskripsi, + }); + toast.info("Form dikembalikan ke data awal"); + }; + const handleSubmit = async () => { if (!formData.judul.trim()) { toast.error('Judul wajib diisi'); @@ -129,23 +146,31 @@ function EditTujuanProgram() { /> - - - + + {/* Tombol Batal */} + + {/* Tombol Simpan */} + diff --git a/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/data-perpustakaan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/data-perpustakaan/[id]/edit/page.tsx index 1f14cc05..52208e15 100644 --- a/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/data-perpustakaan/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/data-perpustakaan/[id]/edit/page.tsx @@ -1,7 +1,7 @@ /* eslint-disable react-hooks/exhaustive-deps */ 'use client' -import { Box, Button, Group, Image, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core'; +import { ActionIcon, Loader, Box, Button, Group, Image, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -18,6 +18,7 @@ function EditPerpustakaanDigital() { const router = useRouter(); const params = useParams(); const editState = useProxy(perpustakaanDigitalState.dataPerpustakaan); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ judul: '', @@ -26,6 +27,14 @@ function EditPerpustakaanDigital() { imageId: '', }); + const [originalData, setOriginalData] = useState({ + judul: '', + deskripsi: '', + kategoriId: '', + imageId: '', + imageUrl: '' + }); + const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); @@ -48,6 +57,14 @@ function EditPerpustakaanDigital() { imageId: data.imageId || '', }); + setOriginalData({ + judul: data.judul || '', + deskripsi: data.deskripsi || '', + kategoriId: data.kategoriId || '', + imageId: data.imageId || '', + imageUrl: data.image?.link || '', + }); + if (data.image?.link) setPreviewImage(data.image.link); } catch (error) { console.error(error); @@ -62,8 +79,20 @@ function EditPerpustakaanDigital() { setFormData((prev) => ({ ...prev, [field]: value })); }; + const handleResetForm = () => { + setFormData({ + judul: originalData.judul, + deskripsi: originalData.deskripsi, + kategoriId: originalData.kategoriId, + imageId: originalData.imageId, + }); + setPreviewImage(originalData.imageUrl || null); + setFile(null); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); // Update global state hanya pas submit editState.update.form = { ...editState.update.form, ...formData }; @@ -81,6 +110,8 @@ function EditPerpustakaanDigital() { } catch (error) { console.error(error); toast.error('Terjadi kesalahan saat memperbarui data perpustakaan'); + } finally { + setIsSubmitting(false); } }; @@ -120,7 +151,7 @@ function EditPerpustakaanDigital() { }} onReject={() => toast.error('File tidak valid.')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} radius="md" p="xl" > @@ -139,14 +170,14 @@ function EditPerpustakaanDigital() { 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)', + }} + > + + )} @@ -190,6 +239,17 @@ function EditPerpustakaanDigital() { {/* Submit Button */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/data-perpustakaan/create/page.tsx b/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/data-perpustakaan/create/page.tsx index e5eec0d7..a85c0caa 100644 --- a/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/data-perpustakaan/create/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/data-perpustakaan/create/page.tsx @@ -3,7 +3,7 @@ import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor'; import perpustakaanDigitalState from '@/app/admin/(dashboard)/_state/pendidikan/perpustakaan-digital'; import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; -import { Box, Button, Group, Image, Paper, Select, Stack, Text, TextInput, Title } 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'; @@ -16,6 +16,7 @@ function CreateDataPerpustakaan() { const router = useRouter(); const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); useEffect(() => { perpustakaanDigitalState.kategoriBuku.findMany.load(); @@ -33,24 +34,32 @@ function CreateDataPerpustakaan() { }; const handleSubmit = async () => { - if (!file) { - return toast.warn("Pilih file gambar terlebih dahulu"); + try { + if (!file) { + return toast.warn("Pilih file gambar terlebih dahulu"); + } + + setIsSubmitting(true); + 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"); + } + + createState.create.form.imageId = uploaded.id; + await createState.create.create(); + resetForm(); + router.push("/admin/pendidikan/perpustakaan-digital/data-perpustakaan") + } catch (error) { + console.error("Error creating data perpustakaan:", error); + toast.error("Gagal menambahkan data perpustakaan"); + } 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"); - } - - createState.create.form.imageId = uploaded.id; - await createState.create.create(); - resetForm(); - router.push("/admin/pendidikan/perpustakaan-digital/data-perpustakaan") }; return ( @@ -78,7 +87,7 @@ function CreateDataPerpustakaan() { Judul} placeholder='Masukkan judul' - defaultValue={createState.create.form.judul} + value={createState.create.form.judul} onChange={(val) => { createState.create.form.judul = val.target.value; }} @@ -119,7 +128,7 @@ function CreateDataPerpustakaan() { }} onReject={() => toast.error('File tidak valid.')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} radius="md" p="xl" > @@ -140,7 +149,7 @@ function CreateDataPerpustakaan() { {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)/pendidikan/perpustakaan-digital/kategori-buku/[id]/page.tsx b/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/kategori-buku/[id]/page.tsx index b29b3156..67a51242 100644 --- a/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/kategori-buku/[id]/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/kategori-buku/[id]/page.tsx @@ -3,7 +3,7 @@ import perpustakaanDigitalState from '@/app/admin/(dashboard)/_state/pendidikan/perpustakaan-digital'; import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Stack, TextInput, Title } 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'; @@ -14,6 +14,11 @@ function EditKategoriBuku() { const editState = useProxy(perpustakaanDigitalState.kategoriBuku); const router = useRouter(); const params = useParams(); + const [isSubmitting, setIsSubmitting] = useState(false); + + const [originalData, setOriginalData] = useState({ + name: "", + }) const [formData, setFormData] = useState({ name: '' }); const [loading, setLoading] = useState(true); @@ -27,6 +32,9 @@ function EditKategoriBuku() { const data = await editState.update.load(id); if (data) { setFormData({ name: data.name || '' }); + setOriginalData({ + name: data.name || "", + }); } } catch (error) { console.error('Error loading kategori buku:', error); @@ -43,8 +51,16 @@ function EditKategoriBuku() { setFormData((prev) => ({ ...prev, name: e.target.value })); }; + const handleResetForm = () => { + setFormData({ + name: originalData.name, + }); + toast.info('Form dikembalikan ke data awal'); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); // Update global state hanya saat submit editState.update.form = { ...editState.update.form, name: formData.name }; await editState.update.update(); @@ -53,6 +69,8 @@ function EditKategoriBuku() { } catch (error) { console.error('Error updating kategori buku:', error); toast.error('Terjadi kesalahan saat memperbarui kategori buku'); + } finally { + setIsSubmitting(false); } }; @@ -86,6 +104,18 @@ function EditKategoriBuku() { /> + {/* Tombol Batal */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/kategori-buku/create/page.tsx b/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/kategori-buku/create/page.tsx index c82b184c..ec7ca529 100644 --- a/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/kategori-buku/create/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/kategori-buku/create/page.tsx @@ -1,14 +1,17 @@ 'use client' import perpustakaanDigitalState from '@/app/admin/(dashboard)/_state/pendidikan/perpustakaan-digital'; import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; +import { Box, Button, Loader, Group, Paper, Stack, Text, 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 CreateKategoriBuku() { const createState = useProxy(perpustakaanDigitalState.kategoriBuku) const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { createState.create.form = { @@ -17,9 +20,17 @@ function CreateKategoriBuku() { }; const handleSubmit = async () => { - await createState.create.create(); - resetForm(); - router.push("/admin/pendidikan/perpustakaan-digital/kategori-buku"); + try { + setIsSubmitting(true); + await createState.create.create(); + resetForm(); + router.push("/admin/pendidikan/perpustakaan-digital/kategori-buku"); + } catch (error) { + console.error("Error creating kategori buku:", error); + toast.error("Terjadi kesalahan saat menambahkan kategori buku"); + } finally { + setIsSubmitting(false); + } }; return ( @@ -47,7 +58,7 @@ function CreateKategoriBuku() { Nama Kategori Buku} placeholder='Masukkan nama kategori buku' - defaultValue={createState.create.form.name} + value={createState.create.form.name} onChange={(val) => { createState.create.form.name = val.target.value; }} @@ -55,6 +66,17 @@ function CreateKategoriBuku() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/peminjam/[id]/edit/page.tsx b/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/peminjam/[id]/edit/page.tsx index 48463e0a..161a10bf 100644 --- a/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/peminjam/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/peminjam/[id]/edit/page.tsx @@ -8,6 +8,7 @@ import { Box, Button, Group, + Loader, Paper, Select, Stack, @@ -29,6 +30,7 @@ function EditPeminjam() { const stateEdit = useProxy(perpustakaanDigitalState.peminjamanBuku); const router = useRouter(); const params = useParams(); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState<{ nama: string; @@ -56,6 +58,18 @@ function EditPeminjam() { catatan: '', }); + const [originalData, setOriginalData] = useState({ + nama: "", + noTelp: "", + alamat: "", + bukuId: "", + tanggalPinjam: "", + batasKembali: "", + tanggalKembali: "", + status: "Dipinjam", // Default status + catatan: "", + }) + useShallowEffect(() => { perpustakaanDigitalState.dataPerpustakaan.findManyAll.load() }) @@ -84,6 +98,22 @@ function EditPeminjam() { status: (data.status as Status) ?? prev.status, catatan: data.catatan ?? prev.catatan, })); + setOriginalData((prev) => ({ + ...prev, + nama: data.nama ?? prev.nama, + noTelp: data.noTelp ?? prev.noTelp, + alamat: data.alamat ?? prev.alamat, + bukuId: data.bukuId ?? prev.bukuId, + buku: data.buku ? { + id: data.buku.id, + judul: data.buku.judul + } : undefined, + tanggalPinjam: data.tanggalPinjam ?? prev.tanggalPinjam, + batasKembali: data.batasKembali ?? prev.batasKembali, + tanggalKembali: data.tanggalKembali ?? prev.tanggalKembali, + status: (data.status as Status) ?? prev.status, + catatan: data.catatan ?? prev.catatan, + })); } } catch (error) { console.error("Error loading peminjam:", error); @@ -94,6 +124,22 @@ function EditPeminjam() { loadPeminjam(); }, [params?.id]); + const handleResetForm = () => { + setFormData((prev) => ({ + ...prev, + nama: originalData.nama ?? prev.nama, + noTelp: originalData.noTelp ?? prev.noTelp, + alamat: originalData.alamat ?? prev.alamat, + bukuId: originalData.bukuId ?? prev.bukuId, + tanggalPinjam: originalData.tanggalPinjam ?? prev.tanggalPinjam, + batasKembali: originalData.batasKembali ?? prev.batasKembali, + tanggalKembali: originalData.tanggalKembali ?? prev.tanggalKembali, + status: (originalData.status as Status) ?? prev.status, + catatan: originalData.catatan ?? prev.catatan, + })); + toast.info("Form dikembalikan ke data awal"); + }; + const handleChange = (field: string, value: string | Status) => { setFormData((prev) => ({ ...prev, [field]: value })); @@ -101,6 +147,7 @@ function EditPeminjam() { const handleSubmit = async () => { try { + setIsSubmitting(true); stateEdit.update.form = { ...stateEdit.update.form, ...formData, @@ -112,6 +159,8 @@ function EditPeminjam() { } catch (error) { console.error("Error updating peminjam:", error); toast.error("Terjadi kesalahan saat memperbarui peminjam"); + } finally { + setIsSubmitting(false); } }; @@ -174,6 +223,7 @@ function EditPeminjam() { { perpustakaanDigitalState.peminjamanBuku.update.form.tanggalPinjam = @@ -185,6 +235,7 @@ function EditPeminjam() { /> { perpustakaanDigitalState.peminjamanBuku.update.form.tanggalKembali = @@ -196,6 +247,7 @@ function EditPeminjam() { /> { perpustakaanDigitalState.peminjamanBuku.update.form.batasKembali = @@ -229,6 +281,17 @@ function EditPeminjam() { + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/peminjam/page.tsx b/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/peminjam/page.tsx index 89d8505a..16ebda74 100644 --- a/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/peminjam/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/peminjam/page.tsx @@ -89,9 +89,10 @@ function ListPeminjamBuku({ search }: { search: string }) { No - Nama Peminjam - Status - Detail + Nama Peminjam + Status + Alamat + Detail @@ -105,6 +106,9 @@ function ListPeminjamBuku({ search }: { search: string }) { {renderStatusBadge(item.status)} + + {item.alamat} + + + {/* Tombol Batal */} + - - + {/* Tombol Simpan */} + + diff --git a/src/app/admin/(dashboard)/pendidikan/program-pendidikan-anak/tujuan-program/edit/page.tsx b/src/app/admin/(dashboard)/pendidikan/program-pendidikan-anak/tujuan-program/edit/page.tsx index 5d3c2e87..e9bef0b8 100644 --- a/src/app/admin/(dashboard)/pendidikan/program-pendidikan-anak/tujuan-program/edit/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/program-pendidikan-anak/tujuan-program/edit/page.tsx @@ -6,6 +6,7 @@ import { Button, Center, Group, + Loader, Paper, Stack, Text, @@ -32,6 +33,10 @@ function EditTujuanProgram() { // Menggunakan satu state lokal untuk form const [formData, setFormData] = useState({ judul: '', deskripsi: '' }); const [isSubmitting, setIsSubmitting] = useState(false); + const [originalData, setOriginalData] = useState({ + judul: '', + deskripsi: '', + }); // Load data pertama kali useShallowEffect(() => { @@ -47,9 +52,21 @@ function EditTujuanProgram() { judul: editState.findById.data.judul ?? '', deskripsi: editState.findById.data.deskripsi ?? '', }); + setOriginalData({ + judul: editState.findById.data.judul ?? '', + deskripsi: editState.findById.data.deskripsi ?? '', + }); } }, [editState.findById.data]); + const handleResetForm = () => { + setFormData({ + judul: originalData.judul, + deskripsi: originalData.deskripsi, + }); + toast.info("Form dikembalikan ke data awal"); + }; + const handleChange = (field: 'judul' | 'deskripsi', value: string) => { setFormData(prev => ({ ...prev, [field]: value })); }; @@ -132,23 +149,31 @@ function EditTujuanProgram() { /> - - - + + {/* Tombol Batal */} + + {/* Tombol Simpan */} + diff --git a/src/app/admin/(dashboard)/ppid/daftar-informasi-publik/create/page.tsx b/src/app/admin/(dashboard)/ppid/daftar-informasi-publik/create/page.tsx index 7f8918bb..aa331732 100644 --- a/src/app/admin/(dashboard)/ppid/daftar-informasi-publik/create/page.tsx +++ b/src/app/admin/(dashboard)/ppid/daftar-informasi-publik/create/page.tsx @@ -66,7 +66,7 @@ export default function CreateDaftarInformasi() { { daftarInformasi.create.form.jenisInformasi = e.target.value; }} @@ -94,7 +94,7 @@ export default function CreateDaftarInformasi() { { daftarInformasi.create.form.tanggal = e.target.value; }} diff --git a/src/app/admin/(dashboard)/ppid/dasar-hukum/edit/page.tsx b/src/app/admin/(dashboard)/ppid/dasar-hukum/edit/page.tsx index 4867d90b..a092f2e9 100644 --- a/src/app/admin/(dashboard)/ppid/dasar-hukum/edit/page.tsx +++ b/src/app/admin/(dashboard)/ppid/dasar-hukum/edit/page.tsx @@ -1,6 +1,6 @@ 'use client'; import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Stack, Text, Title } from '@mantine/core'; +import { Loader, Box, Button, Group, Paper, Stack, Text, Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack } from '@tabler/icons-react'; import dynamic from 'next/dynamic'; @@ -8,6 +8,7 @@ import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; import { useProxy } from 'valtio/utils'; import stateDasarHukumPPID from '../../../_state/ppid/dasar_hukum/dasarHukum'; +import { toast } from 'react-toastify'; const PPIDTextEditor = dynamic( () => import('../../_com/PPIDTextEditor').then(mod => mod.PPIDTextEditor), @@ -20,6 +21,11 @@ function EditDasarHukum() { // Gabungkan judul & content jadi satu state lokal const [formData, setFormData] = useState({ judul: '', content: '' }); + const [isSubmitting, setIsSubmitting] = useState(false); + const [originalData, setOriginalData] = useState({ + judul: '', + content: '', + }); // Load data awal sekali useShallowEffect(() => { @@ -35,24 +41,44 @@ function EditDasarHukum() { judul: dasarHukumState.findById.data.judul ?? '', content: dasarHukumState.findById.data.content ?? '', }); + setOriginalData({ + judul: dasarHukumState.findById.data.judul ?? '', + content: dasarHukumState.findById.data.content ?? '', + }); } }, [dasarHukumState.findById.data]); + const handleResetForm = () => { + setFormData({ + judul: originalData.judul, + content: originalData.content, + }); + toast.info("Form dikembalikan ke data awal"); + }; + const handleSubmit = () => { - if (dasarHukumState.findById.data) { - // Update global state hanya saat submit - const updated = { ...dasarHukumState.findById.data, ...formData }; - dasarHukumState.update.save(updated); + try { + setIsSubmitting(true); + if (dasarHukumState.findById.data) { + // Update global state hanya saat submit + const updated = { ...dasarHukumState.findById.data, ...formData }; + dasarHukumState.update.save(updated); + } + router.push('/admin/ppid/dasar-hukum'); + } catch (error) { + console.error("Error updating dasar hukum:", error); + toast.error("Terjadi kesalahan saat memperbarui dasar hukum"); + } finally { + setIsSubmitting(false); } - router.push('/admin/ppid/dasar-hukum'); }; return ( - + Edit Dasar Hukum PPID @@ -93,10 +119,22 @@ function EditDasarHukum() { - + {/* ======= Tombol Aksi ======= */} + + {/* Tombol Batal */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ppid/indeks-kepuasan-masyarakat/responden/create/page.tsx b/src/app/admin/(dashboard)/ppid/indeks-kepuasan-masyarakat/responden/create/page.tsx index 7056c869..9feb87e7 100644 --- a/src/app/admin/(dashboard)/ppid/indeks-kepuasan-masyarakat/responden/create/page.tsx +++ b/src/app/admin/(dashboard)/ppid/indeks-kepuasan-masyarakat/responden/create/page.tsx @@ -64,7 +64,7 @@ function RespondenCreate() { label="Nama" type='text' placeholder="masukkan nama" - defaultValue={stategrafikBerdasarkanResponden.create.form.name} + value={stategrafikBerdasarkanResponden.create.form.name} onChange={(val) => { stategrafikBerdasarkanResponden.create.form.name = val.currentTarget.value; }} @@ -73,7 +73,7 @@ function RespondenCreate() { label="Tanggal" type="date" placeholder="masukkan tanggal" - defaultValue={stategrafikBerdasarkanResponden.create.form.tanggal} + value={stategrafikBerdasarkanResponden.create.form.tanggal} onChange={(val) => { stategrafikBerdasarkanResponden.create.form.tanggal = val.currentTarget.value; }} diff --git a/src/app/admin/(dashboard)/ppid/profil-ppid/[id]/page.tsx b/src/app/admin/(dashboard)/ppid/profil-ppid/[id]/page.tsx new file mode 100644 index 00000000..6c166b5b --- /dev/null +++ b/src/app/admin/(dashboard)/ppid/profil-ppid/[id]/page.tsx @@ -0,0 +1,300 @@ +'use client'; + +import colors from '@/con/colors'; +import ApiFetch from '@/lib/api-fetch'; +import { ActionIcon, Alert, Box, Button, Center, Group, Image, Loader, Paper, 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'; +import { useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; +import stateProfilePPID from '../../../_state/ppid/profile_ppid/profile_PPID'; +import Biodata from '../_com/biodata/biodataForm'; +import PengalamanOrganisasi from '../_com/pengalaman_organisasi/pengalamanForm'; +import ProgramKerjaUnggulan from '../_com/program_kerja_unggulan/programKerjaForm'; +import RiwayatKarir from '../_com/riwayat_karir/riwayatKarirForm'; + + +// import komponen rich text jika ada + + +function EditProfilePPID() { + const allState = useProxy(stateProfilePPID); + const router = useRouter(); + const params = useParams(); + const id = params?.id as string; + + const [file, setFile] = useState(null); + const [previewImage, setPreviewImage] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + useEffect(() => { + if (!id) return; + stateProfilePPID.loadForEdit(id).then((data) => { + if (data?.image?.link) setPreviewImage(data.image.link); + }); + }, [id]); + + const handleFieldChange = (field: keyof typeof allState.editForm.form, value: string) => { + stateProfilePPID.editForm.updateField(field, value); + }; + + const handleSubmit = async () => { + try { + setIsSubmitting(true); + stateProfilePPID.editForm.loading = true; + + let imageId = allState.editForm.form.imageId; + + // Upload file baru jika ada + if (file) { + const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name }); + const uploaded = res.data?.data; + if (!uploaded?.id) { + toast.error("Gagal upload gambar"); + return; + } + imageId = uploaded.id; + } + + // Update form di state + allState.editForm.form.imageId = imageId; + + const success = await stateProfilePPID.editForm.submit(); + if (success) { + toast.success("Profil berhasil diperbarui!"); + router.push("/admin/ppid/profil-ppid"); + } + } catch (error) { + console.error("Error updating profile:", error); + toast.error("Terjadi kesalahan saat memperbarui profil"); + } finally { + stateProfilePPID.editForm.loading = false; + setIsSubmitting(false); + } + }; + + const handleResetForm = () => { + if (!allState.profile.data) return; + + // Reset form ke data awal yang di-load + const original = allState.profile.data; + + stateProfilePPID.editForm.form = { + name: original.name || '', + imageId: original.imageId || '', + biodata: original.biodata || '', + riwayat: original.riwayat || '', + pengalaman: original.pengalaman || '', + unggulan: original.unggulan || '', + }; + + // Reset preview gambar juga + setPreviewImage(original.image?.link || null); + setFile(null); + + toast.info('Perubahan dibatalkan'); + }; + + + const handleBack = () => router.back(); + + // Kondisi loading & error handling + if (allState.profile.loading) { + return ( +
+ Memuat data profil... +
+ ); + } + + if (allState.profile.error) { + return ( + + + } color="red"> + Error + {allState.profile.error} + + + ); + } + + if (!allState.profile.data) { + return ( + + + } color="yellow"> + Data tidak ditemukan + Profil PPID tidak dapat ditemukan + + + ); + } + + return ( + + + + + + Edit Profil PPID + + + + + + Edit Profil PPID + + {/* Nama */} + Nama Perbekel} + placeholder="Masukkan nama perbekel" + value={allState.editForm.form.name} + onChange={(e) => handleFieldChange('name', e.currentTarget.value)} + error={!allState.editForm.form.name && "Nama wajib diisi"} + /> + + {/* Upload Gambar */} + + Gambar + { + const selectedFile = files[0]; + if (selectedFile) { + setFile(selectedFile); + setPreviewImage(URL.createObjectURL(selectedFile)); + } + }} + onReject={() => toast.error('File tidak valid.')} + maxSize={5 * 1024 ** 2} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} + > + + + + + + + + + + +
+ + Tarik gambar ke sini atau klik untuk memilih file + + + 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)', + }} + > + + + + )} +
+ + {/* Form Rich Text */} + handleFieldChange('biodata', val)} + /> + + handleFieldChange('riwayat', val)} + /> + + handleFieldChange('pengalaman', val)} + /> + + handleFieldChange('unggulan', val)} + /> + + {/* ======= Tombol Aksi ======= */} + + {/* Tombol Batal */} + + + {/* Tombol Simpan */} + + +
+
+
+
+ ); +} + +export default EditProfilePPID; diff --git a/src/app/admin/(dashboard)/ppid/profile-ppid/[id]/biodata/biodataForm.tsx b/src/app/admin/(dashboard)/ppid/profil-ppid/_com/biodata/biodataForm.tsx similarity index 100% rename from src/app/admin/(dashboard)/ppid/profile-ppid/[id]/biodata/biodataForm.tsx rename to src/app/admin/(dashboard)/ppid/profil-ppid/_com/biodata/biodataForm.tsx diff --git a/src/app/admin/(dashboard)/ppid/profile-ppid/_com/editPPIDEditor.tsx b/src/app/admin/(dashboard)/ppid/profil-ppid/_com/editPPIDEditor.tsx similarity index 100% rename from src/app/admin/(dashboard)/ppid/profile-ppid/_com/editPPIDEditor.tsx rename to src/app/admin/(dashboard)/ppid/profil-ppid/_com/editPPIDEditor.tsx diff --git a/src/app/admin/(dashboard)/ppid/profile-ppid/[id]/pengalaman_organisasi/pengalamanForm.tsx b/src/app/admin/(dashboard)/ppid/profil-ppid/_com/pengalaman_organisasi/pengalamanForm.tsx similarity index 100% rename from src/app/admin/(dashboard)/ppid/profile-ppid/[id]/pengalaman_organisasi/pengalamanForm.tsx rename to src/app/admin/(dashboard)/ppid/profil-ppid/_com/pengalaman_organisasi/pengalamanForm.tsx diff --git a/src/app/admin/(dashboard)/ppid/profile-ppid/[id]/program_kerja_unggulan/programKerjaForm.tsx b/src/app/admin/(dashboard)/ppid/profil-ppid/_com/program_kerja_unggulan/programKerjaForm.tsx similarity index 100% rename from src/app/admin/(dashboard)/ppid/profile-ppid/[id]/program_kerja_unggulan/programKerjaForm.tsx rename to src/app/admin/(dashboard)/ppid/profil-ppid/_com/program_kerja_unggulan/programKerjaForm.tsx diff --git a/src/app/admin/(dashboard)/ppid/profile-ppid/[id]/riwayat_karir/riwayatKarirForm.tsx b/src/app/admin/(dashboard)/ppid/profil-ppid/_com/riwayat_karir/riwayatKarirForm.tsx similarity index 100% rename from src/app/admin/(dashboard)/ppid/profile-ppid/[id]/riwayat_karir/riwayatKarirForm.tsx rename to src/app/admin/(dashboard)/ppid/profil-ppid/_com/riwayat_karir/riwayatKarirForm.tsx diff --git a/src/app/admin/(dashboard)/ppid/profile-ppid/page.tsx b/src/app/admin/(dashboard)/ppid/profil-ppid/page.tsx similarity index 98% rename from src/app/admin/(dashboard)/ppid/profile-ppid/page.tsx rename to src/app/admin/(dashboard)/ppid/profil-ppid/page.tsx index c6c5fdc7..1e49f3da 100644 --- a/src/app/admin/(dashboard)/ppid/profile-ppid/page.tsx +++ b/src/app/admin/(dashboard)/ppid/profil-ppid/page.tsx @@ -40,7 +40,7 @@ function Page() { variant="light" leftSection={} radius="md" - onClick={() => router.push(`/admin/ppid/profile-ppid/${allList.profile.data?.id}`)} + onClick={() => router.push(`/admin/ppid/profil-ppid/${allList.profile.data?.id}`)} > Edit diff --git a/src/app/admin/(dashboard)/ppid/profile-ppid/[id]/page.tsx b/src/app/admin/(dashboard)/ppid/profile-ppid/[id]/page.tsx deleted file mode 100644 index babed390..00000000 --- a/src/app/admin/(dashboard)/ppid/profile-ppid/[id]/page.tsx +++ /dev/null @@ -1,289 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -'use client' -import colors from '@/con/colors'; -import { Alert, Box, Button, Center, Group, Image, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core'; -import { useEffect, useState } from 'react'; -import { useProxy } from 'valtio/utils'; -import stateProfilePPID from '../../../_state/ppid/profile_ppid/profile_PPID'; - -import ApiFetch from '@/lib/api-fetch'; -import { Dropzone } from '@mantine/dropzone'; -import { IconAlertCircle, IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; -import { useParams, useRouter } from 'next/navigation'; -import { toast } from 'react-toastify'; -import Biodata from './biodata/biodataForm'; -import PengalamanOrganisasi from './pengalaman_organisasi/pengalamanForm'; -import ProgramKerjaUnggulan from './program_kerja_unggulan/programKerjaForm'; -import RiwayatKarir from './riwayat_karir/riwayatKarirForm'; - -function EditProfilePPID() { - const allState = useProxy(stateProfilePPID); - const params = useParams(); - const router = useRouter(); - - // UI States - const [previewImage, setPreviewImage] = useState(null); - const [file, setFile] = useState(null); - const [isSubmitting, setIsSubmitting] = useState(false); - - // Load data on mount - useEffect(() => { - - const loadData = async () => { - const id = params?.id as string; - if (!id) { - toast.error("ID tidak valid"); - router.push("/admin/ppid/profile-ppid"); - return; - } - - try { - const profileData = await stateProfilePPID.loadForEdit(id); - - if (profileData && profileData.image?.link) { - setPreviewImage(profileData.image.link); - } - } catch (error) { - console.error("Error loading profile:", error); - toast.error("Gagal memuat data profile"); - } - }; - - loadData(); - - return () => { - stateProfilePPID.editForm.reset(); // cleanup form - }; - }, [params?.id, router]); - - const handleFieldChange = (field: string, value: string) => { - stateProfilePPID.editForm.updateField(field as any, value); - }; - - const handleSubmit = async () => { - if (isSubmitting || !stateProfilePPID.editForm.form.name.trim()) { - toast.error("Nama wajib diisi"); - return; - } - - setIsSubmitting(true); - - try { - // Upload file jika ada - if (file) { - const uploadResponse = await ApiFetch.api.fileStorage.create.post({ file, name: file.name }); - const uploaded = uploadResponse.data?.data; - - if (!uploaded?.id) { - toast.error("Gagal upload gambar"); - return; - } - - stateProfilePPID.editForm.form.imageId = uploaded.id; - } - - // Submit form - const success = await stateProfilePPID.editForm.submit(); - - if (success) { - toast.success("Berhasil menyimpan perubahan"); - router.push("/admin/ppid/profile-ppid"); - } - } catch (error) { - console.error("Error submitting form:", error); - toast.error("Gagal menyimpan profile"); - } finally { - setIsSubmitting(false); - } - }; - - const handleBack = () => { - router.back(); - }; - - // Loading state - if (allState.profile.loading) { - return ( - -
- Memuat data profile... -
-
- ); - } - - // Error state - if (allState.profile.error) { - return ( - - - - } color="red"> - Error - {allState.profile.error} - - - - ); - } - - // No data state - if (!allState.profile.data) { - return ( - - - - } color="yellow"> - Data tidak ditemukan - Profile PPID tidak dapat ditemukan - - - - ); - } - - return ( - - - - - - - - Edit Profil PPID - - - - - - Edit Profile PPID - - {/* Nama Field */} - Nama Perbekel} - placeholder="Masukkan nama perbekel" - value={allState.editForm.form.name} - onChange={(e) => handleFieldChange('name', e.currentTarget.value)} - error={!allState.editForm.form.name && "Nama wajib diisi"} - /> - - {/* File Upload */} - - Gambar - - { - const selectedFile = files[0]; // Ambil file pertama - if (selectedFile) { - setFile(selectedFile); - setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview - } - }} - onReject={() => toast.error('File tidak valid.')} - maxSize={5 * 1024 ** 2} // Maks 5MB - accept={{ 'image/*': [] }} - > - - - - - - - - - - - -
- - Tarik gambar ke sini atau klik untuk memilih file - - - Maksimal 5MB dan harus format gambar - -
-
-
- - {/* Tampilkan preview kalau ada */} - {previewImage && ( - - - - )} - -
-
- - {/* Rich Components */} - handleFieldChange('biodata', val)} - /> - - handleFieldChange('riwayat', val)} - /> - - handleFieldChange('pengalaman', val)} - /> - - handleFieldChange('unggulan', val)} - /> - - {/* Submit Button */} - - - - - -
-
-
-
- ); -} - -export default EditProfilePPID; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/ppid/struktur-ppid/pegawai/[id]/edit/page.tsx b/src/app/admin/(dashboard)/ppid/struktur-ppid/pegawai/[id]/edit/page.tsx index f4c68282..946398c3 100644 --- a/src/app/admin/(dashboard)/ppid/struktur-ppid/pegawai/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/ppid/struktur-ppid/pegawai/[id]/edit/page.tsx @@ -4,6 +4,7 @@ import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, @@ -13,7 +14,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'; @@ -26,7 +28,20 @@ export default function EditPegawaiPPID() { const router = useRouter(); const { id } = useParams<{ id: string }>(); const stateOrganisasi = useProxy(stateStrukturPPID.pegawai); + const [isSubmitting, setIsSubmitting] = useState(false); + const [originalData, setOriginalData] = useState({ + namaLengkap: "", + gelarAkademik: "", + imageId: "", + tanggalMasuk: "", + email: "", + telepon: "", + alamat: "", + posisiId: "", + imageUrl: "", + isActive: true, + }); const [formData, setFormData] = useState({ namaLengkap: '', gelarAkademik: '', @@ -66,6 +81,18 @@ export default function EditPegawaiPPID() { 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 || '', + imageUrl: data.image?.link || '', + isActive: data.isActive ?? true, + }); setPreviewImage(data.image?.link || null); } @@ -78,8 +105,26 @@ export default function EditPegawaiPPID() { 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'); } @@ -102,15 +147,17 @@ export default function EditPegawaiPPID() { } 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 @@ -163,20 +210,44 @@ export default function EditPegawaiPPID() { 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)', + }} + > + + )} @@ -219,12 +290,29 @@ export default function EditPegawaiPPID() { Posisi ({ + label: item.nama, + value: item.id, + })) || []} + value={stateOrganisasi.create.form.posisiId || null} + onChange={(val: string | null) => { + if (val) { + const selected = stateStrukturPPID.posisiOrganisasi.findManyAll.data?.find( + (item) => item.id === val + ); + if (selected) { + stateOrganisasi.create.form.posisiId = selected.id; + } + } else { + stateOrganisasi.create.form.posisiId = ''; + } + }} + searchable + clearable + nothingFoundMessage="Tidak ditemukan" + required + /> + {/*
+ + + Uraian + + Anggaran (Rp) + + + Realisasi (Rp) + + + Lebih/(Kurang) (Rp) + + + Persentase (%) + + + + + {items.map((item, index) => { + const selisih = Number(item.anggaran) - Number(item.realisasi); + const persen = item.anggaran + ? (item.realisasi / item.anggaran) * 100 + : 0; + return ( + + + {`${index + 1}. ${item.nama}`} + + {formatRupiah(item.anggaran)} + {formatRupiah(item.realisasi)} + {formatRupiah(selisih)} + {persen.toFixed(2)} + + ); + })} + + + + Total {title} + {formatRupiah(totalAnggaran)} + {formatRupiah(totalRealisasi)} + {formatRupiah(totalSelisih)} + {totalPersen.toFixed(2)} + + +
+
+ ); + }; + + return ( + + + + APB Desa Tahun {data.tahun} + + + {renderSection('Pendapatan', data.pendapatan)} + {renderSection('Belanja', data.belanja)} + {renderSection('Pembiayaan', data.pembiayaan)} + + + ); +} + +function APBDesaProgress() { + const data = { + tahun: 2024, + pendapatan: [ + { id: 1, nama: 'Pendapatan Asli Desa', anggaran: 32000000, realisasi: 6500000 }, + { id: 2, nama: 'Dana Desa', anggaran: 125000000, realisasi: 120000000 }, + { id: 3, nama: 'Bagi Hasil Pajak dan Retribusi', anggaran: 10000000, realisasi: 9000000 }, + ], + belanja: [ + { id: 1, nama: 'Belanja Pegawai', anggaran: 80000000, realisasi: 75000000 }, + { id: 2, nama: 'Belanja Barang & Jasa', anggaran: 50000000, realisasi: 42000000 }, + ], + pembiayaan: [ + { id: 1, nama: 'Penerimaan Pembiayaan', anggaran: 15000000, realisasi: 15000000 }, + { id: 2, nama: 'Pengeluaran Pembiayaan', anggaran: 10000000, realisasi: 8000000 }, + ], + }; + + const formatRupiah = (value: number) => + new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 2, + }).format(value); + + const calcProgress = (items: any[]) => { + const anggaran = items.reduce((sum, i) => sum + i.anggaran, 0); + const realisasi = items.reduce((sum, i) => sum + i.realisasi, 0); + const persen = anggaran ? (realisasi / anggaran) * 100 : 0; + return { anggaran, realisasi, persen }; + }; + + const pendapatan = calcProgress(data.pendapatan); + const belanja = calcProgress(data.belanja); + const pembiayaan = calcProgress(data.pembiayaan); + + const renderProgress = (label: string, dataset: any) => ( + + {label} + + {formatRupiah(dataset.realisasi)} | {formatRupiah(dataset.anggaran)} + + + + ); + + return ( + + + + Grafik APB Desa Tahun {data.tahun} + + + {renderProgress('Pendapatan Desa', pendapatan)} + {renderProgress('Belanja Desa', belanja)} + {renderProgress('Pembiayaan Desa', pembiayaan)} + + + ); +} + + + export default Page diff --git a/src/app/darmasaba/_com/ModernNeewsNotification.tsx b/src/app/darmasaba/_com/ModernNeewsNotification.tsx index 2741289c..b2dfd547 100644 --- a/src/app/darmasaba/_com/ModernNeewsNotification.tsx +++ b/src/app/darmasaba/_com/ModernNeewsNotification.tsx @@ -3,7 +3,7 @@ import { useState, useEffect } from "react"; import { Box, Paper, Text, Group, CloseButton, Badge, ActionIcon, Stack, Transition } from "@mantine/core"; import { IconBell, IconChevronRight } from "@tabler/icons-react"; -import { usePathname, useRouter } from "next/navigation"; // 👉 tambahkan ini +import { usePathname, useRouter } from "next/navigation"; interface NewsItem { id: string | number; @@ -31,7 +31,7 @@ export default function ModernNewsNotification({ news = [], autoShowDelay = 2000 }: ModernNewsNotificationProps) { - const router = useRouter(); // 👉 router Next.js + const router = useRouter(); const [toastVisible, setToastVisible] = useState(false); const [widgetOpen, setWidgetOpen] = useState(false); const [hasNewNotifications, setHasNewNotifications] = useState(true); @@ -39,8 +39,7 @@ export default function ModernNewsNotification({ const [iconVisible, setIconVisible] = useState(true); const pathname = usePathname(); - - + // Auto show toast on page load useEffect(() => { if (news.length > 0 && !toastVisible && !hasShownToast) { const timer = setTimeout(() => { @@ -51,6 +50,7 @@ export default function ModernNewsNotification({ } }, [news.length, autoShowDelay, toastVisible, hasShownToast]); + // Auto hide toast after 8 seconds useEffect(() => { if (toastVisible) { const timer = setTimeout(() => { @@ -60,22 +60,26 @@ export default function ModernNewsNotification({ } }, [toastVisible]); - // Ganti useEffect scroll yang lama dengan versi berikut: - + // Enhanced scroll handler with better thresholds useEffect(() => { let lastScrollY = window.scrollY; + const HIDE_THRESHOLD = 100; // Mulai hide saat scroll > 100px + const SHOW_THRESHOLD = 50; // Hanya show ketika benar-benar di atas (< 50px) const handleScroll = () => { const currentScrollY = window.scrollY; + const scrollDirection = currentScrollY > lastScrollY ? 'down' : 'up'; - // Kontrol ikon lonceng - if (currentScrollY > lastScrollY && currentScrollY > 100) { + // Logic untuk hide/show icon + if (scrollDirection === 'down' && currentScrollY > HIDE_THRESHOLD) { + // Scroll ke bawah dan sudah melewati threshold → hide setIconVisible(false); - } else if (currentScrollY < lastScrollY) { + } else if (scrollDirection === 'up' && currentScrollY < SHOW_THRESHOLD) { + // Scroll ke atas dan sudah di posisi paling atas → show setIconVisible(true); } - // 🔴 BARU: Sembunyikan toast saat scroll ke bawah melewati 150px + // Hide toast saat scroll ke bawah melewati 150px if (currentScrollY > 150 && toastVisible) { setToastVisible(false); } @@ -85,11 +89,11 @@ export default function ModernNewsNotification({ window.addEventListener('scroll', handleScroll, { passive: true }); return () => window.removeEventListener('scroll', handleScroll); - }, [toastVisible]); // 👈 tambahkan toastVisible sebagai dependency + }, [toastVisible]); const currentNews = news[0]; - // 👉 Fungsi baru untuk handle klik notifikasi + // Handle notification click const handleNotificationClick = (item: NewsItem) => { setWidgetOpen(false); if (item.type === "berita") { @@ -105,13 +109,14 @@ export default function ModernNewsNotification({ setHasNewNotifications(false); }; - // Ganti dengan path landing page Anda + // Only show on landing page if (pathname !== '/darmasaba') { return null; } return ( <> + {/* Floating Bell Icon */} {(transitionStyles) => ( + {/* Widget Panel */} {(styles) => ( - Berita & Pengumuman + Berita & Pengumuman setWidgetOpen(false)} @@ -257,6 +264,7 @@ export default function ModernNewsNotification({ )} + {/* Toast Notification */} {(styles) => ( setToastVisible(false)} + onClick={(e) => { + e.stopPropagation(); + setToastVisible(false); + }} size="sm" />
diff --git a/src/app/darmasaba/_com/main-page/kepuasan/index.tsx b/src/app/darmasaba/_com/main-page/kepuasan/index.tsx index 5c05733e..9d96292e 100644 --- a/src/app/darmasaba/_com/main-page/kepuasan/index.tsx +++ b/src/app/darmasaba/_com/main-page/kepuasan/index.tsx @@ -339,7 +339,7 @@ function Kepuasan() { label="Nama" type='text' placeholder="Masukkan nama" - defaultValue={state.create.form.name} + value={state.create.form.name} onChange={(val) => { state.create.form.name = val.currentTarget.value; }} @@ -348,7 +348,7 @@ function Kepuasan() { label="Tanggal" type="date" placeholder="masukkan tanggal" - defaultValue={state.create.form.tanggal} + value={state.create.form.tanggal} onChange={(val) => { state.create.form.tanggal = val.currentTarget.value; }} @@ -357,7 +357,7 @@ function Kepuasan() { key={"jenisKelamin"} label={"Jenis Kelamin"} placeholder={indeksKepuasanState.jenisKelaminResponden.findMany.loading ? 'Memuat...' : 'Pilih jenis kelamin'} - defaultValue={state.create.form.jenisKelaminId || ""} + value={state.create.form.jenisKelaminId || ""} onChange={(val) => { state.create.form.jenisKelaminId = val ?? ""; }} @@ -375,7 +375,7 @@ function Kepuasan() { key={"rating_responden"} label={"Rating"} placeholder={indeksKepuasanState.pilihanRatingResponden.findMany.loading ? 'Memuat...' : 'Pilih rating'} - defaultValue={state.create.form.ratingId || ""} + value={state.create.form.ratingId || ""} onChange={(val) => { state.create.form.ratingId = val ?? ""; }} @@ -393,7 +393,7 @@ function Kepuasan() { key={"kelompokUmur"} label={"Kelompok Umur"} placeholder={indeksKepuasanState.kelompokUmurResponden.findMany.loading ? 'Memuat...' : 'Pilih kelompok umur'} - defaultValue={state.create.form.kelompokUmurId || ""} + value={state.create.form.kelompokUmurId || ""} onChange={(val) => { state.create.form.kelompokUmurId = val ?? ""; }} @@ -611,7 +611,7 @@ function Kepuasan() { label="Nama" type='text' placeholder="masukkan nama" - defaultValue={state.create.form.name} + value={state.create.form.name} onChange={(val) => { state.create.form.name = val.currentTarget.value; }} @@ -620,7 +620,7 @@ function Kepuasan() { label="Tanggal Pengisian" type="date" placeholder="masukkan tanggal" - defaultValue={state.create.form.tanggal} + value={state.create.form.tanggal} onChange={(val) => { state.create.form.tanggal = val.currentTarget.value; }} @@ -629,7 +629,7 @@ function Kepuasan() { key={"jenisKelamin"} label={"Jenis Kelamin"} placeholder={indeksKepuasanState.jenisKelaminResponden.findMany.loading ? 'Memuat...' : 'Pilih jenis kelamin'} - defaultValue={state.create.form.jenisKelaminId || ""} + value={state.create.form.jenisKelaminId || ""} onChange={(val) => { state.create.form.jenisKelaminId = val ?? ""; }} @@ -647,7 +647,7 @@ function Kepuasan() { key={"rating_responden"} label={"Rating"} placeholder={indeksKepuasanState.pilihanRatingResponden.findMany.loading ? 'Memuat...' : 'Pilih rating'} - defaultValue={state.create.form.ratingId || ""} + value={state.create.form.ratingId || ""} onChange={(val) => { state.create.form.ratingId = val ?? ""; }} @@ -665,7 +665,7 @@ function Kepuasan() { key={"kelompokUmur"} label={"Kelompok Umur"} placeholder={indeksKepuasanState.kelompokUmurResponden.findMany.loading ? 'Memuat...' : 'Pilih kelompok umur'} - defaultValue={state.create.form.kelompokUmurId || ""} + value={state.create.form.kelompokUmurId || ""} onChange={(val) => { state.create.form.kelompokUmurId = val ?? ""; }} diff --git a/src/app/darmasaba/_com/main-page/landing-page/index.tsx b/src/app/darmasaba/_com/main-page/landing-page/index.tsx index c8c1d117..d2e40772 100644 --- a/src/app/darmasaba/_com/main-page/landing-page/index.tsx +++ b/src/app/darmasaba/_com/main-page/landing-page/index.tsx @@ -18,14 +18,10 @@ import { } from "@mantine/core"; import { Prisma } from "@prisma/client"; import { IconCalendarTime, IconInfoCircle } from "@tabler/icons-react"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useState } from "react"; import ModuleView from "./ModuleView"; import ProfileView from "./ProfileView"; import SosmedView from "./SosmedView"; -import { useProxy } from "valtio/utils"; -import stateDashboardBerita from "@/app/admin/(dashboard)/_state/desa/berita"; -import stateDesaPengumuman from "@/app/admin/(dashboard)/_state/desa/pengumuman"; -import ModernNewsNotification from "../../ModernNeewsNotification"; const getDayOfWeek = () => { const days = ["Minggu", "Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu"]; @@ -72,57 +68,7 @@ function LandingPage() { >(null); const [isLoading, setIsLoading] = useState(true); - const featured = useProxy(stateDashboardBerita.berita.findFirst); - const loadingFeatured = featured.loading; - const pengumuman = useProxy(stateDesaPengumuman.pengumuman.findFirst); - const loadingPengumuman = pengumuman.loading; - - useEffect(() => { - if (!featured.data && !loadingFeatured) { - stateDashboardBerita.berita.findFirst.load(); - } - }, [featured.data, loadingFeatured]); - - useEffect(() => { - if (!pengumuman.data && !loadingPengumuman) { - stateDesaPengumuman.pengumuman.findFirst.load(); - } - }, [pengumuman.data, loadingPengumuman]); - - // Transform data untuk notification system - const newsData = useMemo(() => { - const items = []; - - if (featured.data) { - items.push({ - id: String(featured.data.id || "berita-1"), - type: "berita" as const, - title: String(featured.data.judul || "Berita Terbaru"), - content: String(featured.data.content || ""), - timestamp: featured.data.createdAt - ? (typeof featured.data.createdAt === 'string' - ? featured.data.createdAt - : new Date(featured.data.createdAt).toISOString()) - : new Date().toISOString(), - }); - } - - if (pengumuman.data) { - items.push({ - id: String(pengumuman.data.id || "pengumuman-1"), - type: "pengumuman" as const, - title: String(pengumuman.data.judul || "Pengumuman Penting"), - content: String(pengumuman.data.content || ""), - timestamp: pengumuman.data.createdAt - ? (typeof pengumuman.data.createdAt === 'string' - ? pengumuman.data.createdAt - : new Date(pengumuman.data.createdAt).toISOString()) - : new Date().toISOString(), - }); - } - - return items; - }, [featured.data, pengumuman.data]); + useEffect(() => { const fetchSocialMedia = async () => { @@ -272,11 +218,6 @@ function LandingPage() { )} - {/* Modern Notification System */} - ); } diff --git a/src/app/darmasaba/page.tsx b/src/app/darmasaba/page.tsx index 5f2d73a1..36e652c3 100644 --- a/src/app/darmasaba/page.tsx +++ b/src/app/darmasaba/page.tsx @@ -14,15 +14,72 @@ import Prestasi from "./_com/main-page/prestasi"; import ScrollToTopButton from "./_com/scrollToTopButton"; import NewsReaderLanding from "./_com/NewsReaderalanding"; +import ModernNewsNotification from "./_com/ModernNeewsNotification"; +import { useMemo } from "react"; +import { useProxy } from "valtio/utils"; +import stateDashboardBerita from "../admin/(dashboard)/_state/desa/berita"; +import stateDesaPengumuman from "../admin/(dashboard)/_state/desa/pengumuman"; +import { useEffect } from "react"; export default function Page() { + const featured = useProxy(stateDashboardBerita.berita.findFirst); + const loadingFeatured = featured.loading; + const pengumuman = useProxy(stateDesaPengumuman.pengumuman.findFirst); + const loadingPengumuman = pengumuman.loading; + + useEffect(() => { + if (!featured.data && !loadingFeatured) { + stateDashboardBerita.berita.findFirst.load(); + } + }, [featured.data, loadingFeatured]); + + useEffect(() => { + if (!pengumuman.data && !loadingPengumuman) { + stateDesaPengumuman.pengumuman.findFirst.load(); + } + }, [pengumuman.data, loadingPengumuman]); + + + const newsData = useMemo(() => { + const items = []; + + if (featured.data) { + items.push({ + id: String(featured.data.id || "berita-1"), + type: "berita" as const, + title: String(featured.data.judul || "Berita Terbaru"), + content: String(featured.data.content || ""), + timestamp: featured.data.createdAt + ? (typeof featured.data.createdAt === 'string' + ? featured.data.createdAt + : new Date(featured.data.createdAt).toISOString()) + : new Date().toISOString(), + }); + } + + if (pengumuman.data) { + items.push({ + id: String(pengumuman.data.id || "pengumuman-1"), + type: "pengumuman" as const, + title: String(pengumuman.data.judul || "Pengumuman Penting"), + content: String(pengumuman.data.content || ""), + timestamp: pengumuman.data.createdAt + ? (typeof pengumuman.data.createdAt === 'string' + ? pengumuman.data.createdAt + : new Date(pengumuman.data.createdAt).toISOString()) + : new Date().toISOString(), + }); + } + + return items; + }, [featured.data, pengumuman.data]); return ( - {/* HAPUS RUNNING TEXT, GANTI DENGAN MODERN NOTIFICATION */} @@ -40,8 +97,11 @@ export default function Page() { {/* Tombol Scroll ke Atas */} - - + + ); } \ No newline at end of file diff --git a/src/schema.ts b/src/schema.ts index ee73d8e9..444bd281 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -1,67 +1,67 @@ -import { SchemaObject } from '@paljs/types' +// import { SchemaObject } from '@paljs/types' -export const schema: SchemaObject = { - models: [ - { - name: 'Layanan', - fields: [ - { - name: 'id', - type: 'String', - isId: true, - unique: false, - defaultValue: 'cuid()', - list: false, - required: true, - kind: 'scalar', - documentation: '', - relationField: false, - }, - { - name: 'name', - type: 'String', - isId: false, - unique: true, - list: false, - required: true, - kind: 'scalar', - documentation: '', - relationField: false, - }, - ], - documentation: '', - }, - { - name: 'Potensi', - fields: [ - { - name: 'id', - type: 'String', - isId: true, - unique: false, - defaultValue: 'cuid()', - list: false, - required: true, - kind: 'scalar', - documentation: '', - relationField: false, - }, - { - name: 'name', - type: 'String', - isId: false, - unique: true, - list: false, - required: true, - kind: 'scalar', - documentation: '', - relationField: false, - }, - ], - documentation: '', - }, - ], - enums: [], - dataSource: { provider: 'postgresql', url: 'env("DATABASE_URL")' }, - generators: [{ name: 'client', provider: 'prisma-client-js' }], -} +// export const schema: SchemaObject = { +// models: [ +// { +// name: 'Layanan', +// fields: [ +// { +// name: 'id', +// type: 'String', +// isId: true, +// unique: false, +// value: 'cuid()', +// list: false, +// required: true, +// kind: 'scalar', +// documentation: '', +// relationField: false, +// }, +// { +// name: 'name', +// type: 'String', +// isId: false, +// unique: true, +// list: false, +// required: true, +// kind: 'scalar', +// documentation: '', +// relationField: false, +// }, +// ], +// documentation: '', +// }, +// { +// name: 'Potensi', +// fields: [ +// { +// name: 'id', +// type: 'String', +// isId: true, +// unique: false, +// value: 'cuid()', +// list: false, +// required: true, +// kind: 'scalar', +// documentation: '', +// relationField: false, +// }, +// { +// name: 'name', +// type: 'String', +// isId: false, +// unique: true, +// list: false, +// required: true, +// kind: 'scalar', +// documentation: '', +// relationField: false, +// }, +// ], +// documentation: '', +// }, +// ], +// enums: [], +// dataSource: { provider: 'postgresql', url: 'env("DATABASE_URL")' }, +// generators: [{ name: 'client', provider: 'prisma-client-js' }], +// }