diff --git a/prisma/data/desa/layanan/pelayanaPendudukNonPermanen.json b/prisma/data/desa/layanan/pelayanaPendudukNonPermanen.json new file mode 100644 index 00000000..2259bea4 --- /dev/null +++ b/prisma/data/desa/layanan/pelayanaPendudukNonPermanen.json @@ -0,0 +1,7 @@ +[ + { + "id": "1", + "name": "Pelayanan Penduduk Non-Permanent", + "deskripsi": "

Surat Keterangan Penduduk Non-Permanent adalah dokumen yang dikeluarkan oleh pihak berwenang untuk memberikan keterangan bahwa seseorang atau kelompok orang memiliki status penduduk non-permanent di suatu wilayah. Dokumen ini biasanya digunakan untuk keperluan administratif atau legal, seperti mendapatkan akses ke layanan kesehatan, pendidikan, atau pelayanan publik lainnya.

" + } +] \ No newline at end of file diff --git a/prisma/data/desa/layanan/pelayananPerizinanBerusaha.json b/prisma/data/desa/layanan/pelayananPerizinanBerusaha.json index 1e7d9025..8df36cf0 100644 --- a/prisma/data/desa/layanan/pelayananPerizinanBerusaha.json +++ b/prisma/data/desa/layanan/pelayananPerizinanBerusaha.json @@ -1,8 +1,8 @@ [ { "id": "1", - "name": "I.B Surya Prabhawa Manuaba, S.H., M.H.", - "deskripsi": "

I.B Surya Prabhawa Manuaba, S.H., M.H., adalah Perbekel Darmasaba periode 2021-2027, seorang advokat, pendiri Mantra Legal Consultants & Advocates, serta aktif di bidang musik dan akademis. Dia menempuh pendidikan hukum di Universitas Udayana dan Universitas Mahasaraswati Denpasar, serta memiliki pengalaman luas di berbagai organisasi dan kepemimpinan.

", + "name": "Pelayanan Perizinan Berusaha Berbasis Risiko Melalui Sistem ONLINE SINGLE SUBMISSION (OSS)", + "deskripsi": "

Penyelenggaraan Perizinan Berusaha Berbasis Risiko melalui Sistem Online Single Submission (OSS) merupakan pelaksanaan Undang-Undang Nomor 11 Tahun 2020 Tentang Cipta Kerja. OSS Berbasis Risiko wajib digunakan oleh Pelaku Usaha, Kementerian/Lembaga, Pemerintah Daerah, Administrator Kawasan Ekonomi Khusus (KEK), dan Badan Pengusahaan Kawasan Perdagangan Bebas Pelabuhan Bebas (KPBPB).Berdasarkan Peraturan Pemerintah Nomor 5 Tahun 2021 terdapat 1.702 kegiatan usaha yang terdiri atas 1.349 Klasifikasi Baku Lapangan Usaha Indonesia (KBLI) yang sudah diimplementasikan dalam Sistem OSS Berbasis Risiko.

", "link" : "https://oss.go.id/" } ] \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e6dacf37..374834dd 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -69,6 +69,8 @@ model FileStorage { GalleryFoto GalleryFoto[] PelayananSuratKeterangan PelayananSuratKeterangan[] + + Penghargaan Penghargaan[] } //========================================= MENU PPID ========================================= // @@ -408,6 +410,20 @@ model PelayananPendudukNonPermanen { isActive Boolean @default(true) } +// ========================================= PENGHARGAAN ========================================= // +model Penghargaan { + id String @id @default(cuid()) + name String + juara String + deskripsi String @db.Text + image FileStorage @relation(fields: [imageId], references: [id]) + imageId String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime @default(now()) + isActive Boolean @default(true) +} + // ========================================= MENU KESEHATAN ========================================= // // ========================================= DATA KESEHATAN WARGA ========================================= // diff --git a/prisma/seed.ts b/prisma/seed.ts index 361c7ee2..82a63aec 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -11,6 +11,7 @@ import profilePPID from "./data/ppid/profile-ppid/profilePPid.json"; import visiMisiPPID from "./data/ppid/visi-misi-ppid/visimisiPPID.json"; import strukturPPID from "./data/ppid/struktur-ppid/strukturPPID.json"; import pelayananPerizinanBerusaha from "./data/desa/layanan/pelayananPerizinanBerusaha.json"; +import pelayananPendudukNonPermanen from "./data/desa/layanan/pelayanaPendudukNonPermanen.json"; (async () => { for (const l of layanan) { @@ -50,6 +51,25 @@ import pelayananPerizinanBerusaha from "./data/desa/layanan/pelayananPerizinanBe console.log("pelayanan perizinan berusaha success ..."); + for (const l of pelayananPendudukNonPermanen) { + await prisma.pelayananPendudukNonPermanen.upsert({ + where: { + id: l.id, + }, + update: { + name: l.name, + deskripsi: l.deskripsi, + }, + create: { + id: l.id, + name: l.name, + deskripsi: l.deskripsi, + }, + }); + } + + console.log("pelayanan perizinan berusaha success ..."); + for (const s of strukturPPID) { await prisma.strukturPPID.upsert({ where: { diff --git a/src/app/admin/(dashboard)/_state/desa/layananDesa.ts b/src/app/admin/(dashboard)/_state/desa/layananDesa.ts index 9d48cfc4..558d4672 100644 --- a/src/app/admin/(dashboard)/_state/desa/layananDesa.ts +++ b/src/app/admin/(dashboard)/_state/desa/layananDesa.ts @@ -50,6 +50,25 @@ const pelayananPerizinanBerusahaForm = { link: "", }; +const templatePelayananPendudukNonPermanen = z.object({ + name: z.string().min(3, "Nama minimal 3 karakter"), + deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"), +}); + +type pelayananPendudukNonPermanenForm = + Prisma.PelayananPendudukNonPermanenGetPayload<{ + select: { + id: true; + name: true; + deskripsi: true; + }; + }>; + +const pelayananPendudukNonPermanenForm = { + name: "", + deskripsi: "", +}; + const suratKeterangan = proxy({ create: { @@ -572,10 +591,128 @@ const pelayananPerizinanBerusaha = proxy({ }, }); +const pelayananPendudukNonPermanen = proxy({ + findById: { + data: null as pelayananPendudukNonPermanenForm | null, + loading: false, + initialize() { + pelayananPendudukNonPermanen.findById.data = { + id: "", + name: "", + deskripsi: "", + } as pelayananPendudukNonPermanenForm; + }, + async load(id: string) { + try { + pelayananPendudukNonPermanen.findById.loading = true; + const res = await fetch( + `/api/desa/layanan/pelayananpenduduknonpermanen/${id}` + ); + if (res.ok) { + const data = await res.json(); + pelayananPendudukNonPermanen.findById.data = data.data ?? null; + } else { + console.error( + "Failed to fetch pelayanan penduduk non permanen:", + res.statusText + ); + pelayananPendudukNonPermanen.findById.data = null; + } + } catch (error) { + console.error("Error fetching pelayanan penduduk non permanen:", error); + pelayananPendudukNonPermanen.findById.data = null; + } + }, + }, + update: { + id: "", + form: { ...pelayananPendudukNonPermanenForm }, + loading: false, + async load(id: string) { + if (!id) { + toast.warn("ID tidak boleh kosong"); + return null; + } + try { + const response = await fetch( + `/api/desa/layanan/pelayananpenduduknonpermanen/${id}`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + } + ); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const result = await response.json(); + if (result?.success) { + const data = result.data; + pelayananPendudukNonPermanen.update.id = data.id; + pelayananPendudukNonPermanen.update.form = { + name: data.name, + deskripsi: data.deskripsi, + }; + return data; + } else { + throw new Error(result.message || "Gagal memuat data"); + } + } catch (error) { + console.error("Error fetching pelayanan penduduk non permanen:", error); + toast.error( + error instanceof Error + ? error.message + : "Gagal memuat data" + ); + return null; + } + }, + async update(data: pelayananPendudukNonPermanenForm) { + const cek = templatePelayananPendudukNonPermanen.safeParse(data); + if (!cek.success) { + const errors = cek.error.issues + .map((issue) => `${issue.path.join(".")}: ${issue.message}`) + .join(", "); + toast.error(`Form tidak valid: ${errors}`); + return; + } + + try { + pelayananPendudukNonPermanen.update.loading = true; + const res = await fetch( + `/api/desa/layanan/pelayananpenduduknonpermanen/${data.id}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + } + ); + if (res.ok) { + toast.success("Pelayanan penduduk non permanen berhasil diupdate"); + await pelayananPendudukNonPermanen.findById.load(data.id); + } else { + toast.error("Gagal mengupdate pelayanan penduduk non permanen"); + } + } catch (error) { + console.error("Error updating pelayanan penduduk non permanen:", error); + toast.error( + "Terjadi kesalahan saat mengupdate pelayanan penduduk non permanen" + ); + } finally { + pelayananPendudukNonPermanen.update.loading = false; + } + }, + }, +}); + const stateLayananDesa = proxy({ suratKeterangan, pelayananPerizinanBerusaha, pelayananTelunjukSaktiDesa, + pelayananPendudukNonPermanen, }); export default stateLayananDesa; diff --git a/src/app/admin/(dashboard)/_state/desa/penghargaan.ts b/src/app/admin/(dashboard)/_state/desa/penghargaan.ts new file mode 100644 index 00000000..2545ae1a --- /dev/null +++ b/src/app/admin/(dashboard)/_state/desa/penghargaan.ts @@ -0,0 +1,221 @@ +import ApiFetch from "@/lib/api-fetch"; +import { Prisma } from "@prisma/client"; +import { toast } from "react-toastify"; +import { proxy } from "valtio"; +import { z } from "zod"; + +const templateForm = z.object({ + name: z.string().min(1).max(50), + juara: z.string().min(1).max(50), + deskripsi: z.string().min(1).max(5000), + imageId: z.string().min(1).max(50), +}); + +const defaultForm = { + name: "", + juara: "", + deskripsi: "", + imageId: "", +}; + +const penghargaanState = proxy({ + create: { + form: { ...defaultForm }, + loading: false, + async create() { + const cek = templateForm.safeParse(penghargaanState.create.form); + if (!cek.success) { + const err = `[${cek.error.issues + .map((v) => `${v.path.join(".")}`) + .join("\n")}] required`; + return toast.error(err); + } + + try { + penghargaanState.create.loading = true; + const res = await ApiFetch.api.desa.penghargaan["create"].post( + penghargaanState.create.form + ); + if (res.status === 200) { + penghargaanState.findMany.load(); + return toast.success("success create"); + } + console.log(res); + return toast.error("failed create"); + } catch (error) { + console.log((error as Error).message); + } finally { + penghargaanState.create.loading = false; + } + }, + }, + findMany: { + data: null as + | Prisma.PenghargaanGetPayload<{ + include: { + image: true; + }; + }>[] + | null, + async load() { + const res = await ApiFetch.api.desa.penghargaan["find-many"].get(); + if (res.status === 200) { + penghargaanState.findMany.data = res.data?.data ?? []; + } + }, + }, + findUnique: { + data: null as Prisma.PenghargaanGetPayload<{ + include: { + image: true; + }; + }> | null, + async load(id: string) { + try { + const res = await fetch(`/api/desa/penghargaan/${id}`); + if (res.ok) { + const data = await res.json(); + penghargaanState.findUnique.data = data.data ?? null; + } else { + console.error("Failed to fetch data", res.status, res.statusText); + penghargaanState.findUnique.data = null; + } + } catch (error) { + console.error("Error loading penghargaan:", error); + penghargaanState.findUnique.data = null; + } + }, + }, + delete: { + loading: false, + async byId(id: string) { + if (!id) return toast.warn("ID tidak valid"); + + try { + penghargaanState.delete.loading = true; + const response = await fetch(`/api/desa/penghargaan/del/${id}`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + }); + const result = await response.json(); + + if (response.ok) { + toast.success(result.message || "Penghargaan berhasil dihapus"); + await penghargaanState.findMany.load(); + } else { + toast.error(result?.message || "Gagal menghapus penghargaan"); + } + } catch (error) { + console.log((error as Error).message); + toast.error("Terjadi kesalahan saat menghapus penghargaan"); + } finally { + penghargaanState.delete.loading = false; + } + }, + }, + edit: { + id: "", + form: { ...defaultForm }, + loading: false, + + async load(id: string) { + if (!id) { + toast.warn("ID tidak valid"); + return null; + } + + try { + const response = await fetch(`/api/desa/penghargaan/${id}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const result = await response.json(); + if (result?.success) { + const data = result.data; + this.id = data.id; + this.form = { + name: data.name, + juara: data.juara, + deskripsi: data.deskripsi, + imageId: data.imageId, + }; + return data; + } else { + throw new Error(result?.message || "Gagal memuat data"); + } + } catch (error) { + console.error("Error loading penghargaan:", error); + toast.error( + error instanceof Error ? error.message : "Gagal memuat data" + ); + return null; + } + }, + async update() { + const cek = templateForm.safeParse(penghargaanState.edit.form); + if (!cek.success) { + const err = `[${cek.error.issues + .map((v) => `${v.path.join(".")}`) + .join("\n")}] required`; + toast.error(err); + return false; + } + + try { + penghargaanState.edit.loading = true; + const response = await fetch( + `/api/desa/penghargaan/${penghargaanState.edit.id}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: this.form.name, + juara: this.form.juara, + deskripsi: this.form.deskripsi, + imageId: this.form.imageId, + }), + } + ); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error( + errorData.message || `HTTP error! status: ${response.status}` + ); + } + const result = await response.json(); + if (result.success) { + toast.success("Berhasil update penghargaan"); + await penghargaanState.findMany.load(); + return true; + } else { + throw new Error(result?.message || "Gagal update penghargaan"); + } + } catch (error) { + console.error("Error updating penghargaan:", error); + toast.error( + error instanceof Error + ? error.message + : "Terjadi kesalahan saat update penghargaan" + ); + return false; + } finally { + penghargaanState.edit.loading = false; + } + }, + reset() { + penghargaanState.edit.id = ""; + penghargaanState.edit.form = { ...defaultForm }; + }, + }, +}); +export default penghargaanState; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/desa/layanan/pelayanan_penduduk_non_permanent/edit/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_penduduk_non_permanent/edit/page.tsx new file mode 100644 index 00000000..80841411 --- /dev/null +++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_penduduk_non_permanent/edit/page.tsx @@ -0,0 +1,103 @@ +'use client' +/* eslint-disable react-hooks/exhaustive-deps */ +import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; +import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa'; +import colors from '@/con/colors'; +import { Box, Button, Group, 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'; + +function EditPelayananPendudukNonPermanent() { + const router = useRouter(); + const params = useParams() + const statePendudukNonPermanent = useProxy(stateLayananDesa.pelayananPendudukNonPermanen) + const [formData, setFormData] = useState({ + name: statePendudukNonPermanent.findById.data?.name || '', + deskripsi: statePendudukNonPermanent.findById.data?.deskripsi || '', + }) + + useEffect(() => { + const loadPelayananPerizinan = async () => { + const id = params?.id as string; + if (!id) return; + try { + const data = await statePendudukNonPermanent.update.load(id); + if (data) { + setFormData({ + name: data.name || '', + deskripsi: data.deskripsi || '', + }); + } + } catch (error) { + console.error("Error loading pelayanan perizinan berusaha:", error); + toast.error("Gagal memuat data pelayanan perizinan berusaha"); + } + }; + loadPelayananPerizinan(); + }, [params?.id]); + + const handleSubmit = async () => { + if (statePendudukNonPermanent.findById.data) { + statePendudukNonPermanent.findById.data.name = formData.name; + statePendudukNonPermanent.findById.data.deskripsi = formData.deskripsi; + statePendudukNonPermanent.update.update(statePendudukNonPermanent.findById.data) + } + router.push('/admin/desa/layanan/pelayanan_penduduk_non_permanent') + } + return ( + + + + + + + + + Edit Pelayanan Penduduk Non Permanent + Judul + { + setFormData({ + ...formData, + name: val.target.value, + }) + }} + /> + Deskripsi + { + setFormData({ + ...formData, + deskripsi: val, + }) + }} + /> + + + + + + + + + + ); +} + +export default EditPelayananPendudukNonPermanent; diff --git a/src/app/admin/(dashboard)/desa/layanan/pelayanan_penduduk_non_permanent/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_penduduk_non_permanent/page.tsx index 9813c4cf..3d9065f3 100644 --- a/src/app/admin/(dashboard)/desa/layanan/pelayanan_penduduk_non_permanent/page.tsx +++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_penduduk_non_permanent/page.tsx @@ -1,40 +1,48 @@ -import JudulListTab from '@/app/admin/(dashboard)/_com/jusulListTab'; +'use client' import colors from '@/con/colors'; -import { Box, Button, Paper, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; -import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; +import { Box, Button, Grid, GridCol, Paper, Skeleton, Stack, Text } from '@mantine/core'; +import { useShallowEffect } from '@mantine/hooks'; +import { IconEdit } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; +import { useProxy } from 'valtio/utils'; +import stateLayananDesa from '../../../_state/desa/layananDesa'; function SuratKeterangan() { + const router = useRouter() + const pelayananPendudukNonPermanen = useProxy(stateLayananDesa.pelayananPendudukNonPermanen) + + useShallowEffect(() => { + pelayananPendudukNonPermanen.findById.load('1') + }, []) + + if (!pelayananPendudukNonPermanen.findById.data) { + return ( + + + + ) + } return ( - } - /> - - - - Nama - Deskripsi - Detail - - - - - Pelayanan Penduduk Non-Permanent - Deskripsi Pelayanan Penduduk Non-Permanent - - - - - - - -
+ + + +
+ {pelayananPendudukNonPermanen.findById.data.name} + + ); diff --git a/src/app/admin/(dashboard)/desa/penghargaan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/penghargaan/[id]/edit/page.tsx new file mode 100644 index 00000000..761d2ee9 --- /dev/null +++ b/src/app/admin/(dashboard)/desa/penghargaan/[id]/edit/page.tsx @@ -0,0 +1,147 @@ +'use client' +/* eslint-disable react-hooks/exhaustive-deps */ +import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; +import penghargaanState from '@/app/admin/(dashboard)/_state/desa/penghargaan'; +import colors from '@/con/colors'; +import ApiFetch from '@/lib/api-fetch'; +import { Box, Button, Paper, Stack, Title, TextInput, FileInput, Center, Text, Image } from '@mantine/core'; +import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; +import { useParams, useRouter } from 'next/navigation'; +import React, { useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; + +function EditPenghargaan() { + const statePenghargaan = useProxy(penghargaanState) + const router = useRouter() + const params = useParams() + const [previewImage, setPreviewImage] = useState(null) + const [file, setFile] = useState(null) + const [formData, setFormData] = useState({ + name: statePenghargaan.findUnique.data?.name || '', + juara: statePenghargaan.findUnique.data?.juara || '', + deskripsi: statePenghargaan.findUnique.data?.deskripsi || '', + imageId: statePenghargaan.findUnique.data?.imageId || '', + }) + + useEffect(() => { + const loadPenghargaan = async () => { + const id = params?.id as string; + if (!id) return; + + try { + const data = await statePenghargaan.edit.load(id); + if (data) { + setFormData({ + name: data.name || '', + juara: data.juara || '', + deskripsi: data.deskripsi || '', + imageId: data.imageId || '', + }); + + if (data?.image?.link) { + setPreviewImage(data.image.link); + } + } + } catch (error) { + console.error("Error loading penghargaan:", error); + toast.error("Gagal memuat data penghargaan"); + } + }; + + loadPenghargaan(); + }, [params?.id]); + + const handleSubmit = async () => { + try { + statePenghargaan.edit.form = { + ...statePenghargaan.edit.form, + name: formData.name, + juara: formData.juara, + deskripsi: formData.deskripsi, + imageId: formData.imageId, + } + + 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"); + } + + statePenghargaan.edit.form.imageId = uploaded.id; + } + + await statePenghargaan.edit.update(); + toast.success("Penghargaan berhasil diperbarui!"); + router.push("/admin/desa/penghargaan"); + } catch (error) { + console.error("Error updating penghargaan:", error); + toast.error("Terjadi kesalahan saat memperbarui penghargaan"); + } + } + + return ( + + + + + + + Edit Penghargaan + setFormData({ ...formData, name: e.target.value })} + label={Judul} + placeholder="masukkan judul" + /> + + setFormData({ ...formData, juara: e.target.value })} + label={Juara} + placeholder="masukkan juara" + /> + Upload Gambar Baru (Opsional)} + value={file} + onChange={async (e) => { + if (!e) return; + setFile(e); + const base64 = await e.arrayBuffer().then((buf) => + "data:image/png;base64," + Buffer.from(buf).toString("base64") + ); + setPreviewImage(base64); + }} + /> + + {previewImage ? ( + + ) : ( +
+ +
+ )} + + + Deskripsi + { + setFormData((prev) => ({ ...prev, deskripsi: htmlContent })); + statePenghargaan.edit.form.deskripsi = htmlContent; + }} + /> + + + + + + + ); +} + +export default EditPenghargaan; diff --git a/src/app/admin/(dashboard)/desa/penghargaan/[id]/page.tsx b/src/app/admin/(dashboard)/desa/penghargaan/[id]/page.tsx new file mode 100644 index 00000000..4b094730 --- /dev/null +++ b/src/app/admin/(dashboard)/desa/penghargaan/[id]/page.tsx @@ -0,0 +1,111 @@ +'use client' +import React, { useState } from 'react'; +import penghargaanState from '../../../_state/desa/penghargaan'; +import { useProxy } from 'valtio/utils'; +import { useParams, useRouter } from 'next/navigation'; +import { useShallowEffect } from '@mantine/hooks'; +import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; +import colors from '@/con/colors'; +import { IconArrowBack, IconX, IconEdit } from '@tabler/icons-react'; +import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; + +function DetailPenghargaan() { + const statePenghargaan = useProxy(penghargaanState) + const [modalHapus, setModalHapus] = useState(false); + const [selectedId, setSelectedId] = useState(null); + const router = useRouter() + const params = useParams() + + useShallowEffect(() => { + statePenghargaan.findUnique.load(params?.id as string) + }, []) + + const handleHapus = () => { + if (selectedId) { + statePenghargaan.delete.byId(selectedId) + setModalHapus(false) + setSelectedId(null) + router.push("/admin/desa/penghargaan") + } + } + + if (!statePenghargaan.findUnique.data) { + return ( + + + + ) + } + + return ( + + + + + + + Detail Penghargaan + {statePenghargaan.findUnique.data ? ( + + + + Judul + {statePenghargaan.findUnique.data?.name} + + + Juara + {statePenghargaan.findUnique.data?.juara} + + + Deskripsi + + + + Gambar + gambar + + + + + + + + ) : null} + + + + {/* Modal Konfirmasi Hapus */} + setModalHapus(false)} + onConfirm={handleHapus} + text='Apakah anda yakin ingin menghapus penghargaan ini?' + /> + + ); +} + +export default DetailPenghargaan; diff --git a/src/app/admin/(dashboard)/desa/penghargaan/create/page.tsx b/src/app/admin/(dashboard)/desa/penghargaan/create/page.tsx new file mode 100644 index 00000000..838aa368 --- /dev/null +++ b/src/app/admin/(dashboard)/desa/penghargaan/create/page.tsx @@ -0,0 +1,114 @@ +'use client' +import colors from '@/con/colors'; +import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; +import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; +import penghargaanState from '../../../_state/desa/penghargaan'; +import ApiFetch from '@/lib/api-fetch'; +import CreateEditor from '../../../_com/createEditor'; + + +function CreatePenghargaan() { + const statePenghargaan = useProxy(penghargaanState) + const [previewImage, setPreviewImage] = useState(null); + const [file, setFile] = useState(null); + const router = useRouter() + + const resetForm = () => { + statePenghargaan.create.form = { + name: "", + juara: "", + deskripsi: "", + imageId: "", + } + setPreviewImage(null) + setFile(null) + } + + const handleSubmit = async () => { + if (!file) { + return toast.error("Silahkan pilih file gambar terlebih dahulu") + } + + const res = await ApiFetch.api.fileStorage.create.post({ + file: file, + name: file.name + }) + + const uploaded = res.data?.data + if (!uploaded?.id) { + return toast.error("Gagal upload gambar") + } + + statePenghargaan.create.form.imageId = uploaded.id + + await statePenghargaan.create.create() + resetForm() + router.push("/admin/desa/penghargaan") + + } + return ( + + + + + + + Create Penghargaan + { + statePenghargaan.create.form.name = val.target.value; + }} + label={Nama Penghargaan} + placeholder="masukkan nama penghargaan" + /> + { + statePenghargaan.create.form.juara = val.target.value; + }} + label={Juara} + placeholder="masukkan juara" + /> + + Deskripsi + { + statePenghargaan.create.form.deskripsi = htmlContent; + }} + /> + + Upload Gambar Konten} + value={file} + onChange={async (e) => { + if (!e) return; + setFile(e); + const base64 = await e.arrayBuffer().then((buf) => + "data:image/png;base64," + Buffer.from(buf).toString("base64") + ); + setPreviewImage(base64); + }} + /> + {previewImage ? ( + + ) : ( +
+ +
+ )} + +
+
+
+ ); +} + +export default CreatePenghargaan; diff --git a/src/app/admin/(dashboard)/desa/penghargaan/page.tsx b/src/app/admin/(dashboard)/desa/penghargaan/page.tsx index 40e01edb..6d839be6 100644 --- a/src/app/admin/(dashboard)/desa/penghargaan/page.tsx +++ b/src/app/admin/(dashboard)/desa/penghargaan/page.tsx @@ -1,34 +1,69 @@ +'use client' +import penghargaanState from '@/app/admin/(dashboard)/_state/desa/penghargaan'; import colors from '@/con/colors'; -import { Box, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core'; -import React from 'react'; -import Penghargaan from './ui/penghargaan/page'; -import GambarPerhargaan from './ui/gambar_perhargaan/page'; +import { Box, Button, Image, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; +import { useShallowEffect } from '@mantine/hooks'; +import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; +import { useProxy } from 'valtio/utils'; +import JudulListTab from '../../_com/jusulListTab'; -function Page() { +function Penghargaan() { + const state = useProxy(penghargaanState) + const router = useRouter() + useShallowEffect(() => { + state.findMany.load() + }, []) + + if (!state.findMany.data) { + return( + + + + ) + } return ( - - Penghargaan - - - - Penghargaan - - - Gambar Penghargaan - - - - - - - - - - - + + } + /> + + + + Nama + Deskripsi + Image + Detail + + + + {state.findMany.data?.map((item) => ( + + {item.name} + + + + + + + + + + + + + ))} + +
+
); } -export default Page; +export default Penghargaan; diff --git a/src/app/admin/(dashboard)/desa/penghargaan/ui/gambar_perhargaan/listPage.tsx b/src/app/admin/(dashboard)/desa/penghargaan/ui/gambar_perhargaan/listPage.tsx deleted file mode 100644 index cfdaf4c7..00000000 --- a/src/app/admin/(dashboard)/desa/penghargaan/ui/gambar_perhargaan/listPage.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import colors from '@/con/colors'; -import { Box, Paper, Stack, Title } from '@mantine/core'; -import React from 'react'; - -function ListGambarPenghargaan() { - return ( - - - - List Gambar Penghargaan - - - - ); -} - -export default ListGambarPenghargaan; diff --git a/src/app/admin/(dashboard)/desa/penghargaan/ui/gambar_perhargaan/page.tsx b/src/app/admin/(dashboard)/desa/penghargaan/ui/gambar_perhargaan/page.tsx deleted file mode 100644 index 93bfcbe7..00000000 --- a/src/app/admin/(dashboard)/desa/penghargaan/ui/gambar_perhargaan/page.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import colors from '@/con/colors'; -import { Box, Paper, SimpleGrid, Stack, Title, Text, Group, Button, TextInput, Center } from '@mantine/core'; -import React from 'react'; -import { DesaEditor } from '../../../_com/desaEditor'; -import ListGambarPenghargaan from './listPage'; -import { IconUpload } from '@tabler/icons-react'; - - -function GambarPerhargaan() { - return ( - - - - - - - Tambah Gambar Penghargaan - - Deskripsi Gambar Penghargaan - - - Upload Gambar Penghargaan - -
- -
-
-
- - - -
-
-
- -
-
-
- ); -} - -export default GambarPerhargaan; diff --git a/src/app/admin/(dashboard)/desa/penghargaan/ui/penghargaan/listPage.tsx b/src/app/admin/(dashboard)/desa/penghargaan/ui/penghargaan/listPage.tsx deleted file mode 100644 index d217eb80..00000000 --- a/src/app/admin/(dashboard)/desa/penghargaan/ui/penghargaan/listPage.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import colors from '@/con/colors'; -import { Box, Paper, Stack, Title } from '@mantine/core'; -import React from 'react'; - -function ListPenghargaan() { - return ( - - - - List Penghargaan - - - - ); -} - -export default ListPenghargaan; diff --git a/src/app/admin/(dashboard)/desa/penghargaan/ui/penghargaan/page.tsx b/src/app/admin/(dashboard)/desa/penghargaan/ui/penghargaan/page.tsx deleted file mode 100644 index ba63a677..00000000 --- a/src/app/admin/(dashboard)/desa/penghargaan/ui/penghargaan/page.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import colors from '@/con/colors'; -import { Box, Button, Group, Paper, SimpleGrid, Stack, Text, Title } from '@mantine/core'; -import React from 'react'; -import { DesaEditor } from '../../../_com/desaEditor'; -import ListPenghargaan from './listPage'; - -function Penghargaan() { - return ( - - - - - - - Penghargaan - - Deskripsi Penghargaan - - - - - - - - - - - - - ); -} - -export default Penghargaan; diff --git a/src/app/api/[[...slugs]]/_lib/desa/index.ts b/src/app/api/[[...slugs]]/_lib/desa/index.ts index 9769536c..1955ed81 100644 --- a/src/app/api/[[...slugs]]/_lib/desa/index.ts +++ b/src/app/api/[[...slugs]]/_lib/desa/index.ts @@ -6,6 +6,7 @@ import PotensiDesa from "./potensi"; import GalleryFoto from "./gallery/foto"; import GalleryVideo from "./gallery/video"; import LayananDesa from "./layanan"; +import Penghargaan from "./penghargaan"; const Desa = new Elysia({ prefix: "/api/desa", tags: ["Desa"] }) @@ -16,5 +17,6 @@ const Desa = new Elysia({ prefix: "/api/desa", tags: ["Desa"] }) .use(GalleryFoto) .use(GalleryVideo) .use(LayananDesa) + .use(Penghargaan) export default Desa; diff --git a/src/app/api/[[...slugs]]/_lib/desa/penghargaan/create.ts b/src/app/api/[[...slugs]]/_lib/desa/penghargaan/create.ts new file mode 100644 index 00000000..199a8f75 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/desa/penghargaan/create.ts @@ -0,0 +1,32 @@ +import prisma from "@/lib/prisma"; +import { Prisma } from "@prisma/client"; +import { Context } from "elysia"; + +type FormCreate = Prisma.PenghargaanGetPayload<{ + select: { + juara: true; + name: true; + deskripsi: true; + imageId: true; + } +}> +export default async function penghargaanCreate(context: Context){ + const body = context.body as FormCreate; + + await prisma.penghargaan.create({ + data: { + juara: body.juara, + name: body.name, + deskripsi: body.deskripsi, + imageId: body.imageId, + } + }) + + return { + success: true, + message: "Success create penghargaan", + data: { + ...body, + } + } +} \ No newline at end of file diff --git a/src/app/api/[[...slugs]]/_lib/desa/penghargaan/del.ts b/src/app/api/[[...slugs]]/_lib/desa/penghargaan/del.ts new file mode 100644 index 00000000..72f8222a --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/desa/penghargaan/del.ts @@ -0,0 +1,53 @@ +import prisma from "@/lib/prisma"; +import { Context } from "elysia"; +import fs from "fs/promises"; +import path from "path"; + +const penghargaanDelete = async (context: Context) => { + const id = context.params?.id as string; + + if (!id) { + return { + status: 400, + body: "ID tidak diberikan", + }; + } + + const penghargaan = await prisma.penghargaan.findUnique({ + where: { id }, + include: { + image: true, + } + }); + + if (!penghargaan) { + return { + status: 404, + body: "Penghargaan tidak ditemukan", + }; + } + + if (penghargaan.image) { + try { + const filePath = path.join(penghargaan.image.path, penghargaan.image.name); + await fs.unlink(filePath); + await prisma.fileStorage.delete({ + where: { id: penghargaan.image.id }, + }); + } catch (error) { + console.error("Gagal hapus file image:", error); + } + } + + await prisma.penghargaan.delete({ + where: { id }, + }); + + return { + success: true, + message: "Penghargaan berhasil dihapus", + status: 200, + }; +} + +export default penghargaanDelete diff --git a/src/app/api/[[...slugs]]/_lib/desa/penghargaan/find-many.ts b/src/app/api/[[...slugs]]/_lib/desa/penghargaan/find-many.ts new file mode 100644 index 00000000..9c2f44ec --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/desa/penghargaan/find-many.ts @@ -0,0 +1,23 @@ +import prisma from "@/lib/prisma"; + +export default async function penghargaanFindMany() { + try { + const data = await prisma.penghargaan.findMany({ + include: { + image: true, + }, + }); + + return { + success: true, + message: "Success fetch penghargaan", + data, + }; + } catch (error) { + console.error("Find many error:", error); + return { + success: false, + message: "Failed fetch penghargaan", + }; + } +} diff --git a/src/app/api/[[...slugs]]/_lib/desa/penghargaan/findUnique.ts b/src/app/api/[[...slugs]]/_lib/desa/penghargaan/findUnique.ts new file mode 100644 index 00000000..5bf8e5c3 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/desa/penghargaan/findUnique.ts @@ -0,0 +1,49 @@ +import prisma from "@/lib/prisma"; + +export default async function penghargaanFindUnique(request: Request) { + const url = new URL(request.url); + const pathSegments = url.pathname.split("/"); + const id = pathSegments[pathSegments.length - 1]; + + if (!id) { + return Response.json({ + success: false, + message: "ID tidak ditemukan", + }, {status: 400}); + } + + try { + if (typeof id !== 'string') { + return Response.json({ + success: false, + message: "ID tidak valid", + }, {status: 400}); + } + + const data = await prisma.penghargaan.findUnique({ + where: { id }, + include: { + image: true, + }, + }); + + if (!data) { + return Response.json({ + success: false, + message: "Penghargaan tidak ditemukan", + }, {status: 404}); + } + + return Response.json({ + success: true, + message: "Success fetch penghargaan by ID", + data, + }, {status: 200}); + } catch (error) { + console.error("Find by ID error:", error); + return Response.json({ + success: false, + message: "Gagal mengambil penghargaan: " + (error instanceof Error ? error.message : 'Unknown error'), + }, {status: 500}); + } +} diff --git a/src/app/api/[[...slugs]]/_lib/desa/penghargaan/index.ts b/src/app/api/[[...slugs]]/_lib/desa/penghargaan/index.ts new file mode 100644 index 00000000..223fa815 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/desa/penghargaan/index.ts @@ -0,0 +1,35 @@ +import Elysia, { t } from "elysia"; +import penghargaanFindMany from "./find-many"; +import penghargaanFindUnique from "./findUnique"; +import penghargaanCreate from "./create"; +import penghargaanDelete from "./del"; +import penghargaanUpdate from "./updt"; + + +const Penghargaan = new Elysia({prefix: "/penghargaan", tags: ["Desa/Penghargaan"]}) + .get("/find-many", penghargaanFindMany) + .get("/:id", async (context) => { + const response = await penghargaanFindUnique(context.request); + return response; + }) + .post("/create", penghargaanCreate, { + body: t.Object({ + juara: t.String(), + name: t.String(), + deskripsi: t.String(), + imageId: t.String(), + }), + }) + .delete("/del/:id", penghargaanDelete) + .put("/:id", async (context) => { + const response = await penghargaanUpdate(context); + return response; + }, { + body: t.Object({ + juara: t.String(), + name: t.String(), + deskripsi: t.String(), + imageId: t.String(), + }), + }); +export default Penghargaan; \ No newline at end of file diff --git a/src/app/api/[[...slugs]]/_lib/desa/penghargaan/updt.ts b/src/app/api/[[...slugs]]/_lib/desa/penghargaan/updt.ts new file mode 100644 index 00000000..82a4e1a3 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/desa/penghargaan/updt.ts @@ -0,0 +1,106 @@ +import prisma from "@/lib/prisma"; +import { Prisma } from "@prisma/client"; +import path from "path"; +import fs from "fs/promises"; +import { Context } from "elysia"; + +type FormUpdate = Prisma.PenghargaanGetPayload<{ + select: { + id: true; + juara: true; + name: true; + deskripsi: true; + imageId: true; + } +}> +export default async function penghargaanUpdate(context: Context) { +try { + const id = context.params?.id as string; + const body = (await context.body) as Omit; + + const { + juara, + name, + deskripsi, + imageId + } = body; + + if (!id) { + return new Response(JSON.stringify({ + success: false, + message: "ID tidak ditemukan", + }), { + status: 400, + headers: { + 'Content-Type': 'application/json' + } + }) + } + + const existing = await prisma.penghargaan.findUnique({ + where: {id}, + include: { + image: true, + } + }) + + if (!existing) { + return new Response(JSON.stringify({ + success: false, + message: "Penghargaan tidak ditemukan", + }), { + status: 404, + headers: { + 'Content-Type': 'application/json' + } + }) + } + + if (existing.imageId && existing.imageId !== imageId) { + const oldImage = existing.image; + if (oldImage) { + try { + const filePath = path.join(oldImage.path, oldImage.name); + await fs.unlink(filePath); + await prisma.fileStorage.delete({ + where: { id: oldImage.id }, + }); + } catch (error) { + console.error("Gagal hapus gambar lama:", error); + } + } + } + + const updated = await prisma.penghargaan.update({ + where: { id }, + data: { + juara, + name, + deskripsi, + imageId, + } + }) + + return new Response(JSON.stringify({ + success: true, + message: "Penghargaan berhasil diupdate", + data: updated, + }), { + status: 200, + headers: { + 'Content-Type': 'application/json' + } + }) +} catch (error) { + console.error("Error updating penghargaan:", error); + return new Response(JSON.stringify({ + success: false, + message: "Terjadi kesalahan saat mengupdate penghargaan", + }), { + status: 500, + headers: { + 'Content-Type': 'application/json' + } + }) +} +} \ No newline at end of file