diff --git a/.claude/ARCHITECTURE.md b/.claude/ARCHITECTURE.md index a91f33d7..8e4dd7c6 100644 --- a/.claude/ARCHITECTURE.md +++ b/.claude/ARCHITECTURE.md @@ -8,7 +8,7 @@ - **UI**: Mantine UI v7-8 - **State**: Jotai (atoms), Valtio (proxies), SWR (data fetching) - **Auth**: iron-session + JWT -- **File storage**: Local uploads + Seafile (self-hosted) +- **File storage**: Local uploads + MinIO (object storage) + Seafile (self-hosted fallback) ## Request Flow @@ -20,14 +20,16 @@ Browser → Next.js middleware (src/middleware.ts) └── _lib/*.ts (domain modules) ``` -The Elysia server is a single entry point with domain-specific modules: `desa.ts`, `kesehatan.ts`, `ekonomi.ts`, `keamanan.ts`, `lingkungan.ts`, `pendidikan.ts`, `kependudukan.ts`, `ppid.ts`, `inovasi.ts`, `auth/`, `user/`, `fileStorage/`. Swagger docs are auto-generated at `/api/docs`. +The Elysia server is a single entry point with domain-specific modules: `desa/`, `kesehatan/`, `ekonomi/`, `keamanan/`, `lingkungan/`, `pendidikan/`, `kependudukan/`, `ppid/`, `inovasi/`, `landing_page/`, `search/`, `auth/`, `user/`, `fileStorage/`. Swagger docs are auto-generated at `/api/docs`. ## Domain Modules Each domain (desa, kesehatan, ekonomi, etc.) has: -- API handler in `src/app/api/[[...slugs]]/_lib/.ts` +- API handler in `src/app/api/[[...slugs]]/_lib//` - Admin CMS pages in `src/app/admin/(dashboard)//` - Public pages in `src/app/darmasaba/(pages)//` +Active domains: `desa`, `ekonomi`, `inovasi`, `keamanan`, `kependudukan`, `kesehatan`, `lingkungan`, `musik`, `pendidikan`, `ppid` — plus `landing_page` and `search` (API-only, no public/admin pages). + ## Key Files | File | Purpose | diff --git a/package.json b/package.json index 6ca5e126..a322ee6f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "desa-darmasaba", - "version": "0.1.46", + "version": "0.1.47", "private": true, "scripts": { "dev": "next dev", diff --git a/src/app/admin/(dashboard)/_state/desa/kegiatanDesa.ts b/src/app/admin/(dashboard)/_state/desa/kegiatanDesa.ts new file mode 100644 index 00000000..2754cfca --- /dev/null +++ b/src/app/admin/(dashboard)/_state/desa/kegiatanDesa.ts @@ -0,0 +1,409 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import ApiFetch from "@/lib/api-fetch"; +import { toast } from "react-toastify"; +import { proxy } from "valtio"; +import { z } from "zod"; + +// ========================================= SCHEMAS ========================================= // + +const templateForm = z.object({ + judul: z.string().min(3, "Judul minimal 3 karakter"), + deskripsiSingkat: z.string().min(3, "Deskripsi singkat minimal 3 karakter"), + deskripsiLengkap: z.string().min(3, "Deskripsi lengkap minimal 3 karakter"), + tanggal: z.string().nonempty("Tanggal harus diisi"), + lokasi: z.string().min(3, "Lokasi minimal 3 karakter"), + partisipan: z.number().optional().default(0), + kategoriKegiatanId: z.string().nonempty("Kategori kegiatan harus dipilih"), + imageId: z.string().optional(), +}); + +const defaultForm = { + judul: "", + deskripsiSingkat: "", + deskripsiLengkap: "", + tanggal: "", + lokasi: "", + partisipan: 0, + kategoriKegiatanId: "", + imageId: "" as string | undefined, +}; + +const templateKategori = z.object({ + nama: z.string().min(1, "Nama kategori harus diisi"), +}); + +const defaultKategori = { + nama: "", +}; + +// ========================================= KEGIATAN DESA ========================================= // + +const kegiatan = proxy({ + create: { + form: { ...defaultForm }, + loading: false, + async create() { + const cek = templateForm.safeParse(kegiatan.create.form); + if (!cek.success) { + const err = `[${cek.error.issues + .map((v) => `${v.path.join(".")}`) + .join("\n")}] required`; + return toast.error(err); + } + + try { + kegiatan.create.loading = true; + const res = await ApiFetch.api.desa["kegiatandesa"]["create"].post( + kegiatan.create.form + ); + if (res.status === 200 && res.data?.success) { + kegiatan.findMany.load(); + toast.success("Kegiatan desa berhasil disimpan!"); + return true; + } + + toast.error(res.data?.message || "Gagal menyimpan kegiatan desa"); + return false; + } catch (error) { + console.error("Error creating kegiatan:", error); + toast.error("Terjadi kesalahan saat menyimpan kegiatan"); + return false; + } finally { + kegiatan.create.loading = false; + } + }, + resetForm() { + kegiatan.create.form = { ...defaultForm }; + }, + }, + + findMany: { + data: null as any[] | null, + page: 1, + totalPages: 1, + loading: false, + search: "", + load: async (page = 1, limit = 10, search = "", kategori = "") => { + const startTime = Date.now(); + kegiatan.findMany.loading = true; + kegiatan.findMany.page = page; + kegiatan.findMany.search = search; + + try { + const query: any = { page, limit }; + if (search) query.search = search; + if (kategori) query.kategori = kategori; + + const res = await ApiFetch.api.desa["kegiatandesa"]["find-many"].get({ query }); + + if (res.status === 200 && res.data?.success) { + kegiatan.findMany.data = res.data.data ?? []; + kegiatan.findMany.totalPages = res.data.totalPages ?? 1; + } else { + kegiatan.findMany.data = []; + kegiatan.findMany.totalPages = 1; + } + } catch (err) { + console.error("Gagal fetch kegiatan paginated:", err); + kegiatan.findMany.data = []; + kegiatan.findMany.totalPages = 1; + } finally { + const elapsed = Date.now() - startTime; + const minDelay = 300; + const delay = elapsed < minDelay ? minDelay - elapsed : 0; + + setTimeout(() => { + kegiatan.findMany.loading = false; + }, delay); + } + }, + }, + + findUnique: { + data: null as any | null, + loading: false, + async load(id: string) { + if (!id) return; + this.loading = true; + try { + const res = await fetch(`/api/desa/kegiatandesa/${id}`); // Assuming unique endpoint follows standard + if (res.ok) { + const result = await res.json(); + kegiatan.findUnique.data = result.data ?? null; + } + } catch (error) { + console.error("Error fetching unique kegiatan:", error); + } finally { + this.loading = false; + } + }, + }, + + delete: { + loading: false, + async byId(id: string) { + if (!id) return toast.warn("ID tidak valid"); + + try { + kegiatan.delete.loading = true; + const response = await fetch(`/api/desa/kegiatandesa/del/${id}`, { + method: "DELETE", + }); + + const result = await response.json(); + if (response.ok && result?.success) { + toast.success(result.message || "Kegiatan berhasil dihapus"); + await kegiatan.findMany.load(); + } else { + toast.error(result?.message || "Gagal menghapus kegiatan"); + } + } catch (error) { + console.error("Gagal delete:", error); + toast.error("Terjadi kesalahan saat menghapus kegiatan"); + } finally { + kegiatan.delete.loading = false; + } + }, + }, + + edit: { + id: "", + form: { ...defaultForm }, + loading: false, + + async load(id: string) { + if (!id) return null; + try { + kegiatan.edit.loading = true; + const response = await fetch(`/api/desa/kegiatandesa/${id}`); + const result = await response.json(); + + if (result?.success) { + const data = result.data; + this.id = data.id; + this.form = { + judul: data.judul, + deskripsiSingkat: data.deskripsiSingkat, + deskripsiLengkap: data.deskripsiLengkap, + tanggal: data.tanggal ? new Date(data.tanggal).toISOString().split('T')[0] : "", + lokasi: data.lokasi, + partisipan: data.partisipan || 0, + kategoriKegiatanId: data.kategoriKegiatanId || "", + imageId: data.imageId || undefined, + }; + return data; + } + } catch (error) { + console.error("Error loading kegiatan for edit:", error); + } finally { + kegiatan.edit.loading = false; + } + return null; + }, + + async update() { + const cek = templateForm.safeParse(kegiatan.edit.form); + if (!cek.success) { + const err = `[${cek.error.issues + .map((v) => `${v.path.join(".")}`) + .join("\n")}] required`; + toast.error(err); + return false; + } + + try { + kegiatan.edit.loading = true; + const response = await fetch(`/api/desa/kegiatandesa/${this.id}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(kegiatan.edit.form), + }); + + const result = await response.json(); + if (result.success) { + toast.success("Berhasil update kegiatan"); + await kegiatan.findMany.load(); + return true; + } + throw new Error(result.message || "Gagal update kegiatan"); + } catch (error) { + toast.error(error instanceof Error ? error.message : "Terjadi kesalahan saat update"); + return false; + } finally { + kegiatan.edit.loading = false; + } + }, + + reset() { + kegiatan.edit.id = ""; + kegiatan.edit.form = { ...defaultForm }; + }, + }, +}); + +// ========================================= KATEGORI KEGIATAN ========================================= // + +const kategoriKegiatan = proxy({ + create: { + form: { ...defaultKategori }, + loading: false, + async create() { + const cek = templateKategori.safeParse(kategoriKegiatan.create.form); + if (!cek.success) { + return toast.error("Nama kategori harus diisi"); + } + + try { + kategoriKegiatan.create.loading = true; + const res = await ApiFetch.api.desa["kategorikegiatan"]["create"].post( + kategoriKegiatan.create.form + ); + if (res.status === 200 && res.data?.success) { + kategoriKegiatan.findMany.load(); + toast.success("Kategori kegiatan berhasil dibuat"); + return true; + } + toast.error(res.data?.message || "Gagal membuat kategori"); + return false; + } catch (error) { + console.error(error); + toast.error("Terjadi kesalahan"); + return false; + } finally { + kategoriKegiatan.create.loading = false; + } + }, + resetForm() { + kategoriKegiatan.create.form = { ...defaultKategori }; + }, + }, + + findMany: { + data: [] as any[], + page: 1, + totalPages: 1, + loading: false, + search: "", + load: async (page = 1, limit = 10, search = "") => { + kategoriKegiatan.findMany.loading = true; + kategoriKegiatan.findMany.page = page; + kategoriKegiatan.findMany.search = search; + + try { + const query: any = { page, limit }; + if (search) query.search = search; + + const res = await ApiFetch.api.desa["kategorikegiatan"]["findMany"].get({ query }); + + if (res.status === 200 && res.data?.success) { + kategoriKegiatan.findMany.data = res.data.data ?? []; + kategoriKegiatan.findMany.totalPages = res.data.totalPages ?? 1; + } else { + kategoriKegiatan.findMany.data = []; + } + } catch (err) { + console.error("Gagal fetch kategori kegiatan:", err); + } finally { + kategoriKegiatan.findMany.loading = false; + } + }, + }, + + findUnique: { + data: null as any | null, + loading: false, + async load(id: string) { + try { + const res = await fetch(`/api/desa/kategorikegiatan/${id}`); + if (res.ok) { + const result = await res.json(); + kategoriKegiatan.findUnique.data = result.data ?? null; + } + } catch (error) { + console.error(error); + } + }, + }, + + delete: { + loading: false, + async byId(id: string) { + if (!id) return; + try { + kategoriKegiatan.delete.loading = true; + const res = await fetch(`/api/desa/kategorikegiatan/del/${id}`, { method: "DELETE" }); + const result = await res.json(); + if (result.success) { + toast.success(result.message); + kategoriKegiatan.findMany.load(); + } else { + toast.error(result.message); + } + } catch (error) { + console.error(error); + } finally { + kategoriKegiatan.delete.loading = false; + } + }, + }, + + update: { + id: "", + form: { ...defaultKategori }, + loading: false, + async load(id: string) { + if (!id) return; + try { + this.loading = true; + const res = await fetch(`/api/desa/kategorikegiatan/${id}`); + const result = await res.json(); + if (result.success) { + this.id = result.data.id; + this.form = { nama: result.data.nama }; + } + } catch (error) { + console.error(error); + } finally { + this.loading = false; + } + }, + async update() { + const cek = templateKategori.safeParse(kategoriKegiatan.update.form); + if (!cek.success) return toast.error("Nama kategori harus diisi"); + + try { + this.loading = true; + const res = await fetch(`/api/desa/kategorikegiatan/${this.id}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(this.form), + }); + const result = await res.json(); + if (result.success) { + toast.success("Berhasil update kategori"); + kategoriKegiatan.findMany.load(); + return true; + } + toast.error(result.message); + } catch (error) { + console.error(error); + } finally { + this.loading = false; + } + return false; + }, + reset() { + this.id = ""; + this.form = { ...defaultKategori }; + }, + }, +}); + +// ========================================= GLOBAL STATE ========================================= // + +const stateDashboardKegiatan = proxy({ + kegiatan, + kategoriKegiatan, +}); + +export default stateDashboardKegiatan; diff --git a/src/app/admin/(dashboard)/desa/kegiatan-desa/_com/layoutTabs.tsx b/src/app/admin/(dashboard)/desa/kegiatan-desa/_com/layoutTabs.tsx new file mode 100644 index 00000000..6a2bc760 --- /dev/null +++ b/src/app/admin/(dashboard)/desa/kegiatan-desa/_com/layoutTabs.tsx @@ -0,0 +1,106 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +'use client' +import colors from '@/con/colors'; +import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core'; +import { IconCalendarEvent, IconCategory } from '@tabler/icons-react'; +import { usePathname, useRouter } from 'next/navigation'; +import React, { useEffect, useState } from 'react'; + +function LayoutTabsKegiatanDesa({ children }: { children: React.ReactNode }) { + const router = useRouter(); + const pathname = usePathname(); + + const tabs = [ + { + label: "List Kegiatan", + value: "list_kegiatan", + href: "/admin/desa/kegiatan-desa/list-kegiatan-desa", + icon: + }, + { + label: "Kategori Kegiatan", + value: "kategori_kegiatan", + href: "/admin/desa/kegiatan-desa/kategori-kegiatan-desa", + icon: + }, + ]; + + const currentTab = tabs.find(tab => tab.href === pathname); + const [activeTab, setActiveTab] = useState(currentTab?.value || tabs[0].value); + + const handleTabChange = (value: string | null) => { + const tab = tabs.find(t => t.value === value); + if (tab) { + router.push(tab.href); + } + setActiveTab(value); + }; + + useEffect(() => { + const match = tabs.find(tab => tab.href === pathname); + if (match) { + setActiveTab(match.value); + } + }, [pathname]); + + return ( + + Kegiatan Desa + + + + {tabs.map((tab, i) => ( + + {tab.label} + + ))} + + + + {tabs.map((tab, i) => ( + + <>{children} + + ))} + + + ); +} + +export default LayoutTabsKegiatanDesa; diff --git a/src/app/admin/(dashboard)/desa/kegiatan-desa/kategori-kegiatan-desa/[id]/page.tsx b/src/app/admin/(dashboard)/desa/kegiatan-desa/kategori-kegiatan-desa/[id]/page.tsx new file mode 100644 index 00000000..47681a6e --- /dev/null +++ b/src/app/admin/(dashboard)/desa/kegiatan-desa/kategori-kegiatan-desa/[id]/page.tsx @@ -0,0 +1,137 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +'use client' +import stateDashboardKegiatan from '@/app/admin/(dashboard)/_state/desa/kegiatanDesa'; +import colors from '@/con/colors'; +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 EditKategoriKegiatan() { + const editState = useProxy(stateDashboardKegiatan.kategoriKegiatan); + const router = useRouter(); + const params = useParams(); + const [isSubmitting, setIsSubmitting] = useState(false); + + const [formData, setFormData] = useState({ nama: '' }); + const [originalData, setOriginalData] = useState({ nama: '' }); + + const isFormValid = () => formData.nama?.trim() !== ''; + + useEffect(() => { + const loadKategori = async () => { + const id = params?.id as string; + if (!id) return; + + try { + await stateDashboardKegiatan.kategoriKegiatan.update.load(id); + const nama = stateDashboardKegiatan.kategoriKegiatan.update.form.nama || ''; + setFormData({ nama }); + setOriginalData({ nama }); + } catch (error) { + console.error('Error loading kategori kegiatan:', error); + toast.error('Gagal memuat data kategori kegiatan'); + } + }; + + loadKategori(); + }, [params?.id]); + + const handleResetForm = () => { + setFormData({ nama: originalData.nama }); + toast.info('Form dikembalikan ke data awal'); + }; + + const handleSubmit = async () => { + if (!formData.nama?.trim()) { + toast.error('Nama kategori kegiatan wajib diisi'); + return; + } + + try { + setIsSubmitting(true); + + editState.update.form = { + ...editState.update.form, + nama: formData.nama, + }; + + const success = await editState.update.update(); + if (success) { + router.push('/admin/desa/kegiatan-desa/kategori-kegiatan-desa'); + } + } catch (error) { + console.error('Error updating kategori kegiatan:', error); + toast.error('Terjadi kesalahan saat memperbarui kategori kegiatan'); + } finally { + setIsSubmitting(false); + } + }; + + return ( + + + + + Edit Kategori Kegiatan + + + + + + setFormData((prev) => ({ ...prev, nama: e.target.value }))} + required + /> + + + + + + + + + ); +} + +export default EditKategoriKegiatan; diff --git a/src/app/admin/(dashboard)/desa/kegiatan-desa/kategori-kegiatan-desa/create/page.tsx b/src/app/admin/(dashboard)/desa/kegiatan-desa/kategori-kegiatan-desa/create/page.tsx new file mode 100644 index 00000000..933d22ac --- /dev/null +++ b/src/app/admin/(dashboard)/desa/kegiatan-desa/kategori-kegiatan-desa/create/page.tsx @@ -0,0 +1,107 @@ +'use client' +import stateDashboardKegiatan from '@/app/admin/(dashboard)/_state/desa/kegiatanDesa'; +import colors from '@/con/colors'; +import { + Box, + Button, + Group, + Loader, + Paper, + Stack, + TextInput, + Title, +} from '@mantine/core'; +import { IconArrowBack } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; + +function CreateKategoriKegiatan() { + const createState = useProxy(stateDashboardKegiatan.kategoriKegiatan); + const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); + + const isFormValid = () => createState.create.form.nama?.trim() !== ''; + + const resetForm = () => { + createState.create.resetForm(); + }; + + const handleSubmit = async () => { + if (!createState.create.form.nama?.trim()) { + toast.error('Nama kategori kegiatan wajib diisi'); + return; + } + + setIsSubmitting(true); + try { + const success = await createState.create.create(); + if (success) { + resetForm(); + router.push('/admin/desa/kegiatan-desa/kategori-kegiatan-desa'); + } + } catch (error) { + console.error('Error creating kategori kegiatan:', error); + toast.error('Gagal menambahkan kategori kegiatan'); + } finally { + setIsSubmitting(false); + } + }; + + return ( + + + + + Tambah Kategori Kegiatan + + + + + + (createState.create.form.nama = e.target.value)} + required + /> + + + + + + + + + ); +} + +export default CreateKategoriKegiatan; diff --git a/src/app/admin/(dashboard)/desa/kegiatan-desa/kategori-kegiatan-desa/page.tsx b/src/app/admin/(dashboard)/desa/kegiatan-desa/kategori-kegiatan-desa/page.tsx new file mode 100644 index 00000000..4b2cae95 --- /dev/null +++ b/src/app/admin/(dashboard)/desa/kegiatan-desa/kategori-kegiatan-desa/page.tsx @@ -0,0 +1,239 @@ +/* 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 { useDebouncedValue } from '@mantine/hooks'; +import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import { useProxy } from 'valtio/utils'; +import HeaderSearch from '../../../_com/header'; +import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; +import stateDashboardKegiatan from '../../../_state/desa/kegiatanDesa'; + +function KategoriKegiatanDesa() { + const [search, setSearch] = useState(''); + return ( + + } + value={search} + onChange={(e) => setSearch(e.currentTarget.value)} + /> + + + ); +} + +function ListKategoriKegiatan({ search }: { search: string }) { + const listDataState = useProxy(stateDashboardKegiatan.kategoriKegiatan); + const router = useRouter(); + const [modalHapus, setModalHapus] = useState(false); + const [selectedId, setSelectedId] = useState(null); + const [debouncedSearch] = useDebouncedValue(search, 1000); + + const { data, loading, load, page, totalPages } = listDataState.findMany; + + useEffect(() => { + load(page, 10, debouncedSearch); + }, [page, debouncedSearch]); + + const handleDelete = () => { + if (selectedId) { + listDataState.delete.byId(selectedId); + setModalHapus(false); + setSelectedId(null); + } + }; + + const filteredData = data || []; + + if (loading || !data) { + return ( + + + + ); + } + + return ( + + + + + Daftar Kategori Kegiatan + + + + + {/* Desktop Table */} + + + + + + Kategori + + + Edit + + + Hapus + + + + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + {item.nama} + + + + + + + + + + )) + ) : ( + + +
+ + Tidak ada data kategori kegiatan yang cocok + +
+
+
+ )} +
+
+
+ + {/* Mobile Cards */} + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + Kategori + + {item.nama} + + + + + + + + )) + ) : ( +
+ + Tidak ada data kategori kegiatan yang cocok + +
+ )} +
+
+ +
+ { + load(newPage, 10, search); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }} + total={totalPages} + color="blue" + radius="md" + /> +
+ + setModalHapus(false)} + onConfirm={handleDelete} + text="Apakah anda yakin ingin menghapus kategori kegiatan ini?" + /> +
+ ); +} + +export default KategoriKegiatanDesa; diff --git a/src/app/admin/(dashboard)/desa/kegiatan-desa/layout.tsx b/src/app/admin/(dashboard)/desa/kegiatan-desa/layout.tsx new file mode 100644 index 00000000..7a4e1875 --- /dev/null +++ b/src/app/admin/(dashboard)/desa/kegiatan-desa/layout.tsx @@ -0,0 +1,28 @@ +'use client' +import React from 'react'; +import LayoutTabsKegiatanDesa from './_com/layoutTabs'; +import { usePathname } from 'next/navigation'; +import { Box } from '@mantine/core'; + +function Layout({ children }: { children: React.ReactNode }) { + const pathname = usePathname(); + + const segments = pathname.split('/').filter(Boolean); + const isDetailPage = segments.length >= 5; + + if (isDetailPage) { + return ( + + {children} + + ); + } + + return ( + + {children} + + ); +} + +export default Layout; diff --git a/src/app/admin/(dashboard)/desa/kegiatan-desa/list-kegiatan-desa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/kegiatan-desa/list-kegiatan-desa/[id]/edit/page.tsx new file mode 100644 index 00000000..71f2117b --- /dev/null +++ b/src/app/admin/(dashboard)/desa/kegiatan-desa/list-kegiatan-desa/[id]/edit/page.tsx @@ -0,0 +1,335 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +'use client' +import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; +import stateDashboardKegiatan from '@/app/admin/(dashboard)/_state/desa/kegiatanDesa'; +import colors from '@/con/colors'; +import ApiFetch from '@/lib/api-fetch'; +import { + ActionIcon, + Box, + Button, + Group, + Image, + Loader, + NumberInput, + Paper, + Select, + Stack, + Text, + Textarea, + 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'; +import { useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; + +const isHtmlEmpty = (html: string) => html.replace(/<[^>]*>/g, '').trim() === ''; + +interface KegiatanData { + id: string; + judul: string; + deskripsiSingkat: string; + deskripsiLengkap: string; + tanggal: string; + lokasi: string; + partisipan: number; + kategoriKegiatanId: string | null; + imageId: string | null; + image?: { link: string } | null; +} + +function EditKegiatanDesa() { + const kegiatanState = useProxy(stateDashboardKegiatan); + const router = useRouter(); + const params = useParams(); + + const [previewImage, setPreviewImage] = useState(null); + const [file, setFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + const emptyForm = { + judul: '', + deskripsiSingkat: '', + deskripsiLengkap: '', + tanggal: '', + lokasi: '', + partisipan: 0, + kategoriKegiatanId: '', + imageId: '', + }; + + const [formData, setFormData] = useState(emptyForm); + const [originalData, setOriginalData] = useState({ ...emptyForm, imageUrl: '' }); + + const isFormValid = () => + formData.judul.trim() !== '' && + formData.deskripsiSingkat.trim() !== '' && + !isHtmlEmpty(formData.deskripsiLengkap) && + formData.tanggal !== '' && + formData.lokasi.trim() !== '' && + formData.kategoriKegiatanId !== ''; + + useEffect(() => { + kegiatanState.kategoriKegiatan.findMany.load(); + + const load = async () => { + const id = params?.id as string; + if (!id) return; + + try { + const data = await stateDashboardKegiatan.kegiatan.edit.load(id) as KegiatanData | null; + if (data) { + const tanggal = data.tanggal + ? new Date(data.tanggal).toISOString().split('T')[0] + : ''; + + const form = { + judul: data.judul || '', + deskripsiSingkat: data.deskripsiSingkat || '', + deskripsiLengkap: data.deskripsiLengkap || '', + tanggal, + lokasi: data.lokasi || '', + partisipan: data.partisipan || 0, + kategoriKegiatanId: data.kategoriKegiatanId || '', + imageId: data.imageId || '', + }; + + setFormData(form); + setOriginalData({ ...form, imageUrl: data.image?.link || '' }); + if (data.image?.link) setPreviewImage(data.image.link); + } + } catch (error) { + console.error('Error loading kegiatan:', error); + toast.error('Gagal memuat data kegiatan'); + } + }; + + load(); + }, [params?.id]); + + const handleChange = (field: string, value: string | number) => { + setFormData((prev) => ({ ...prev, [field]: value })); + }; + + const handleSubmit = async () => { + if (!formData.judul.trim()) return toast.error('Judul wajib diisi'); + if (!formData.deskripsiSingkat.trim()) return toast.error('Deskripsi singkat wajib diisi'); + if (isHtmlEmpty(formData.deskripsiLengkap)) return toast.error('Deskripsi lengkap wajib diisi'); + if (!formData.tanggal) return toast.error('Tanggal wajib diisi'); + if (!formData.lokasi.trim()) return toast.error('Lokasi wajib diisi'); + if (!formData.kategoriKegiatanId) return toast.error('Kategori wajib dipilih'); + + try { + setIsSubmitting(true); + + kegiatanState.kegiatan.edit.form = { + ...kegiatanState.kegiatan.edit.form, + ...formData, + }; + + if (file) { + 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'); + kegiatanState.kegiatan.edit.form.imageId = uploaded.id; + } + + const success = await kegiatanState.kegiatan.edit.update(); + if (success) { + router.push('/admin/desa/kegiatan-desa/list-kegiatan-desa'); + } + } catch (error) { + console.error('Error updating kegiatan:', error); + toast.error('Terjadi kesalahan saat memperbarui kegiatan'); + } finally { + setIsSubmitting(false); + } + }; + + const handleReset = () => { + setFormData({ + judul: originalData.judul, + deskripsiSingkat: originalData.deskripsiSingkat, + deskripsiLengkap: originalData.deskripsiLengkap, + tanggal: originalData.tanggal, + lokasi: originalData.lokasi, + partisipan: originalData.partisipan, + kategoriKegiatanId: originalData.kategoriKegiatanId, + imageId: originalData.imageId, + }); + setPreviewImage(originalData.imageUrl || null); + setFile(null); + toast.info('Form dikembalikan ke data awal'); + }; + + return ( + + + + Edit Kegiatan Desa + + + + + handleChange('judul', e.target.value)} + required + /> + +