diff --git a/prisma/data/ppid/profile-ppid/profilePPid.json b/prisma/data/ppid/profile-ppid/profilePPid.json index 3727382c..dc86f436 100644 --- a/prisma/data/ppid/profile-ppid/profilePPid.json +++ b/prisma/data/ppid/profile-ppid/profilePPid.json @@ -5,7 +5,6 @@ "biodata": "

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.

", "riwayat": "", "pengalaman": "", - "unggulan": "

Pemberdayaan Ekonomi dan UMKM

", - "imageUrl": "/uploads/seeded-images/profile-ppid/perbekel.png" + "unggulan": "

Pemberdayaan Ekonomi dan UMKM

" } ] diff --git a/prisma/data/ppid/struktur-ppid/strukturPPID.json b/prisma/data/ppid/struktur-ppid/strukturPPID.json new file mode 100644 index 00000000..cdc2ff14 --- /dev/null +++ b/prisma/data/ppid/struktur-ppid/strukturPPID.json @@ -0,0 +1,6 @@ +[ + { + "id" : "1", + "name" : "Struktur PPID" + } +] \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9436a43d..17da3fe6 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -64,9 +64,22 @@ model FileStorage { PotensiDesa PotensiDesa[] Posyandu Posyandu[] ProfilePPID ProfilePPID[] + StrukturPPID StrukturPPID[] } //========================================= MENU PPID ========================================= // + +//========================================= STRUKTUR PPID ========================================= // +model StrukturPPID { + id String @id @default(cuid()) + name 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) +} // ========================================= VISI MISI PPID ========================================= // model VisiMisiPPID { id String @id @default(cuid()) diff --git a/prisma/seed.ts b/prisma/seed.ts index 7b4686d9..8a46c959 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -9,6 +9,7 @@ import potensi from "./data/list-potensi.json"; import dasarHukumPPID from "./data/ppid/dasar-hukum-ppid/dasarhukumPPID.json"; 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"; (async () => { for (const l of layanan) { @@ -27,6 +28,22 @@ import visiMisiPPID from "./data/ppid/visi-misi-ppid/visimisiPPID.json"; console.log("layanan success ..."); + for (const s of strukturPPID) { + await prisma.strukturPPID.upsert({ + where: { + id: s.id, + }, + update: { + name: s.name, + }, + create: { + id: s.id, + name: s.name, + }, + }); + } + console.log("struktur ppid success ..."); + for (const p of potensi) { await prisma.potensi.upsert({ where: { @@ -179,8 +196,6 @@ import visiMisiPPID from "./data/ppid/visi-misi-ppid/visimisiPPID.json"; }); } console.log("dasar hukum PPID success ..."); - - })() .then(() => prisma.$disconnect()) .catch((e) => { diff --git a/public/struktur_ppid.png b/public/struktur_ppid.png new file mode 100644 index 00000000..5124ac1e Binary files /dev/null and b/public/struktur_ppid.png differ diff --git a/src/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID.ts b/src/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID.ts new file mode 100644 index 00000000..4df4347c --- /dev/null +++ b/src/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID.ts @@ -0,0 +1,169 @@ +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(3, "Nama minimal 3 karakter"), + imageId: z.string().min(1, "Gambar wajib dipilih"), +}) + +const defaultForm = { + name: "", + imageId: "", +}; + +type StrukturPPIDForm = Prisma.StrukturPPIDGetPayload<{ + select: { + id: true; + name: true; + imageId: true; + image?: { + select: { + link: true; + }; + }; + }; +}>; + +const stateStrukturPPID = proxy({ + struktur: { + data: null as StrukturPPIDForm | null, + loading: false, + error: null as string | null, + + async load(id: string) { + if(!id) { + toast.warn("ID tidak valid") + return null + } + + this.loading = true; + this.error = null; + + try { + const response = await fetch(`/api/ppid/strukturppid/${id}`); + + if(!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const result = await response.json(); + + if(result.success) { + this.data = result.data; + return result.data + } else { + throw new Error(result.message || "Gagal mengambil data struktur") + } + } catch (error) { + const errorMessage = (error as Error).message; + this.error = errorMessage; + console.error("Load struktur error:", errorMessage); + toast.error("Terjadi kesalahan saat mengambil data struktur"); + return null; + } finally { + this.loading = false; + } + }, + + reset() { + this.data = null; + this.error = null; + this.loading = false; + } + }, + + editStruktur: { + id: "", + form: { ...defaultForm }, + loading: false, + error: null as string | null, + isReadOnly: false, + + initialize(strukturData: StrukturPPIDForm) { + this.id = strukturData.id; + this.isReadOnly = false; + this.form = { + name: strukturData.name || "", + imageId: strukturData.imageId || "", + }; + }, + + updateField(field: keyof typeof defaultForm, value: string) { + this.form[field] = value; + }, + + async submit() { + const validation = templateForm.safeParse(this.form); + + if (!validation.success) { + const errors = validation.error.issues + .map((issue) => `${issue.path.join(".")}: ${issue.message}`) + .join(", "); + toast.error(`Form tidak valid: ${errors}`); + return false; + } + + this.loading = true; + this.error = null; + + try { + const response = await fetch(`/api/ppid/strukturppid/${this.id}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(this.form), + }) + + 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 struktur"); + await stateStrukturPPID.struktur.load(this.id); + return true; + } else { + throw new Error(result.message || "Gagal update struktur"); + } + } catch (error) { + const errorMessage = (error as Error).message; + this.error = errorMessage; + console.error("Update struktur error:", errorMessage); + toast.error("Terjadi kesalahan saat update struktur"); + return false; + } finally { + this.loading = false; + } + }, + + reset() { + this.id = ""; + this.form = { ...defaultForm }; + this.error = null; + this.loading = false; + this.isReadOnly = false; + } + }, + + async loadForEdit(id: string) { + const strukturData = await this.struktur.load(id); + if (strukturData) { + this.editStruktur.initialize(strukturData); + } + return strukturData; + }, + + reset() { + this.struktur.reset(); + this.editStruktur.reset(); + } +}) + +export default stateStrukturPPID; + \ No newline at end of file diff --git a/src/app/admin/(dashboard)/ppid/profile-ppid/[id]/page.tsx b/src/app/admin/(dashboard)/ppid/profile-ppid/[id]/page.tsx index cd8c8b73..72956a86 100644 --- a/src/app/admin/(dashboard)/ppid/profile-ppid/[id]/page.tsx +++ b/src/app/admin/(dashboard)/ppid/profile-ppid/[id]/page.tsx @@ -1,13 +1,13 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ 'use client' import colors from '@/con/colors'; -import { Box, Button, Center, FileInput, Group, Image, Paper, Stack, Text, TextInput, Title, Alert } from '@mantine/core'; +import { Alert, Box, Button, Center, FileInput, Group, Image, Paper, Stack, Text, TextInput, Title } 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 { IconArrowBack, IconImageInPicture, IconAlertCircle } from '@tabler/icons-react'; +import { IconAlertCircle, IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { toast } from 'react-toastify'; import Biodata from './biodata/biodataForm'; diff --git a/src/app/admin/(dashboard)/ppid/struktur-ppid/[id]/page.tsx b/src/app/admin/(dashboard)/ppid/struktur-ppid/[id]/page.tsx new file mode 100644 index 00000000..e07ca511 --- /dev/null +++ b/src/app/admin/(dashboard)/ppid/struktur-ppid/[id]/page.tsx @@ -0,0 +1,225 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +'use client' +import colors from '@/con/colors'; +import { + Alert, + Box, + Button, Center, FileInput, Group, Image, Paper, + Stack, + Text, + TextInput, + Title +} from '@mantine/core'; +import { IconAlertCircle, IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; +import { useParams, useRouter } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import { useProxy } from 'valtio/utils'; +import stateStrukturPPID from '../../../_state/ppid/struktur_ppid/struktur_PPID'; +import { toast } from 'react-toastify'; +import ApiFetch from '@/lib/api-fetch'; + + +function EditStrukturPPID() { + const strukturPPID = useProxy(stateStrukturPPID); + const params = useParams(); + const router = useRouter() + + const [previewImage, setPreviewImage] = useState(null); + const [file, setFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + useEffect(() => { + const loadData = async () => { + const id = params?.id as string; + if (!id) { + toast.error("ID tidak valid"); + router.push("/admin/ppid/struktur-ppid"); + return; + } + + try { + const profileData = await strukturPPID.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 () => { + strukturPPID.editStruktur.reset(); // cleanup form + }; + }, [params?.id, router]); + + const handleFieldChange = (field: string, value: string) => { + strukturPPID.editStruktur.updateField(field as any, value); + }; + + 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); + }; + + const handleSubmit = async () => { + if (isSubmitting || !strukturPPID.editStruktur.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; + } + + strukturPPID.editStruktur.form.imageId = uploaded.id; + } + + // Submit form + const success = await strukturPPID.editStruktur.submit(); + + if (success) { + toast.success("Berhasil menyimpan perubahan"); + router.push("/admin/ppid/struktur-ppid"); + } + } catch (error) { + console.error("Error submitting form:", error); + toast.error("Gagal menyimpan profile"); + } finally { + setIsSubmitting(false); + } + }; + + const handleBack = () => { + router.back(); + }; + + if (strukturPPID.struktur.loading) { + return ( + +
+ Memuat data struktur... +
+
+ ); + } + + // Error state + if (strukturPPID.struktur.error) { + return ( + + + + } color="red"> + Error + {strukturPPID.struktur.error} + + + + ); + } + + // No data state + if (!strukturPPID.struktur.data) { + return ( + + + + } color="yellow"> + Data tidak ditemukan + Profile PPID tidak dapat ditemukan + + + + ); + } + + return ( + + + + + + + + + Edit Struktur PPID + handleFieldChange('name', e.currentTarget.value)} + label={Judul} + placeholder="Masukkan judul" + /> + {/* File Upload */} + Upload Gambar Baru (Opsional)} + value={file} + onChange={handleFileChange} + accept="image/*" + /> + + {/* Preview Gambar */} + + Preview Gambar + {previewImage ? ( + Profile preview + ) : ( +
+ + + Tidak ada gambar + +
+ )} +
+ + + + +
+
+
+
+
+ ); +} + +export default EditStrukturPPID; diff --git a/src/app/admin/(dashboard)/ppid/struktur-ppid/page.tsx b/src/app/admin/(dashboard)/ppid/struktur-ppid/page.tsx index 705991db..fcdb063a 100644 --- a/src/app/admin/(dashboard)/ppid/struktur-ppid/page.tsx +++ b/src/app/admin/(dashboard)/ppid/struktur-ppid/page.tsx @@ -1,10 +1,59 @@ -import React from 'react'; +'use client' +import colors from '@/con/colors'; +import { Box, Button, Grid, GridCol, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core'; +import { IconEdit } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; +import { useProxy } from 'valtio/utils'; +import stateStrukturPPID from '../../_state/ppid/struktur_ppid/struktur_PPID'; +import { useShallowEffect } from '@mantine/hooks'; function Page() { + const router = useRouter() + const strukturPPID = useProxy(stateStrukturPPID) + useShallowEffect(() => { + strukturPPID.struktur.load("1") + }, []) + + if (!strukturPPID.struktur.data) { + return + + + } + + const dataArray = Array.isArray(strukturPPID.struktur.data) + ? strukturPPID.struktur.data + : [strukturPPID.struktur.data]; + return ( -
- struktur-ppid -
+ + + + + Preview Struktur PPID + + + + + + {dataArray.map((item) => ( + + + {item.name} + { + e.currentTarget.src = "/struktur_ppid.png"; + }} + /> + + + ))} + + ); } diff --git a/src/app/api/[[...slugs]]/_lib/ppid/index.ts b/src/app/api/[[...slugs]]/_lib/ppid/index.ts index d830064c..b590bd86 100644 --- a/src/app/api/[[...slugs]]/_lib/ppid/index.ts +++ b/src/app/api/[[...slugs]]/_lib/ppid/index.ts @@ -9,6 +9,7 @@ import PermohonanKeberatanInformasiPublik from "./permohonan_keberatan_informasi import ProfilePPID from "./profile_ppid"; import VisiMisiPPID from "./visi_misi_ppid/visi_misi_ppid"; import DasarHukumPPID from "./dasar_hukum"; +import StrukturPPID from "./struktur_ppid"; @@ -24,6 +25,7 @@ const PPID = new Elysia({ prefix: "/api/ppid", tags: ["PPID"] }) .use(PermohonanKeberatanInformasiPublik) .use(VisiMisiPPID) .use(DasarHukumPPID) +.use(StrukturPPID) diff --git a/src/app/api/[[...slugs]]/_lib/ppid/struktur_ppid/find-by-id.ts b/src/app/api/[[...slugs]]/_lib/ppid/struktur_ppid/find-by-id.ts new file mode 100644 index 00000000..8ea16406 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/ppid/struktur_ppid/find-by-id.ts @@ -0,0 +1,51 @@ +import prisma from "@/lib/prisma"; + +export default async function strukturPPIDFindById(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 boleh kosong", + }, { status: 400 }); + } + + try { + if (typeof id !== 'string') { + return Response.json({ + success: false, + message: "ID tidak valid", + }, { status: 400 }); + } + + const data = await prisma.strukturPPID.findUnique({ + where: { id }, + include: { + image: true, + } + }); + + if (!data) { + return Response.json({ + success: false, + message: "Data tidak ditemukan", + }, { status: 404 }); + } + + return Response.json({ + success: true, + message: "Berhasil mengambil data berdasarkan ID", + data, + }, { status: 200 }); + } catch (e) { + console.error("Find by ID error:", e); + return Response.json({ + success: false, + message: "Gagal mengambil data: " + (e instanceof Error ? e.message : 'Unknown error'), + }, { + status: 500, + }); + } +} \ No newline at end of file diff --git a/src/app/api/[[...slugs]]/_lib/ppid/struktur_ppid/index.ts b/src/app/api/[[...slugs]]/_lib/ppid/struktur_ppid/index.ts new file mode 100644 index 00000000..5b12d4b8 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/ppid/struktur_ppid/index.ts @@ -0,0 +1,23 @@ +import Elysia, { t } from "elysia"; +import strukturPPIDFindById from "./find-by-id"; +import strukturPPIDUpdate from "./update"; + +const StrukturPPID = new Elysia({ + prefix: "/strukturppid", + tags: ["PPID/Struktur PPID"] +}) +.get("/:id", async (context) => { + const response = await strukturPPIDFindById(new Request(context.request)) + return response +}) +.put("/:id", async (context) => { + const response = await strukturPPIDUpdate(context) + return response +}, { + body: t.Object({ + name: t.String(), + imageId: t.String(), + }) +}) + +export default StrukturPPID diff --git a/src/app/api/[[...slugs]]/_lib/ppid/struktur_ppid/update.ts b/src/app/api/[[...slugs]]/_lib/ppid/struktur_ppid/update.ts new file mode 100644 index 00000000..6af97693 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/ppid/struktur_ppid/update.ts @@ -0,0 +1,116 @@ +import prisma from "@/lib/prisma"; +import { Context } from "elysia"; +import path from "path"; +import fs from "fs/promises"; +import { Prisma } from "@prisma/client"; + +type FormUpdate = Prisma.StrukturPPIDGetPayload<{ + select: { + id: true; + name: true; + imageId: true; + }; +}>; + +export default async function strukturPPIDUpdate(context: Context) { +try { + const id = context.params?.id as string; + const body = (await context.body) as Omit; + + const { name, imageId } = body; + + if (!id) { + return new Response( + JSON.stringify({ + success: false, + message: "ID tidak boleh kosong", + }), + { + status: 400, + headers: { + "Content-Type": "application/json", + }, + } + ) + } + + const existing = await prisma.strukturPPID.findUnique({ + where: { + id + }, + include: { + image: true, + } + }) + + if (!existing) { + return new Response( + JSON.stringify({ + success: false, + message: "Data tidak ditemukan", + }), + { + status: 404, + headers: { + "Content-Type": "application/json", + }, + } + ) + } + + if (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.strukturPPID.update({ + where: { + id + }, + data: { + name, + imageId, + } + }) + + return new Response( + JSON.stringify({ + success: true, + message: "Struktur PPID Berhasil Dibuat", + data: updated, + }), + { + status: 200, + headers: { + "Content-Type": "application/json", + }, + } + ) + +} catch (error) { + console.error("Error updating struktur PPID:", error); + return new Response( + JSON.stringify({ + success: false, + message: "Terjadi kesalahan saat mengupdate struktur PPID", + }), + { + status: 500, + headers: { + "Content-Type": "application/json", + }, + } + ) +} +} + \ No newline at end of file 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 82c37f5d..5523b107 100644 --- a/src/app/darmasaba/_com/main-page/landing-page/index.tsx +++ b/src/app/darmasaba/_com/main-page/landing-page/index.tsx @@ -29,7 +29,7 @@ const getCurrentTime = () => { } const isWorkingHours = (currentTime: string): boolean => { - const [openTime, closeTime] = ['08:00', '16:00']; + const [openTime, closeTime] = ['08:00', '11:00']; const compareTimes = (time1: string, time2: string) => { const [hour1, minute1] = time1.split(':').map(Number); @@ -202,14 +202,14 @@ function LandingPage() { - + {workStatus.status} - + {workStatus.message} @@ -234,7 +234,7 @@ function LandingPage() { Status - {workStatus.status === 'Buka' ? 'Operasional' : 'Libur'} + {workStatus.status === 'Buka' ? 'Operasional' : 'Tutup'}