Compare commits

..

4 Commits

Author SHA1 Message Date
f7437708c0 Menu Desa, Sub Menu Yang Tersisa Tinga Tinggal Profile Aja 2025-06-16 17:46:25 +08:00
7bf5ee69d5 Api Admin Menu Desa Sub Menu Layanan Tabs 3 Pelayanan Done 2025-06-16 15:01:15 +08:00
e03b071b00 UI Admin Dashboard Layanan Desa 1 tabs
API Admin Dashboard Layanan Desa 1 Tabs
2025-06-16 03:08:34 +08:00
8ded234991 UI Desa Gallery Done
API Desa Gallery Done
2025-06-13 16:32:33 +08:00
104 changed files with 5744 additions and 818 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -22,7 +22,7 @@
"@mantine/carousel": "^7.16.2",
"@mantine/charts": "^7.17.1",
"@mantine/core": "^7.17.4",
"@mantine/dates": "^7.17.4",
"@mantine/dates": "^8.1.0",
"@mantine/dropzone": "^7.17.0",
"@mantine/form": "^8.1.0",
"@mantine/hooks": "^7.17.4",

View File

@@ -0,0 +1,7 @@
[
{
"id": "1",
"name": "Pelayanan Penduduk Non-Permanent",
"deskripsi": "<p>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.</p>"
}
]

View File

@@ -0,0 +1,8 @@
[
{
"id": "1",
"name": "Pelayanan Perizinan Berusaha Berbasis Risiko Melalui Sistem ONLINE SINGLE SUBMISSION (OSS)",
"deskripsi": "<p>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.</p>",
"link" : "https://oss.go.id/"
}
]

View File

@@ -50,36 +50,43 @@ model AppMenuChild {
// ========================================= FILE STORAGE ========================================= //
model FileStorage {
id String @id @default(cuid())
name String @unique
realName String
path String
mimeType String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
isActive Boolean @default(true)
link String
Berita Berita[]
PotensiDesa PotensiDesa[]
Posyandu Posyandu[]
ProfilePPID ProfilePPID[]
id String @id @default(cuid())
name String @unique
realName String
path String
mimeType String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
isActive Boolean @default(true)
link String
Berita Berita[]
PotensiDesa PotensiDesa[]
Posyandu Posyandu[]
ProfilePPID ProfilePPID[]
StrukturPPID StrukturPPID[]
GalleryFoto GalleryFoto[]
PelayananSuratKeterangan PelayananSuratKeterangan[]
Penghargaan Penghargaan[]
}
//========================================= MENU PPID ========================================= //
//========================================= STRUKTUR PPID ========================================= //
model StrukturPPID {
id String @id @default(cuid())
name String @db.Text
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)
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())
@@ -104,18 +111,18 @@ model DasarHukumPPID {
// ========================================= PROFILE PPID ========================================= //
model ProfilePPID {
id String @id @default(cuid())
name String @db.Text
biodata String @db.Text
riwayat String @db.Text
pengalaman String @db.Text
unggulan String @db.Text
id String @id @default(cuid())
name String @db.Text
biodata String @db.Text
riwayat String @db.Text
pengalaman String @db.Text
unggulan 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)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= DAFTAR INFORMASI PUBLIK ========================================= //
@@ -334,51 +341,87 @@ model CategoryPengumuman {
isActive Boolean @default(true)
}
// ========================================= IMAGES ========================================= //
model Images {
id String @id @default(cuid())
url String
label String @default("null")
active Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
GalleryFoto GalleryFoto[]
}
// ========================================= VIDEOS ========================================= //
model Videos {
id String @id @default(cuid())
url String
label String @default("null")
active Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
GalleryVideo GalleryVideo[]
}
// ========================================= GALLERY ========================================= //
model GalleryFoto {
id String @id @default(cuid())
id String @id @default(cuid())
name String
image String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
imagesId String? @unique
imageGalleryFoto Images? @relation(fields: [imagesId], references: [id])
deskripsi String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
imagesId String? @unique
imageGalleryFoto FileStorage? @relation(fields: [imagesId], references: [id])
}
model GalleryVideo {
id String @id @default(cuid())
name String
video String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
videosId String? @unique
videosGalleryVideo Videos? @relation(fields: [videosId], references: [id])
id String @id @default(cuid())
name String
deskripsi String @db.Text
linkVideo String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= LAYANAN DESA ========================================= //
model PelayananSuratKeterangan {
id String @id @default(cuid())
name 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)
}
model PelayananTelunjukSaktiDesa {
id String @id @default(cuid())
name String
deskripsi String @db.Text
link String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model PelayananPerizinanBerusaha {
id String @id @default(cuid())
name String
deskripsi String @db.Text
link String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model PelayananPendudukNonPermanen {
id String @id @default(cuid())
name String
deskripsi String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
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 ========================================= //

View File

@@ -10,6 +10,8 @@ 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";
import pelayananPerizinanBerusaha from "./data/desa/layanan/pelayananPerizinanBerusaha.json";
import pelayananPendudukNonPermanen from "./data/desa/layanan/pelayanaPendudukNonPermanen.json";
(async () => {
for (const l of layanan) {
@@ -28,6 +30,46 @@ import strukturPPID from "./data/ppid/struktur-ppid/strukturPPID.json";
console.log("layanan success ...");
for (const l of pelayananPerizinanBerusaha) {
await prisma.pelayananPerizinanBerusaha.upsert({
where: {
id: l.id,
},
update: {
name: l.name,
deskripsi: l.deskripsi,
link: l.link,
},
create: {
id: l.id,
name: l.name,
deskripsi: l.deskripsi,
link: l.link,
},
});
}
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: {

BIN
public/Share.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

BIN
public/bagikanPostingan.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

BIN
public/sematkan.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

BIN
public/video.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@@ -0,0 +1,415 @@
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 fotoForm = z.object({
name: z.string().min(1, { message: "Name is required" }),
deskripsi: z.string().min(1, { message: "Deskripsi is required" }),
imagesId: z.string().nonempty(),
});
const videoForm = z.object({
name: z.string().min(1, { message: "Name is required" }),
deskripsi: z.string().min(1, { message: "Deskripsi is required" }),
linkVideo: z.string().min(1, { message: "Link video is required" }),
});
const defaultFormFoto = {
name: "",
deskripsi: "",
imagesId: "",
};
const defaultFormVideo = {
name: "",
deskripsi: "",
linkVideo: "",
};
const foto = proxy({
create: {
form: { ...defaultFormFoto },
loading: false,
async create() {
const cek = fotoForm.safeParse(foto.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
foto.create.loading = true;
const res = await ApiFetch.api.desa.gallery.foto["create"].post(
foto.create.form
);
if (res.status === 200) {
foto.findMany.load();
return toast.success("Foto berhasil disimpan!");
}
return toast.error("Gagal menyimpan foto");
} catch (error) {
console.log((error as Error).message);
} finally {
foto.create.loading = false;
}
},
resetForm() {
foto.create.form = { ...defaultFormFoto };
},
},
findMany: {
data: null as
| Prisma.GalleryFotoGetPayload<{
include: {
imageGalleryFoto: true;
};
}>[]
| null,
async load() {
const res = await ApiFetch.api.desa.gallery.foto["find-many"].get();
if (res.status === 200) {
foto.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.GalleryFotoGetPayload<{
include: {
imageGalleryFoto: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/desa/gallery/foto/${id}`);
if (res.ok) {
const data = await res.json();
foto.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch foto:", res.statusText);
foto.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching foto:", error);
foto.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
foto.delete.loading = true;
const response = await fetch(`/api/desa/gallery/foto/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok) {
toast.success(result.message || "Foto berhasil dihapus");
await foto.findMany.load(); // refresh list
} else {
toast.error(result.message || "Gagal menghapus foto");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus foto");
} finally {
foto.delete.loading = false;
}
},
},
update: {
id: "",
form: { ...defaultFormFoto },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/desa/gallery/foto/${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,
deskripsi: data.deskripsi,
imagesId: data.imagesId || "",
};
return data;
} else {
throw new Error(result.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading foto:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = fotoForm.safeParse(foto.update.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
foto.update.loading = true;
const response = await fetch(`/api/desa/gallery/foto/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
deskripsi: this.form.deskripsi,
imagesId: this.form.imagesId,
}),
});
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(result.message || "Foto berhasil diupdate");
await foto.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal mengupdate foto");
}
} catch (error) {
console.error("Error updating foto:", error);
toast.error(
error instanceof Error ? error.message : "Gagal mengupdate foto"
);
return false;
} finally {
foto.update.loading = false;
}
},
reset() {
foto.update.id = "";
foto.update.form = { ...defaultFormFoto };
},
},
});
const video = proxy({
create: {
form: { ...defaultFormVideo },
loading: false,
async create() {
const cek = videoForm.safeParse(video.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
video.create.loading = true;
const res = await ApiFetch.api.desa.gallery.video["create"].post(
video.create.form
);
if (res.status === 200) {
video.findMany.load();
return toast.success("Video berhasil disimpan!");
}
return toast.error("Gagal menyimpan video");
} catch (error) {
console.log((error as Error).message);
} finally {
video.create.loading = false;
}
},
resetForm() {
video.create.form = { ...defaultFormVideo };
},
},
findMany: {
data: null as
| Prisma.GalleryVideoGetPayload<{
omit: {
isActive: true;
};
}>[]
| null,
async load() {
const res = await ApiFetch.api.desa.gallery.video["find-many"].get();
if (res.status === 200) {
video.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.GalleryVideoGetPayload<{
omit: {
isActive: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/desa/gallery/video/${id}`);
if (res.ok) {
const data = await res.json();
video.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch video:", res.statusText);
video.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching video:", error);
video.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
video.delete.loading = true;
const response = await fetch(`/api/desa/gallery/video/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok) {
toast.success(result.message || "Video berhasil dihapus");
await video.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus video");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus video");
} finally {
video.delete.loading = false;
}
},
},
update: {
id: "",
form: { ...defaultFormVideo },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/desa/gallery/video/${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,
deskripsi: data.deskripsi,
linkVideo: data.linkVideo,
};
return data;
} else {
throw new Error(result.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading video:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = videoForm.safeParse(video.update.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
video.update.loading = true;
const response = await fetch(`/api/desa/gallery/video/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
deskripsi: this.form.deskripsi,
linkVideo: this.form.linkVideo,
}),
});
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(result.message || "Video berhasil diupdate");
await video.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal mengupdate video");
}
} catch (error) {
console.error("Error updating video:", error);
toast.error(
error instanceof Error ? error.message : "Gagal mengupdate video"
);
return false;
} finally {
video.update.loading = false;
}
},
reset() {
video.update.id = "";
video.update.form = { ...defaultFormVideo };
},
},
});
const stateGallery = proxy({
foto,
video,
});
export default stateGallery;

View File

@@ -0,0 +1,718 @@
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 templateSuratKeteranganForm = z.object({
name: z.string().min(3, "Nama minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
imageId: z.string().nonempty(),
});
const suratKeteranganForm = {
name: "",
deskripsi: "",
imageId: "",
};
const telunjukSaktiDesaForm = {
name: "",
deskripsi: "",
link: "",
};
const templateTelunjukSaktiDesaForm = z.object({
name: z.string().min(3, "Nama minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
});
const templatePelayananPerizinanBerusaha = z.object({
name: z.string().min(3, "Nama minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
link: z.string().min(3, "Link minimal 3 karakter"),
});
type pelayananPerizinanBerusahaForm =
Prisma.PelayananPerizinanBerusahaGetPayload<{
select: {
id: true;
name: true;
deskripsi: true;
link: true;
};
}>;
const pelayananPerizinanBerusahaForm = {
name: "",
deskripsi: "",
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: {
form: { ...suratKeteranganForm },
loading: false,
async create() {
const cek = templateSuratKeteranganForm.safeParse(
suratKeterangan.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
suratKeterangan.create.loading = true;
const res = await ApiFetch.api.desa.layanan.pelayanansuratketerangan[
"create"
].post(suratKeterangan.create.form);
if (res.status === 200) {
suratKeterangan.findMany.load();
return toast.success("Surat Keterangan berhasil disimpan!");
}
return toast.error("Gagal menyimpan surat keterangan");
} catch (error) {
console.log((error as Error).message);
} finally {
suratKeterangan.create.loading = false;
}
},
resetForm() {
suratKeterangan.create.form = { ...suratKeteranganForm };
},
},
findMany: {
data: [] as Prisma.PelayananSuratKeteranganGetPayload<{
include: { image: true };
}>[],
async load() {
const res = await ApiFetch.api.desa.layanan.pelayanansuratketerangan[
"find-many"
].get();
if (res.status === 200) {
suratKeterangan.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.PelayananSuratKeteranganGetPayload<{
include: {
image: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(
`/api/desa/layanan/pelayanansuratketerangan/${id}`
);
if (res.ok) {
const data = await res.json();
suratKeterangan.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch surat keterangan:", res.statusText);
suratKeterangan.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching surat keterangan:", error);
suratKeterangan.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
suratKeterangan.delete.loading = true;
const response = await fetch(
`/api/desa/layanan/pelayanansuratketerangan/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok) {
toast.success(result.message || "Surat Keterangan berhasil dihapus");
await suratKeterangan.findMany.load(); // refresh list
} else {
toast.error(result.message || "Gagal menghapus surat keterangan");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus surat keterangan");
} finally {
suratKeterangan.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...suratKeteranganForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(
`/api/desa/layanan/pelayanansuratketerangan/${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,
deskripsi: data.deskripsi,
imageId: data.imageId || "",
};
return data;
} else {
throw new Error(result.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error fetching surat keterangan:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateSuratKeteranganForm.safeParse(
suratKeterangan.edit.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
suratKeterangan.edit.loading = true;
const response = await fetch(
`/api/desa/layanan/pelayanansuratketerangan/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
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(result.message || "Surat Keterangan berhasil diupdate");
await suratKeterangan.findMany.load(); // refresh list
return true;
} else {
throw new Error(
result.message || "Gagal mengupdate surat keterangan"
);
}
} catch (error) {
console.error("Error updating surat keterangan:", error);
toast.error(
error instanceof Error
? error.message
: "Terjadi kesalahan saat update surat keterangan"
);
return false;
} finally {
suratKeterangan.edit.loading = false;
}
},
},
});
const pelayananTelunjukSaktiDesa = proxy({
create: {
form: { ...telunjukSaktiDesaForm },
loading: false,
async create() {
const cek = templateTelunjukSaktiDesaForm.safeParse(
pelayananTelunjukSaktiDesa.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
pelayananTelunjukSaktiDesa.create.loading = true;
const res = await ApiFetch.api.desa.layanan.pelayanantelunjuksaktidesa[
"create"
].post(pelayananTelunjukSaktiDesa.create.form);
if (res.status === 200) {
pelayananTelunjukSaktiDesa.findMany.load();
return toast.success("Telunjuk Sakti Desa berhasil disimpan!");
}
return toast.error("Gagal menyimpan telunjuk sakti desa");
} catch (error) {
console.log((error as Error).message);
} finally {
pelayananTelunjukSaktiDesa.create.loading = false;
}
},
resetForm() {
pelayananTelunjukSaktiDesa.create.form = { ...telunjukSaktiDesaForm };
},
},
findMany: {
data: [] as Prisma.PelayananTelunjukSaktiDesaGetPayload<{
omit: { isActive: true };
}>[],
async load() {
const res = await ApiFetch.api.desa.layanan.pelayanantelunjuksaktidesa[
"find-many"
].get();
if (res.status === 200) {
pelayananTelunjukSaktiDesa.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.PelayananTelunjukSaktiDesaGetPayload<{
omit: { isActive: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(
`/api/desa/layanan/pelayanantelunjuksaktidesa/${id}`
);
if (res.ok) {
const data = await res.json();
pelayananTelunjukSaktiDesa.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch telunjuk sakti desa:", res.statusText);
pelayananTelunjukSaktiDesa.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching telunjuk sakti desa:", error);
pelayananTelunjukSaktiDesa.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
pelayananTelunjukSaktiDesa.delete.loading = true;
const response = await fetch(
`/api/desa/layanan/pelayanantelunjuksaktidesa/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok) {
toast.success(result.message || "Telunjuk Sakti Desa berhasil dihapus");
await pelayananTelunjukSaktiDesa.findMany.load(); // refresh list
} else {
toast.error(result.message || "Gagal menghapus telunjuk sakti desa");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus telunjuk sakti desa");
} finally {
pelayananTelunjukSaktiDesa.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...telunjukSaktiDesaForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(
`/api/desa/layanan/pelayanantelunjuksaktidesa/${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,
deskripsi: data.deskripsi,
link: data.link,
};
return data;
} else {
throw new Error(result.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error fetching telunjuk sakti desa:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateTelunjukSaktiDesaForm.safeParse(
pelayananTelunjukSaktiDesa.edit.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
pelayananTelunjukSaktiDesa.edit.loading = true;
const response = await fetch(
`/api/desa/layanan/pelayanantelunjuksaktidesa/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
deskripsi: this.form.deskripsi,
link: this.form.link,
}),
}
);
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(result.message || "Telunjuk Sakti Desa berhasil diupdate");
await pelayananTelunjukSaktiDesa.findMany.load(); // refresh list
return true;
} else {
throw new Error(
result.message || "Gagal mengupdate telunjuk sakti desa"
);
}
} catch (error) {
console.error("Error updating telunjuk sakti desa:", error);
toast.error(
error instanceof Error
? error.message
: "Terjadi kesalahan saat update telunjuk sakti desa"
);
return false;
} finally {
pelayananTelunjukSaktiDesa.edit.loading = false;
}
},
},
})
const pelayananPerizinanBerusaha = proxy({
findById: {
data: null as pelayananPerizinanBerusahaForm | null,
loading: false,
initialize() {
pelayananPerizinanBerusaha.findById.data = {
id: "",
name: "",
deskripsi: "",
link: "",
} as pelayananPerizinanBerusahaForm;
},
async load(id: string) {
try {
pelayananPerizinanBerusaha.findById.loading = true;
const res = await fetch(
`/api/desa/layanan/pelayananperizinanberusaha/${id}`
);
if (res.ok) {
const data = await res.json();
pelayananPerizinanBerusaha.findById.data = data.data ?? null;
} else {
console.error(
"Failed to fetch pelayanan perizinan berusaha:",
res.statusText
);
pelayananPerizinanBerusaha.findById.data = null;
}
} catch (error) {
console.error("Error fetching pelayanan perizinan berusaha:", error);
pelayananPerizinanBerusaha.findById.data = null;
}
},
},
update: {
id: "",
form: { ...pelayananPerizinanBerusahaForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak boleh kosong");
return null;
}
try {
const response = await fetch(
`/api/desa/layanan/pelayananperizinanberusaha/${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;
pelayananPerizinanBerusaha.update.id = data.id;
pelayananPerizinanBerusaha.update.form = {
name: data.name,
deskripsi: data.deskripsi,
link: data.link,
};
return data;
} else {
throw new Error(result.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error fetching pelayanan perizinan berusaha:", error);
toast.error(
error instanceof Error
? error.message
: "Gagal memuat data"
);
return null;
}
},
async update(data: pelayananPerizinanBerusahaForm) {
const cek = templatePelayananPerizinanBerusaha.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 {
pelayananPerizinanBerusaha.update.loading = true;
const res = await fetch(
`/api/desa/layanan/pelayananperizinanberusaha/${data.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
}
);
if (res.ok) {
toast.success("Pelayanan perizinan berusaha berhasil diupdate");
await pelayananPerizinanBerusaha.findById.load(data.id);
} else {
toast.error("Gagal mengupdate pelayanan perizinan berusaha");
}
} catch (error) {
console.error("Error updating pelayanan perizinan berusaha:", error);
toast.error(
"Terjadi kesalahan saat mengupdate pelayanan perizinan berusaha"
);
} finally {
pelayananPerizinanBerusaha.update.loading = false;
}
},
},
});
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;

View File

@@ -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;

View File

@@ -52,7 +52,10 @@ const grafikBerdasarkanResponden = proxy({
if (id) {
toast.success("Success create");
grafikBerdasarkanResponden.create.form = {
...defaultForm
sangatbaik: "",
baik: "",
kurangbaik: "",
tidakbaik: "",
};
grafikBerdasarkanResponden.findMany.load();
return id;
@@ -109,7 +112,7 @@ const grafikBerdasarkanResponden = proxy({
form: {...defaultForm},
loading: false,
async byId() {
// Method implementation if needed
},
async submit() {
const id = this.id;

View File

@@ -0,0 +1,72 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
const router = useRouter()
const pathname = usePathname()
const tabs = [
{
label: "Pelayanan Surat Keterangan",
value: "pelayanansuratketerangan",
href: "/admin/desa/layanan/pelayanan_surat_keterangan"
},
{
label: "Pelayanan Perizinan Berusaha",
value: "pelayananperizinanusaha",
href: "/admin/desa/layanan/pelayanan_perizinan_berusaha"
},
{
label: "Pelayanan Telunjuk Sakti Desa",
value: "pelayanantelunjuksaktidesa",
href: "/admin/desa/layanan/pelayanan_telunjuk_sakti_desa"
},
{
label: "Pelayanan Penduduk Non-Permanent",
value: "pelayanantelunjuknonpermanent",
href: "/admin/desa/layanan/pelayanan_penduduk_non_permanent"
}
];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.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 (
<Stack>
<Title order={3}>Layanan</Title>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
{tabs.map((e, i) => (
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
))}
</TabsList>
{tabs.map((e, i) => (
<TabsPanel key={i} value={e.value}>
{/* Konten dummy, bisa diganti tergantung routing */}
<></>
</TabsPanel>
))}
</Tabs>
{children}
</Stack>
);
}
export default LayoutTabsLayanan;

View File

@@ -0,0 +1,119 @@
'use client'
/* eslint-disable react-hooks/exhaustive-deps */
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } 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 EditFoto() {
const fotoState = useProxy(stateGallery.foto)
const router = useRouter();
const params = useParams();
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
useEffect(() => {
const loadFoto = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await fotoState.update.load(id);
if (data) {
if (data?.imageGalleryFoto?.link) {
setPreviewImage(data.imageGalleryFoto.link);
}
}
} catch (error) {
console.error('Error loading foto:', error);
toast.error('Gagal memuat data foto');
}
};
loadFoto();
}, [params?.id]);
const handleSubmit = async () => {
try {
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");
}
fotoState.update.form.imagesId = uploaded.id;
}
await fotoState.update.update();
toast.success('Foto berhasil diperbarui!');
router.push('/admin/desa/gallery/foto');
} catch (error) {
console.error('Error updating foto:', error);
toast.error('Terjadi kesalahan saat memperbarui foto');
}
};
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Foto</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Judul Foto</Text>}
placeholder='Masukkan judul foto'
value={fotoState.update.form.name}
onChange={(e) =>
(fotoState.update.form.name = e.target.value)
}
/>
<FileInput
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar</Text>}
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 ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg={"gray"}>
<IconImageInPicture />
</Center>
)}
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Foto</Text>
<EditEditor
value={fotoState.update.form.deskripsi}
onChange={(val) => {
fotoState.update.form.deskripsi = val;
}}
/>
</Box>
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditFoto;

View File

@@ -0,0 +1,112 @@
'use client'
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
import React from 'react';
import { useProxy } from 'valtio/utils';
import { useState } from 'react';
import { useParams, useRouter } from 'next/navigation';
import { useShallowEffect } from '@mantine/hooks';
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import colors from '@/con/colors';
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
function DetailFoto() {
const fotoState = useProxy(stateGallery.foto)
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter()
useShallowEffect(() => {
fotoState.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
fotoState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/desa/gallery/foto")
}
}
if (!fotoState.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Foto</Text>
{fotoState.findUnique.data ? (
<Paper key={fotoState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Judul</Text>
<Text fz={"lg"}>{fotoState.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Tanggal Foto</Text>
<Text fz={"lg"}>{new Date(fotoState.findUnique.data?.createdAt).toDateString()}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: fotoState.findUnique.data?.deskripsi }} />
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
<Image w={{ base: 300, md: 350}} src={fotoState.findUnique.data?.imageGalleryFoto?.link} alt="gambar" />
</Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (fotoState.findUnique.data) {
setSelectedId(fotoState.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={fotoState.delete.loading || !fotoState.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (fotoState.findUnique.data) {
router.push(`/admin/desa/gallery/foto/${fotoState.findUnique.data.id}/edit`);
}
}}
disabled={!fotoState.findUnique.data}
color={"green"}
>
<IconEdit size={20} />
</Button>
</Flex>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus berita ini?'
/>
</Box>
);
}
export default DetailFoto;

View File

@@ -1,45 +1,108 @@
'use client'
import { KeamananEditor } from '@/app/admin/(dashboard)/keamanan/_com/keamananEditor';
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Group, 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';
function CreateFoto() {
const fotoState = useProxy(stateGallery.foto)
const router = useRouter();
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const resetForm = () => {
fotoState.create.form = {
name: "",
deskripsi: "",
imagesId: "",
};
setPreviewImage(null)
setFile(null)
};
const handleSubmit = async () => {
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");
}
fotoState.create.form.imagesId = uploaded.id;
await fotoState.create.create();
resetForm();
router.push("/admin/desa/gallery/foto")
};
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25}/>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{base: '100%', md: '50%'}} bg={colors['white-1']} p={'md'}>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Foto</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Judul Foto</Text>}
placeholder='Masukkan judul foto'
label={<Text fw={"bold"} fz={"sm"}>Judul Foto</Text>}
placeholder='Masukkan judul foto'
value={fotoState.create.form.name}
onChange={(val) => {
fotoState.create.form.name = val.target.value;
}}
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Tanggal Foto</Text>}
placeholder='Masukkan tanggal foto'
<FileInput
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar</Text>}
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 ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg={"gray"}>
<IconImageInPicture />
</Center>
)}
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Foto</Text>
<KeamananEditor
showSubmit={false}
<CreateEditor
value={fotoState.create.form.deskripsi}
onChange={(val) => {
fotoState.create.form.deskripsi = val;
}}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Stack>
</Paper>
</Box>
</Box>
);
}

View File

@@ -1,62 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Stack, Text } from '@mantine/core';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailFoto() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Foto</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"}>Judul Foto</Text>
<Text>Foto 1</Text>
</Box>
<Box>
<Text fw={"bold"}>Tanggal Foto</Text>
<Text>2022-01-01</Text>
</Box>
<Box>
<Text fw={"bold"}>Deskripsi Foto</Text>
<Text>Deskripsi Foto 1</Text>
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red">
<IconX size={20} />
</Button>
<Button onClick={() => router.push('/admin/desa/gallery/foto/edit')} color="green">
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Hapus
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus potensi ini?"
/> */}
</Box>
);
}
export default DetailFoto;

View File

@@ -1,46 +0,0 @@
'use client'
import { KeamananEditor } from '@/app/admin/(dashboard)/keamanan/_com/keamananEditor';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
function EditFoto() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25}/>
</Button>
</Box>
<Paper w={{base: '100%', md: '50%'}} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Foto</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Judul Foto</Text>}
placeholder='Masukkan judul foto'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Tanggal Foto</Text>}
placeholder='Masukkan tanggal foto'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Foto</Text>
<KeamananEditor
showSubmit={false}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditFoto;

View File

@@ -1,12 +1,29 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { Box, Button, Paper, Skeleton, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import JudulListTab from '../../../_com/jusulListTab';
import { useProxy } from 'valtio/utils';
import stateGallery from '../../../_state/desa/gallery';
import { useShallowEffect } from '@mantine/hooks';
function Foto() {
const fotoState = useProxy(stateGallery.foto)
const router = useRouter();
useShallowEffect(() => {
fotoState.findMany.load()
}, [])
if (!fotoState.findMany.data) {
return (
<Box py={10}>
<Skeleton h={500} />
</Box>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
@@ -26,16 +43,20 @@ function Foto() {
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>Foto 1</TableTd>
<TableTd>2022-01-01</TableTd>
<TableTd>Deskripsi Foto 1</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/desa/gallery/foto/detail')}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
{fotoState.findMany.data?.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>{new Date(item.createdAt).toDateString()}</TableTd>
<TableTd>
<Text dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/desa/gallery/foto/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>

View File

@@ -0,0 +1,10 @@
'use client'
import LayoutTabsGallery from "../../ppid/_com/layoutTabsGallery"
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<LayoutTabsGallery>
{children}
</LayoutTabsGallery>
)
}

View File

@@ -0,0 +1,18 @@
export function convertYoutubeUrlToEmbed(url: string): string | null {
const watchRegex = /(?:https?:\/\/)?(?:www\.)?youtube\.com\/watch\?v=([^&]+)/;
const shortRegex = /(?:https?:\/\/)?youtu\.be\/([^?]+)/;
const matchWatch = url.match(watchRegex);
const matchShort = url.match(shortRegex);
if (matchWatch) {
return `https://www.youtube.com/embed/${matchWatch[1]}`;
}
if (matchShort) {
return `https://www.youtube.com/embed/${matchShort[1]}`;
}
return null;
}

View File

@@ -0,0 +1,33 @@
// components/YoutubeEmbed.tsx
"use client";
import { Box, Text } from "@mantine/core";
type YoutubeEmbedProps = {
url?: string;
showRawUrl?: boolean; // opsional, buat nampilin URL mentahnya
};
export default function YoutubeEmbed({ url, showRawUrl = false }: YoutubeEmbedProps) {
if (!url || !url.includes("embed")) {
return <Text c="red">Link embed Youtube tidak valid</Text>;
}
return (
<Box>
<Box
component="iframe"
src={url}
width="100%"
height={300}
allowFullScreen
style={{ borderRadius: 8 }}
/>
{showRawUrl && (
<Text fz="sm" c="dimmed" mt={5}>
{url}
</Text>
)}
</Box>
);
}

View File

@@ -1,35 +0,0 @@
import colors from '@/con/colors';
import { Box, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import { IconPhoto, IconVideo } from '@tabler/icons-react';
import Foto from './foto/page';
import Video from './video/page';
function Gallery() {
return (
<Box>
<Stack gap={"xs"}>
<Title order={3}>Gallery</Title>
<Tabs color={colors['blue-button']} variant="pills" defaultValue="foto">
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
<TabsTab value="foto" leftSection={<IconPhoto size={12} />}>
Foto
</TabsTab>
<TabsTab value="video" leftSection={<IconVideo size={12} />}>
Video
</TabsTab>
</TabsList>
<TabsPanel value="foto">
<Foto/>
</TabsPanel>
<TabsPanel value="video">
<Video/>
</TabsPanel>
</Tabs>
</Stack>
</Box>
);
}
export default Gallery;

View File

@@ -0,0 +1,142 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
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';
import { convertYoutubeUrlToEmbed } from '../../../lib/youtube-utils';
function EditVideo() {
const router = useRouter();
const [embedLink, setEmbedLink] = useState("");
const videoState = useProxy(stateGallery.video)
const params = useParams()
const [formData, setFormData] = useState({
name: videoState.findUnique.data?.name || '',
deskripsi: videoState.findUnique.data?.deskripsi || '',
linkVideo: videoState.findUnique.data?.linkVideo || '',
})
useEffect(() => {
const loadVideo = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await videoState.update.load(id);
if (data) {
setFormData({
name: data.name || '',
deskripsi: data.deskripsi || '',
linkVideo: data.linkVideo || '',
});
const embed = convertYoutubeUrlToEmbed(data.linkVideo);
setEmbedLink(embed || "");
}
} catch (error) {
console.error('Error loading video:', error);
toast.error('Gagal memuat data video');
}
};
loadVideo();
}, [params?.id]);
const handleSubmit = async () => {
try {
videoState.update.form = {
...videoState.update.form,
name: formData.name,
deskripsi: formData.deskripsi,
linkVideo: formData.linkVideo,
};
const converted = convertYoutubeUrlToEmbed(formData.linkVideo);
if (!converted) {
toast.error("Link YouTube tidak valid. Pastikan formatnya benar.");
return;
}
await videoState.update.update();
toast.success('Video berhasil diperbarui!');
router.push('/admin/desa/gallery/video');
} catch (error) {
console.error('Error updating video:', error);
toast.error('Terjadi kesalahan saat memperbarui video');
}
}
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Video</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Judul Video</Text>}
placeholder='Masukkan judul video'
value={formData.name}
onChange={(val) => {
setFormData({
...formData,
name: val.target.value,
})
}}
/>
<Box>
<TextInput
label="Link Video YouTube"
placeholder="https://www.youtube.com/watch?v=abc123"
value={formData.linkVideo}
onChange={(e) => {
setFormData({
...formData,
linkVideo: e.currentTarget.value,
})
const embed = convertYoutubeUrlToEmbed(e.currentTarget.value);
setEmbedLink(embed || "");
}}
required
/>
{embedLink && (
<iframe
className="rounded"
width="100%"
height="200"
src={embedLink}
title="Preview Video"
allowFullScreen
></iframe>
)}
</Box>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Video</Text>
<EditEditor
value={formData.deskripsi}
onChange={(val) => {
setFormData({
...formData,
deskripsi: val,
})
}}
/>
</Box>
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditVideo;

View File

@@ -0,0 +1,133 @@
'use client'
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
function DetailVideo() {
const videoState = useProxy(stateGallery.video)
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter()
useShallowEffect(() => {
videoState.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
videoState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/desa/gallery/video")
}
}
if (!videoState.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Video</Text>
{videoState.findUnique.data ? (
<Paper key={videoState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Judul</Text>
<Text fz={"lg"}>{videoState.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Video</Text>
<Box component="iframe"
src={convertToEmbedUrl(videoState.findUnique.data?.linkVideo)}
width="100%"
height={300}
allowFullScreen
style={{ borderRadius: 8 }}
/>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Tanggal Video</Text>
<Text fz={"lg"}>{new Date(videoState.findUnique.data?.createdAt).toDateString()}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: videoState.findUnique.data?.deskripsi }} />
</Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (videoState.findUnique.data) {
setSelectedId(videoState.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={videoState.delete.loading || !videoState.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (videoState.findUnique.data) {
router.push(`/admin/desa/gallery/video/${videoState.findUnique.data.id}/edit`);
}
}}
disabled={!videoState.findUnique.data}
color={"green"}
>
<IconEdit size={20} />
</Button>
</Flex>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus berita ini?'
/>
</Box>
);
function convertToEmbedUrl(youtubeUrl: string): string {
try {
const url = new URL(youtubeUrl);
const videoId = url.searchParams.get("v");
if (!videoId) return youtubeUrl;
return `https://www.youtube.com/embed/${videoId}`;
} catch (err) {
console.error("Error converting YouTube URL to embed:", err);
return youtubeUrl;
}
}
}
export default DetailVideo;

View File

@@ -1,45 +1,99 @@
'use client'
import { KeamananEditor } from '@/app/admin/(dashboard)/keamanan/_com/keamananEditor';
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
import colors from '@/con/colors';
import { Box, Button, 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';
import { convertYoutubeUrlToEmbed } from '../../lib/youtube-utils';
function CreateVideo() {
const videoState = useProxy(stateGallery.video)
const router = useRouter();
const [link, setLink] = useState("");
const [embedLink, setEmbedLink] = useState("");
const resetForm = () => {
videoState.create.form = {
name: "",
deskripsi: "",
linkVideo: "",
};
};
const handleSubmit = async () => {
const converted = convertYoutubeUrlToEmbed(videoState.create.form.linkVideo);
if (!converted) {
toast.error("Link YouTube tidak valid. Pastikan formatnya benar.");
return;
}
await videoState.create.create();
resetForm();
router.push("/admin/desa/gallery/video")
};
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25}/>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{base: '100%', md: '50%'}} bg={colors['white-1']} p={'md'}>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Video</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Judul Video</Text>}
placeholder='Masukkan judul video'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Tanggal Video</Text>}
placeholder='Masukkan tanggal video'
label={<Text fw={"bold"} fz={"sm"}>Judul Video</Text>}
placeholder='Masukkan judul video'
value={videoState.create.form.name}
onChange={(val) => {
videoState.create.form.name = val.target.value;
}}
/>
<Box>
<Stack gap={"xs"}>
<TextInput
label="Link Video YouTube"
placeholder="https://www.youtube.com/watch?v=abc123"
value={link}
onChange={(e) => {
setLink(e.currentTarget.value);
const embed = convertYoutubeUrlToEmbed(e.currentTarget.value);
setEmbedLink(embed || "");
}}
required
/>
{embedLink && (
<iframe
style={{ borderRadius: 10, width: "100%", height: 400 }}
src={embedLink}
title="Preview Video"
allowFullScreen
></iframe>
)}
</Stack>
</Box>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Video</Text>
<KeamananEditor
showSubmit={false}
<CreateEditor
value={videoState.create.form.deskripsi}
onChange={(val) => {
videoState.create.form.deskripsi = val;
}}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Stack>
</Paper>
</Box>
</Box>
);
}

View File

@@ -1,62 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Stack, Text } from '@mantine/core';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailVideo() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Video</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"}>Judul Video</Text>
<Text>Video 1</Text>
</Box>
<Box>
<Text fw={"bold"}>Tanggal Video</Text>
<Text>2022-01-01</Text>
</Box>
<Box>
<Text fw={"bold"}>Deskripsi Video</Text>
<Text>Deskripsi Video 1</Text>
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red">
<IconX size={20} />
</Button>
<Button onClick={() => router.push('/admin/desa/gallery/video/edit')} color="green">
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Hapus
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus potensi ini?"
/> */}
</Box>
);
}
export default DetailVideo;

View File

@@ -1,46 +0,0 @@
'use client'
import { KeamananEditor } from '@/app/admin/(dashboard)/keamanan/_com/keamananEditor';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
function EditVideo() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25}/>
</Button>
</Box>
<Paper w={{base: '100%', md: '50%'}} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Video</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Judul Video</Text>}
placeholder='Masukkan judul video'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Tanggal Video</Text>}
placeholder='Masukkan tanggal video'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Video</Text>
<KeamananEditor
showSubmit={false}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditVideo;

View File

@@ -1,12 +1,29 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { Box, Button, Paper, Skeleton, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import JudulListTab from '../../../_com/jusulListTab';
import { useProxy } from 'valtio/utils';
import stateGallery from '../../../_state/desa/gallery';
import { useShallowEffect } from '@mantine/hooks';
function Video() {
const videoState = useProxy(stateGallery.video)
const router = useRouter();
useShallowEffect(() => {
videoState.findMany.load()
}, [])
if (!videoState.findMany.data) {
return (
<Box py={10}>
<Skeleton h={500} />
</Box>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
@@ -26,16 +43,20 @@ function Video() {
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>Video 1</TableTd>
<TableTd>2022-01-01</TableTd>
<TableTd>Deskripsi Video 1</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/desa/gallery/video/detail')}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
{videoState.findMany.data?.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>{new Date(item.createdAt).toDateString()}</TableTd>
<TableTd>
<Text dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/desa/gallery/video/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>

View File

@@ -0,0 +1,10 @@
'use client'
import LayoutTabsLayanan from "../_com/layoutTabLayanan";
export default function Layout({children} : {children: React.ReactNode}) {
return (
<LayoutTabsLayanan>
{children}
</LayoutTabsLayanan>
)
}

View File

@@ -1,48 +0,0 @@
import colors from '@/con/colors';
import { Box, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import SuratKeterangan from './ui/surat_keterangan/page';
import PerizinanUsaha from './ui/perizinan_usaha/page';
import TelunjukSaktiDesa from './ui/telunjuk_sakti_desa/page';
import PendudukNonPermanent from './ui/penduduk_non_permanent/page';
function Page() {
return (
<Box py={10}>
<Stack >
<Title order={3}>Layanan</Title>
<Tabs color={colors['blue-button']} variant='pills' defaultValue={"Pelayanan Surat Keterangan"}>
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
<TabsTab value="Pelayanan Surat Keterangan">
Pelayanan Surat Keterangan
</TabsTab>
<TabsTab value="Pelayanan Perizinan Berusaha">
Pelayanan Perizinan Berusaha
</TabsTab>
<TabsTab value="Pelayanan Telunjuk Sakti Desa">
Pelayanan Telunjuk Sakti Desa
</TabsTab>
<TabsTab value="Pelayanan Penduduk Non-Permanent">
Pelayanan Penduduk Non-Permanent
</TabsTab>
</TabsList>
<TabsPanel value="Pelayanan Surat Keterangan">
<SuratKeterangan />
</TabsPanel>
<TabsPanel value="Pelayanan Perizinan Berusaha">
<PerizinanUsaha />
</TabsPanel>
<TabsPanel value="Pelayanan Telunjuk Sakti Desa">
<TelunjukSaktiDesa />
</TabsPanel>
<TabsPanel value="Pelayanan Penduduk Non-Permanent">
<PendudukNonPermanent />
</TabsPanel>
</Tabs>
</Stack>
</Box>
);
}
export default Page;

View File

@@ -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 (
<Box>
<Stack gap={'xs'}>
<Box>
<Button
variant={'subtle'}
onClick={() => router.back()}
>
<IconArrowBack color={colors['blue-button']} size={20} />
</Button>
</Box>
<Box>
<Paper bg={colors['white-1']} p={'md'} radius={10} w={{ base: '100%', md: '50%' }}>
<Stack gap={'xs'}>
<Title order={3}>Edit Pelayanan Penduduk Non Permanent</Title>
<Text fw={"bold"}>Judul</Text>
<TextInput
value={formData.name}
onChange={(val) => {
setFormData({
...formData,
name: val.target.value,
})
}}
/>
<Text fw={"bold"}>Deskripsi</Text>
<EditEditor
value={formData.deskripsi}
onChange={(val) => {
setFormData({
...formData,
deskripsi: val,
})
}}
/>
<Group>
<Button
bg={colors['blue-button']}
onClick={handleSubmit}
loading={statePendudukNonPermanent.update.loading}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
</Stack>
</Box>
);
}
export default EditPelayananPendudukNonPermanent;

View File

@@ -0,0 +1,51 @@
'use client'
import colors from '@/con/colors';
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 (
<Stack>
<Skeleton radius={10} h={800} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Paper bg={colors['BG-trans']} p={'md'}>
<Box py={15}>
<Stack gap={"xs"}>
<Grid>
<GridCol span={{ base: 12, md: 11 }}>
<Text fz={"h4"} fw={"bold"}>Preview Pelayanan Perizinan Berusaha</Text>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button bg={colors['blue-button']} onClick={() => router.push('/admin/desa/layanan/pelayanan_penduduk_non_permanent/edit')}>
<IconEdit size={16} />
</Button>
</GridCol>
</Grid>
</Stack>
</Box>
<Text fz={{ base: "h4", md: 'h2' }} fw={"bold"}>{pelayananPendudukNonPermanen.findById.data.name}</Text>
<Text py={10} ta={"justify"} fz={{ base: "sm", md: 'h3' }} dangerouslySetInnerHTML={{ __html: pelayananPendudukNonPermanen.findById.data.deskripsi }} />
</Paper>
</Paper>
</Box>
);
}
export default SuratKeterangan;

View File

@@ -0,0 +1,116 @@
'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 EditPelayananPerizinanBerusaha() {
const router = useRouter();
const params = useParams()
const statePerizinanBerusaha = useProxy(stateLayananDesa.pelayananPerizinanBerusaha)
const [formData, setFormData] = useState({
name: statePerizinanBerusaha.findById.data?.name || '',
deskripsi: statePerizinanBerusaha.findById.data?.deskripsi || '',
link: statePerizinanBerusaha.findById.data?.link || '',
})
useEffect(() => {
const loadPelayananPerizinan = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await statePerizinanBerusaha.update.load(id);
if (data) {
setFormData({
name: data.name || '',
deskripsi: data.deskripsi || '',
link: data.link || '',
});
}
} 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 (statePerizinanBerusaha.findById.data) {
statePerizinanBerusaha.findById.data.name = formData.name;
statePerizinanBerusaha.findById.data.deskripsi = formData.deskripsi;
statePerizinanBerusaha.findById.data.link = formData.link;
statePerizinanBerusaha.update.update(statePerizinanBerusaha.findById.data)
}
router.push('/admin/desa/layanan/pelayanan_perizinan_berusaha')
}
return (
<Box>
<Stack gap={'xs'}>
<Box>
<Button
variant={'subtle'}
onClick={() => router.back()}
>
<IconArrowBack color={colors['blue-button']} size={20} />
</Button>
</Box>
<Box>
<Paper bg={colors['white-1']} p={'md'} radius={10} w={{ base: '100%', md: '50%' }}>
<Stack gap={'xs'}>
<Title order={3}>Edit Pelayanan Perizinan Berusaha</Title>
<Text fw={"bold"}>Judul</Text>
<TextInput
value={formData.name}
onChange={(val) => {
setFormData({
...formData,
name: val.target.value,
})
}}
/>
<Text fw={"bold"}>Link</Text>
<TextInput
value={formData.link}
onChange={(val) => {
setFormData({
...formData,
link: val.target.value,
})
}}
/>
<Text fw={"bold"}>Deskripsi</Text>
<EditEditor
value={formData.deskripsi}
onChange={(val) => {
setFormData({
...formData,
deskripsi: val,
})
}}
/>
<Group>
<Button
bg={colors['blue-button']}
onClick={handleSubmit}
loading={statePerizinanBerusaha.update.loading}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
</Stack>
</Box>
);
}
export default EditPelayananPerizinanBerusaha;

View File

@@ -0,0 +1,97 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Grid, GridCol, Group, Paper, Skeleton, Stack, Stepper, StepperCompleted, StepperStep, Text } from '@mantine/core';
import { IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import stateLayananDesa from '../../../_state/desa/layananDesa';
import { useProxy } from 'valtio/utils';
import { useShallowEffect } from '@mantine/hooks';
function PerizinanBerusaha() {
const router = useRouter()
const pelayananPerizinanBerusaha = useProxy(stateLayananDesa.pelayananPerizinanBerusaha)
const [active, setActive] = useState(1);
const nextStep = () => setActive((current) => (current < 6 ? current + 1 : current));
const prevStep = () => setActive((current) => (current > 0 ? current - 1 : current));
useShallowEffect(() => {
pelayananPerizinanBerusaha.findById.load('1')
}, [])
if(!pelayananPerizinanBerusaha.findById.data) {
return (
<Stack>
<Skeleton radius={10} h={800} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Paper bg={colors['BG-trans']} p={'md'}>
<Box py={15}>
<Stack gap={"xs"}>
<Grid>
<GridCol span={{ base: 12, md: 11 }}>
<Text fz={"h4"} fw={"bold"}>Preview Pelayanan Perizinan Berusaha</Text>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button bg={colors['blue-button']} onClick={() => router.push('/admin/desa/layanan/pelayanan_perizinan_berusaha/edit')}>
<IconEdit size={16} />
</Button>
</GridCol>
</Grid>
</Stack>
</Box>
<Text fz={{ base: "h4", md: 'h2' }} fw={"bold"}>{pelayananPerizinanBerusaha.findById.data.name}</Text>
<Text py={10} ta={"justify"} fz={{ base: "sm", md: 'h3' }} dangerouslySetInnerHTML={{__html: pelayananPerizinanBerusaha.findById.data.deskripsi}} />
<Text py={10} fz={{ base: "sm", md: 'h3' }}>Proses pendaftaran NIB melalui OSS mencakup beberapa langkah umum, seperti:</Text>
<Box p={"xl"} w={{ base: "100%", md: "100%" }} >
<Stepper active={active} onStepClick={setActive} orientation="vertical"
styles={{
separator: {
marginLeft: 25
},
step: {
padding: '12px 0'
}
}}>
<StepperStep label="Langkah Pertama" description="Pendaftaran Akun">
Pendaftaran akun pada portal OSS
</StepperStep>
<StepperStep label="Langkah Kedua" description="Pengisian Data Perusahaan">
Mengisi informasi perusahaan, termasuk data pemegang saham, alamat perusahaan, dan lainnya
</StepperStep>
<StepperStep label="Langkah Ketiga" description="Pemilihan KBLI ">
Memilih KBLI dengan jenis usaha yang akan didaftarkan
</StepperStep>
<StepperStep label="Langkah Keempat" description="Pengunggahan Dokumen">
Mengunggah dokumen-dokumen yang diperlukan, seperti akta pendirian perusahaan, surat izin usaha, dan dokumen lainnya sesuai dengan ketentuan yang berlaku
</StepperStep>
<StepperStep label="Langkah Kelima" description="Verifikasi dan Persetujuan">
Proses verifikasi dan persetujuan oleh instansi terkait
</StepperStep>
<StepperStep label="Langkah Keenam" description="Penerimaan NIB">
Jika proses sebelumnya berhasil, perusahaan akan menerima NIB sebagai identitas resmi usaha anda
</StepperStep>
<StepperCompleted >
Selesai, anda telah mengikuti proses pendaftaran NIB melalui OSS
</StepperCompleted>
</Stepper>
<Group justify="center" mt="xl">
<Button variant="default" onClick={prevStep}>Back</Button>
<Button onClick={nextStep}>Next step</Button>
</Group>
<Text py={35} ta={"justify"} fz={{ base: "sm", md: 'h3' }}>Penting untuk diingat bahwa prosedur dan persyaratan dapat berubah
seiring waktu. Untuk informasi yang lebih akurat dan terkini, saya sarankan untuk mengunjungi situs
resmi OSS <a href={pelayananPerizinanBerusaha.findById.data.link}>{pelayananPerizinanBerusaha.findById.data.link}</a> atau menghubungi instansi terkait di pemerintah Indonesia yang bertanggung jawab atas urusan perizinan usaha.</Text>
</Box>
</Paper>
</Paper>
</Box>
);
}
export default PerizinanBerusaha;

View File

@@ -0,0 +1,132 @@
'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 ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } 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 EditSuratKeterangan() {
const router = useRouter()
const params = useParams()
const stateSurat = useProxy(stateLayananDesa.suratKeterangan)
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({
name: stateSurat.edit.form.name,
deskripsi: stateSurat.edit.form.deskripsi,
imageId: stateSurat.edit.form.imageId,
})
useEffect(() => {
const loadSurat = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await stateSurat.edit.load(id);
if (data) {
setFormData({
name: data.name,
deskripsi: data.deskripsi,
imageId: data.imageId,
});
if (data?.image?.link) {
setPreviewImage(data.image.link);
}
}
} catch (error) {
console.error("Error loading surat:", error);
toast.error("Gagal memuat data surat");
}
};
loadSurat();
}, [params?.id]);
const handleSubmit = async () => {
try {
stateSurat.edit.form = {
...stateSurat.edit.form,
name: formData.name,
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");
}
stateSurat.edit.form.imageId = uploaded.id;
}
await stateSurat.edit.update()
toast.success("Surat berhasil diperbarui!")
router.push("/admin/desa/layanan/pelayanan_surat_keterangan")
} catch (error) {
console.error("Error updating surat:", error);
toast.error("Terjadi kesalahan saat memperbarui surat");
}
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Edit Surat Keterangan</Title>
<TextInput
value={formData.name}
onChange={(val) => {
setFormData({ ...formData, name: val.target.value });
}}
label={<Text fz={"sm"} fw={"bold"}>Nama Surat Keterangan</Text>}
placeholder="masukkan nama surat keterangan"
/>
<Box>
<Text fz={"sm"} fw={"bold"}>Konten</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
setFormData({ ...formData, deskripsi: htmlContent });
}}
/>
</Box>
<FileInput
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar Konten</Text>}
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 ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg={"gray"}>
<IconImageInPicture />
</Center>
)}
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
</Stack>
</Paper>
</Box>
);
}
export default EditSuratKeterangan;

View File

@@ -0,0 +1,109 @@
'use client'
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors';
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
function DetailSuratKeterangan() {
const suratKeteranganState = useProxy(stateLayananDesa.suratKeterangan)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter()
useShallowEffect(() => {
suratKeteranganState.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
suratKeteranganState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/desa/layanan/pelayanan_surat_keterangan")
}
}
if (!suratKeteranganState.findUnique.data) {
return (
<Stack py={10}>
{Array.from({ length: 10 }).map((_, k) => (
<Skeleton key={k} h={40} />
))}
</Stack>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Surat Keterangan</Text>
{suratKeteranganState.findUnique.data ? (
<Paper key={suratKeteranganState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Nama</Text>
<Text fz={"lg"}>{suratKeteranganState.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
<Text fz={"lg"}dangerouslySetInnerHTML={{ __html: suratKeteranganState.findUnique.data?.deskripsi }}></Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
<Image w={{ base: 150, md: 150, lg: 150 }} src={suratKeteranganState.findUnique.data?.image?.link} alt="gambar" />
</Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (suratKeteranganState.findUnique.data) {
setSelectedId(suratKeteranganState.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={suratKeteranganState.delete.loading || !suratKeteranganState.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (suratKeteranganState.findUnique.data) {
router.push(`/admin/desa/layanan/pelayanan_surat_keterangan/${suratKeteranganState.findUnique.data.id}/edit`);
}
}}
disabled={!suratKeteranganState.findUnique.data}
color={"green"}
>
<IconEdit size={20} />
</Button>
</Flex>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus berita ini?'
/>
</Box>
);
}
export default DetailSuratKeterangan;

View File

@@ -0,0 +1,104 @@
'use client'
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
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';
function CreateSuratKeterangan() {
const stateSurat = useProxy(stateLayananDesa.suratKeterangan)
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const router = useRouter()
const resetForm = () => {
stateSurat.create.form = {
name: "",
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")
}
stateSurat.create.form.imageId = uploaded.id
await stateSurat.create.create()
resetForm()
router.push("/admin/desa/layanan/pelayanan_surat_keterangan")
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Create Surat Keterangan</Title>
<TextInput
value={stateSurat.create.form.name}
onChange={(val) => {
stateSurat.create.form.name = val.target.value;
}}
label={<Text fz={"sm"} fw={"bold"}>Nama Surat Keterangan</Text>}
placeholder="masukkan nama surat keterangan"
/>
<Box>
<Text fz={"sm"} fw={"bold"}>Konten</Text>
<CreateEditor
value={stateSurat.create.form.deskripsi}
onChange={(htmlContent) => {
stateSurat.create.form.deskripsi = htmlContent;
}}
/>
</Box>
<FileInput
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar Konten</Text>}
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 ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg={"gray"}>
<IconImageInPicture />
</Center>
)}
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
</Stack>
</Paper>
</Box>
);
}
export default CreateSuratKeterangan;

View File

@@ -0,0 +1,71 @@
'use client'
import JudulListTab from '@/app/admin/(dashboard)/_com/jusulListTab';
import colors from '@/con/colors';
import { Box, Button, Image, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useProxy } from 'valtio/utils';
import stateLayananDesa from '../../../_state/desa/layananDesa';
import { useShallowEffect } from '@mantine/hooks';
import { useRouter } from 'next/navigation';
function SuratKeterangan() {
const suratKeteranganState = useProxy(stateLayananDesa.suratKeterangan)
const router = useRouter()
useShallowEffect(() => {
suratKeteranganState.findMany.load()
}, [])
if (!suratKeteranganState.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulListTab
title='List Surat Keterangan'
href='/admin/desa/layanan/pelayanan_surat_keterangan/create'
placeholder='pencarian'
searchIcon={<IconSearch size={16} />}
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Image</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{suratKeteranganState.findMany.data?.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd>
<TableTd>
<Image w={100} src={item.image?.link} alt="gambar" />
</TableTd>
<TableTd>
<Text>
<Button onClick={() => router.push(`/admin/desa/layanan/pelayanan_surat_keterangan/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</Text>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>
</Box>
);
}
export default SuratKeterangan;

View File

@@ -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, 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 EditPelayananTelunjukSakti() {
const stateTelunjukDesa = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa)
const router = useRouter()
const params = useParams()
const [formData, setFormData] = useState({
name: stateTelunjukDesa.edit.form.name,
deskripsi: stateTelunjukDesa.edit.form.deskripsi,
link: stateTelunjukDesa.edit.form.link,
})
useEffect(() => {
const loadPelayananTelunjukSakti = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await stateTelunjukDesa.edit.load(id);
if (data) {
setFormData({
name: data.name,
deskripsi: data.deskripsi,
link: data.link,
});
}
} catch (error) {
console.error("Error loading pelayanan telunjuk sakti:", error);
toast.error("Gagal memuat data pelayanan telunjuk sakti");
}
};
loadPelayananTelunjukSakti();
}, [params?.id]);
const handleSubmit = async () => {
try {
stateTelunjukDesa.edit.form = {
...stateTelunjukDesa.edit.form,
name: formData.name,
deskripsi: formData.deskripsi,
link: formData.link,
}
await stateTelunjukDesa.edit.update()
toast.success("Pelayanan telunjuk sakti berhasil diperbarui!")
router.push("/admin/desa/layanan/pelayanan_telunjuk_sakti_desa")
} catch (error) {
console.error("Error updating pelayanan telunjuk sakti:", error);
toast.error("Terjadi kesalahan saat memperbarui pelayanan telunjuk sakti");
}
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Edit Surat Keterangan</Title>
<TextInput
value={formData.name}
onChange={(val) => {
setFormData({ ...formData, name: val.target.value });
}}
label={<Text fz={"sm"} fw={"bold"}>Nama Surat Keterangan</Text>}
placeholder="masukkan nama surat keterangan"
/>
<TextInput
value={formData.link}
onChange={(val) => {
setFormData({ ...formData, link: val.target.value });
}}
label={<Text fz={"sm"} fw={"bold"}>Link</Text>}
placeholder="masukkan link"
/>
<Box>
<Text fz={"sm"} fw={"bold"}>Konten</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
setFormData({ ...formData, deskripsi: htmlContent });
}}
/>
</Box>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
</Stack>
</Paper>
</Box>
);
}
export default EditPelayananTelunjukSakti;

View File

@@ -0,0 +1,109 @@
'use client'
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
function DetailPelayananTelunjukSakti() {
const telunjukSaktiState = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter()
useShallowEffect(() => {
telunjukSaktiState.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
telunjukSaktiState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/desa/layanan/pelayanan_telunjuk_sakti_desa")
}
}
if (!telunjukSaktiState.findUnique.data) {
return (
<Stack py={10}>
{Array.from({ length: 10 }).map((_, k) => (
<Skeleton key={k} h={40} />
))}
</Stack>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Pelayanan Telunjuk Sakti Desa</Text>
{telunjukSaktiState.findUnique.data ? (
<Paper key={telunjukSaktiState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Nama</Text>
<Text fz={"lg"}>{telunjukSaktiState.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Link</Text>
<Text fz={"lg"}>{telunjukSaktiState.findUnique.data?.link}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
<Text fz={"lg"}dangerouslySetInnerHTML={{ __html: telunjukSaktiState.findUnique.data?.deskripsi }}></Text>
</Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (telunjukSaktiState.findUnique.data) {
setSelectedId(telunjukSaktiState.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={telunjukSaktiState.delete.loading || !telunjukSaktiState.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (telunjukSaktiState.findUnique.data) {
router.push(`/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/${telunjukSaktiState.findUnique.data.id}/edit`);
}
}}
disabled={!telunjukSaktiState.findUnique.data}
color={"green"}
>
<IconEdit size={20} />
</Button>
</Flex>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus berita ini?'
/>
</Box>
);
}
export default DetailPelayananTelunjukSakti;

View File

@@ -0,0 +1,70 @@
'use client'
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
function CreatePelayananTelunjukDesa() {
const stateTelunjukDesa = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa)
const router = useRouter()
const resetForm = () => {
stateTelunjukDesa.create.form = {
name: "",
deskripsi: "",
link: "",
}
}
const handleSubmit = async () => {
await stateTelunjukDesa.create.create()
resetForm()
router.push("/admin/desa/layanan/pelayanan_telunjuk_sakti_desa")
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Create Pelayanan Telunjuk Sakti Desa</Title>
<TextInput
value={stateTelunjukDesa.create.form.name}
onChange={(val) => {
stateTelunjukDesa.create.form.name = val.target.value;
}}
label={<Text fz={"sm"} fw={"bold"}>Nama Pelayanan Telunjuk Sakti Desa</Text>}
placeholder="masukkan nama pelayanan telunjuk sakti desa"
/>
<TextInput
value={stateTelunjukDesa.create.form.link}
onChange={(val) => {
stateTelunjukDesa.create.form.link = val.target.value;
}}
label={<Text fz={"sm"} fw={"bold"}>Link</Text>}
placeholder="masukkan link"
/>
<Box>
<Text fz={"sm"} fw={"bold"}>Konten</Text>
<CreateEditor
value={stateTelunjukDesa.create.form.deskripsi}
onChange={(htmlContent) => {
stateTelunjukDesa.create.form.deskripsi = htmlContent;
}}
/>
</Box>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
</Stack>
</Paper>
</Box>
);
}
export default CreatePelayananTelunjukDesa;

View File

@@ -0,0 +1,65 @@
'use client'
import JudulListTab from '@/app/admin/(dashboard)/_com/jusulListTab';
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useProxy } from 'valtio/utils';
import stateLayananDesa from '../../../_state/desa/layananDesa';
import { useShallowEffect } from '@mantine/hooks';
import { useRouter } from 'next/navigation';
function PelayananTelunjukSakti() {
const telunjukSaktiState = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa)
const router = useRouter()
useShallowEffect(() => {
telunjukSaktiState.findMany.load()
}, [])
if (!telunjukSaktiState.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulListTab
title='List Pelayanan Telunjuk Sakti Desa'
href='/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/create'
placeholder='pencarian'
searchIcon={<IconSearch size={16} />}
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{telunjukSaktiState.findMany.data?.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd><Text truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} /></TableTd>
<TableTd>
<Text>
<Button onClick={() => router.push(`/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</Text>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>
</Box>
);
}
export default PelayananTelunjukSakti;

View File

@@ -1,17 +0,0 @@
import colors from '@/con/colors';
import { Box, Paper, Stack, Title } from '@mantine/core';
import React from 'react';
function ListPendudukNonPermanent() {
return (
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<Title order={3}>List Penduduk Non-Permanent</Title>
</Stack>
</Paper>
</Box>
);
}
export default ListPendudukNonPermanent;

View File

@@ -1,35 +0,0 @@
import colors from '@/con/colors';
import { Box, SimpleGrid, Paper, Stack, Title, Group, Button, Text } from '@mantine/core';
import React from 'react';
import { DesaEditor } from '../../../_com/desaEditor';
import ListPendudukNonPermanent from './listPage';
function PendudukNonPermanent() {
return (
<Box py={10}>
<SimpleGrid cols={{ base: 1, md: 2 }}>
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<Title order={3}>Penduduk Non-Permanent</Title>
<Text fw={"bold"}>Deskripsi Penduduk Non-Permanent</Text>
<DesaEditor showSubmit={false} />
<Group>
<Button
mt={10}
bg={colors['blue-button']}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
<ListPendudukNonPermanent />
</SimpleGrid>
</Box>
);
}
export default PendudukNonPermanent;

View File

@@ -1,17 +0,0 @@
import colors from '@/con/colors';
import { Box, Paper, Stack, Title } from '@mantine/core';
import React from 'react';
function ListPerizinanUsaha() {
return (
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={3}>List Perizinan Usaha</Title>
</Stack>
</Paper>
</Box>
);
}
export default ListPerizinanUsaha;

View File

@@ -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 ListPerizinanUsaha from './listPage';
import { DesaEditor } from '../../../_com/desaEditor';
function PerizinanUsaha() {
return (
<Box py={10}>
<Stack gap={"xs"}>
<SimpleGrid cols={{ base: 1, md: 2 }}>
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={3}>Pelayanan Perizinan Usaha</Title>
<Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi Perizinan Usaha</Text>
<DesaEditor
showSubmit={false}
/>
</Box>
<Group>
<Button
mt={10}
bg={colors['blue-button']}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
<ListPerizinanUsaha />
</SimpleGrid>
</Stack>
</Box>
);
}
export default PerizinanUsaha;

View File

@@ -1,17 +0,0 @@
import colors from '@/con/colors';
import { Box, Paper, Stack, Title } from '@mantine/core';
import React from 'react';
function ListSuratKeterangan() {
return (
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<Title order={3}>List Surat Keterangan</Title>
</Stack>
</Paper>
</Box>
);
}
export default ListSuratKeterangan;

View File

@@ -1,48 +0,0 @@
import colors from '@/con/colors';
import { Box, SimpleGrid, Paper, Stack, Title, Button, Group, TextInput, Text, Center, Flex } from '@mantine/core';
import { IconUpload } from '@tabler/icons-react';
import React from 'react';
import ListSuratKeterangan from './listPage';
function SuratKeterangan() {
return (
<Box py={10}>
<Stack gap={"xs"}>
<SimpleGrid cols={{ base: 1, md: 2 }}>
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={3}>Pelayanan Surat Keterangan</Title>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Nama Surat Keterangan</Text>}
placeholder='masukkan nama surat keterangan'
/>
<Text fz={"sm"} fw={"bold"}>Upload Gambar Surat Keterangan</Text>
<Box bg={colors['BG-trans']} p={"md"}>
<Center>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Center>
</Box>
<Flex>
<Text fz={"xs"} c={"red"}>*</Text>
<Text fz={"xs"} c={"dimmed"} fw={"bold"}>Upload foto untuk konten surat keterangan</Text>
</Flex>
<Group>
<Button
mt={10}
bg={colors['blue-button']}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
<ListSuratKeterangan />
</SimpleGrid>
</Stack>
</Box>
);
}
export default SuratKeterangan;

View File

@@ -1,17 +0,0 @@
import colors from '@/con/colors';
import { Box, Paper, Stack, Title } from '@mantine/core';
import React from 'react';
function ListTelunjukSaktiDesa() {
return (
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<Title order={3}>List Telunjuk Sakti Desa</Title>
</Stack>
</Paper>
</Box>
);
}
export default ListTelunjukSaktiDesa;

View File

@@ -1,36 +0,0 @@
import colors from '@/con/colors';
import { Box, SimpleGrid, Paper, Stack, Title, Group, Button, Text } from '@mantine/core';
import React from 'react';
import { DesaEditor } from '../../../_com/desaEditor';
import ListTelunjukSaktiDesa from './listPage';
function TelunjukSaktiDesa() {
return (
<Box py={10}>
<SimpleGrid cols={{ base: 1, md: 2 }}>
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<Title order={3}>Telunjuk Sakti Desa</Title>
<Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi Telunjuk Sakti Desa</Text>
<DesaEditor showSubmit={false} />
</Box>
<Group>
<Button
mt={10}
bg={colors['blue-button']}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
<ListTelunjukSaktiDesa />
</SimpleGrid>
</Box>
);
}
export default TelunjukSaktiDesa;

View File

@@ -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<string | null>(null)
const [file, setFile] = useState<File | null>(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 (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors["blue-button"]} size={30} />
</Button>
</Box>
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Edit Penghargaan</Title>
<TextInput
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
placeholder="masukkan judul"
/>
<TextInput
value={formData.juara}
onChange={(e) => setFormData({ ...formData, juara: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Juara</Text>}
placeholder="masukkan juara"
/>
<FileInput
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar Baru (Opsional)</Text>}
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 ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg={"gray"}>
<IconImageInPicture />
</Center>
)}
<Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }));
statePenghargaan.edit.form.deskripsi = htmlContent;
}}
/>
</Box>
<Button onClick={handleSubmit}>Edit Penghargaan</Button>
</Stack>
</Paper>
</Box>
);
}
export default EditPenghargaan;

View File

@@ -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<string | null>(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 (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Penghargaan</Text>
{statePenghargaan.findUnique.data ? (
<Paper key={statePenghargaan.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Judul</Text>
<Text fz={"lg"}>{statePenghargaan.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Juara</Text>
<Text fz={"lg"}>{statePenghargaan.findUnique.data?.juara}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: statePenghargaan.findUnique.data?.deskripsi }} />
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
<Image w={{ base: 150, md: 150, lg: 150 }} src={statePenghargaan.findUnique.data?.image?.link} alt="gambar" />
</Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (statePenghargaan.findUnique.data) {
setSelectedId(statePenghargaan.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={statePenghargaan.delete.loading || !statePenghargaan.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (statePenghargaan.findUnique.data) {
router.push(`/admin/desa/penghargaan/${statePenghargaan.findUnique.data.id}/edit`);
}
}}
disabled={!statePenghargaan.findUnique.data}
color={"green"}
>
<IconEdit size={20} />
</Button>
</Flex>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus penghargaan ini?'
/>
</Box>
);
}
export default DetailPenghargaan;

View File

@@ -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<string | null>(null);
const [file, setFile] = useState<File | null>(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 (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Create Penghargaan</Title>
<TextInput
value={statePenghargaan.create.form.name}
onChange={(val) => {
statePenghargaan.create.form.name = val.target.value;
}}
label={<Text fz={"sm"} fw={"bold"}>Nama Penghargaan</Text>}
placeholder="masukkan nama penghargaan"
/>
<TextInput
value={statePenghargaan.create.form.juara}
onChange={(val) => {
statePenghargaan.create.form.juara = val.target.value;
}}
label={<Text fz={"sm"} fw={"bold"}>Juara</Text>}
placeholder="masukkan juara"
/>
<Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
<CreateEditor
value={statePenghargaan.create.form.deskripsi}
onChange={(htmlContent) => {
statePenghargaan.create.form.deskripsi = htmlContent;
}}
/>
</Box>
<FileInput
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar Konten</Text>}
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 ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg={"gray"}>
<IconImageInPicture />
</Center>
)}
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
</Stack>
</Paper>
</Box>
);
}
export default CreatePenghargaan;

View File

@@ -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(
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Stack>
<Title order={3}>Penghargaan</Title>
<Tabs color={colors['blue-button']} variant='pills' defaultValue={"Penghargaan"}>
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
<TabsTab value="Penghargaan">
Penghargaan
</TabsTab>
<TabsTab value="Gambar Penghargaan">
Gambar Penghargaan
</TabsTab>
</TabsList>
<TabsPanel value="Penghargaan">
<Penghargaan/>
</TabsPanel>
<TabsPanel value="Gambar Penghargaan">
<GambarPerhargaan/>
</TabsPanel>
</Tabs>
</Stack>
<Paper bg={colors['white-1']} p={'md'}>
<JudulListTab
title='List Penghargaan'
href='/admin/desa/penghargaan/create'
placeholder='pencarian'
searchIcon={<IconSearch size={16} />}
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Image</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{state.findMany.data?.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd>
<TableTd>
<Image w={100} src={item.image?.link} alt="gambar" />
</TableTd>
<TableTd>
<Text>
<Button onClick={() => router.push(`/admin/desa/penghargaan/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</Text>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>
</Box>
);
}
export default Page;
export default Penghargaan;

View File

@@ -1,17 +0,0 @@
import colors from '@/con/colors';
import { Box, Paper, Stack, Title } from '@mantine/core';
import React from 'react';
function ListGambarPenghargaan() {
return (
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<Title order={3}>List Gambar Penghargaan</Title>
</Stack>
</Paper>
</Box>
);
}
export default ListGambarPenghargaan;

View File

@@ -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 (
<Box py={10}>
<Stack gap={"xs"}>
<SimpleGrid cols={{ base: 1, md: 2 }}>
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={3}>Tambah Gambar Penghargaan</Title>
<TextInput
label="Judul Gambar Penghargaan"
placeholder='masukkan judul gambar penghargaan'
/>
<Text fw={"bold"}>Deskripsi Gambar Penghargaan</Text>
<DesaEditor showSubmit={false} />
<Box>
<Text fw={"bold"}>Upload Gambar Penghargaan</Text>
<Box bg={colors['BG-trans']} p={"md"}>
<Center>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Center>
</Box>
</Box>
<Group>
<Button
mt={10}
bg={colors['blue-button']}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
<ListGambarPenghargaan />
</SimpleGrid>
</Stack>
</Box>
);
}
export default GambarPerhargaan;

View File

@@ -1,17 +0,0 @@
import colors from '@/con/colors';
import { Box, Paper, Stack, Title } from '@mantine/core';
import React from 'react';
function ListPenghargaan() {
return (
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<Title order={3}>List Penghargaan</Title>
</Stack>
</Paper>
</Box>
);
}
export default ListPenghargaan;

View File

@@ -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 (
<Box py={10}>
<Stack>
<SimpleGrid cols={{ base: 1, md: 2 }}>
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={3}>Penghargaan</Title>
<Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi Penghargaan</Text>
<DesaEditor
showSubmit={false}
/>
</Box>
<Group>
<Button
mt={10}
bg={colors['blue-button']}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
<ListPenghargaan/>
</SimpleGrid>
</Stack>
</Box>
);
}
export default Penghargaan;

View File

@@ -0,0 +1,63 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
function LayoutTabsGallery({ children }: { children: React.ReactNode }) {
const router = useRouter()
const pathname = usePathname()
const tabs = [
{
label: "Foto",
value: "foto",
href: "/admin/desa/gallery/foto"
},
{
label: "Video",
value: "video",
href: "/admin/desa/gallery/video"
},
];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.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 (
<Stack>
<Title order={3}>Gallery</Title>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
{tabs.map((e, i) => (
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
))}
</TabsList>
{tabs.map((e, i) => (
<TabsPanel key={i} value={e.value}>
{/* Konten dummy, bisa diganti tergantung routing */}
<></>
</TabsPanel>
))}
</Tabs>
{children}
</Stack>
);
}
export default LayoutTabsGallery;

View File

@@ -55,7 +55,7 @@ function GrafikBerdasarkanJenisKelaminRespondenCreate() {
}}
/>
<TextInput
label="Baik"
label="Perempuan"
type="number"
placeholder="masukkan jumlah"
value={stategrafikBerdasarkanJenisKelamin.create.form.perempuan}

View File

@@ -7,14 +7,14 @@ import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { Cell, Pie, PieChart } from 'recharts';
import { useProxy } from 'valtio/utils';
import { useSnapshot } from 'valtio';
import JudulListTab from '../../../_com/jusulListTab';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import grafikBerdasarkanResponden from '../../../_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanResponden';
function GrafikBerdasarkanResponden() {
const stategrafikBerdasarkanResponden = useProxy(grafikBerdasarkanResponden)
const stategrafikBerdasarkanResponden = useSnapshot(grafikBerdasarkanResponden)
const [donutData, setDonutData] = useState<any[]>([]);
const [mounted, setMounted] = useState(false);
const [modalHapus, setModalHapus] = useState(false)
@@ -39,7 +39,7 @@ function GrafikBerdasarkanResponden() {
}, [])
useEffect(() => {
if (stategrafikBerdasarkanResponden.findMany.data && stategrafikBerdasarkanResponden.findMany.data.length > 0) {
if (stategrafikBerdasarkanResponden.findMany.data) {
const totalSangatBaik = stategrafikBerdasarkanResponden.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.sangatbaik || 0), 0);
const totalBaik = stategrafikBerdasarkanResponden.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.baik || 0), 0);
const totalKurangBaik = stategrafikBerdasarkanResponden.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.kurangbaik || 0), 0);

View File

@@ -1,7 +1,7 @@
'use client'
import JudulListTab from '@/app/admin/(dashboard)/_com/jusulListTab';
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Title } from '@mantine/core';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { useMediaQuery, useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
@@ -30,30 +30,34 @@ function GrafikHasilKepuasanMasyarakat() {
const router = useRouter();
const handleDelete = () => {
if (selectedId) {
stateGrafikHasilKepuasan.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
stateGrafikHasilKepuasan.findMany.load()
}
}
useShallowEffect(() => {
setMounted(true)
stateGrafikHasilKepuasan.findMany.load()
}, [])
useEffect(() => {
if (stateGrafikHasilKepuasan.findMany.data && stateGrafikHasilKepuasan.findMany.data.length > 0) {
setChartData([...stateGrafikHasilKepuasan.findMany.data.map((item) => ({
id: item.id,
label: item.label,
kepuasan: Number(item.kepuasan),
}))]);
if (stateGrafikHasilKepuasan.findMany.data) {
setChartData(
stateGrafikHasilKepuasan.findMany.data.map((item) => ({
id: item.id,
label: item.label,
kepuasan: Number(item.kepuasan),
}))
);
}
}, [stateGrafikHasilKepuasan.findMany.data])
}, [stateGrafikHasilKepuasan.findMany.data]);
if (!stateGrafikHasilKepuasan.findMany.data) {
@@ -114,7 +118,7 @@ function GrafikHasilKepuasanMasyarakat() {
<Paper style={{ width: '100%', minWidth: 300, height: 500, minHeight: 300 }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title pb={10} order={3}>Data Kepuasan Masyarakat</Title>
{mounted && chartData.length > 0 && (
{mounted && chartData.length > 0 ? (
<BarChart width={isMobile ? 300 : isTablet ? 300 : 300} height={380} data={chartData} >
<XAxis dataKey="label" />
<YAxis />
@@ -122,6 +126,8 @@ function GrafikHasilKepuasanMasyarakat() {
<Legend />
<Bar dataKey="kepuasan" fill={colors['blue-button']} name="Kepuasan" />
</BarChart>
) : (
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text>
)}
</Stack>
</Paper>

View File

@@ -128,12 +128,12 @@ export const navBar = [
{
id: "Desa_5",
name: "Gallery",
path: "/admin/desa/gallery"
path: "/admin/desa/gallery/foto"
},
{
id: "Desa_6",
name: "Layanan",
path: "/admin/desa/layanan"
path: "/admin/desa/layanan/pelayanan_surat_keterangan"
},
{
id: "Desa_7",

View File

@@ -0,0 +1,32 @@
import prisma from "@/lib/prisma";
import { Prisma } from "@prisma/client";
import { Context } from "elysia";
type FormCreate = Prisma.GalleryFotoGetPayload<{
select: {
name: true;
imagesId: true;
deskripsi: true;
};
}>;
async function galleryFotoCreate(context: Context) {
const body = context.body as FormCreate;
await prisma.galleryFoto.create({
data: {
name: body.name,
deskripsi: body.deskripsi,
imagesId: body.imagesId,
},
});
return {
success: true,
message: "Success create gallery foto",
data: {
...body,
},
};
}
export default galleryFotoCreate

View File

@@ -0,0 +1,53 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
import fs from "fs/promises";
import path from "path";
const galleryFotoDelete = async (context: Context) => {
const id = context.params?.id as string;
if (!id) {
return {
status: 400,
body: "ID tidak diberikan",
};
}
const foto = await prisma.galleryFoto.findUnique({
where: { id },
include: {
imageGalleryFoto: true,
},
});
if (!foto) {
return {
status: 404,
body: "Foto tidak ditemukan",
};
}
// Hapus file gambar dari filesystem jika ada
if (foto.imageGalleryFoto) {
try {
const filePath = path.join(foto.imageGalleryFoto.path, foto.imageGalleryFoto.name);
await fs.unlink(filePath);
await prisma.fileStorage.delete({
where: { id: foto.imageGalleryFoto.id },
});
} catch (err) {
console.error("Gagal hapus gambar lama:", err);
}
}
await prisma.galleryFoto.delete({
where: { id },
});
return {
status: 200,
body: "Foto berhasil dihapus",
};
}
export default galleryFotoDelete

View File

@@ -0,0 +1,25 @@
import prisma from "@/lib/prisma";
async function galleryFotoFindMany() {
try {
const data = await prisma.galleryFoto.findMany({
where: { isActive: true },
include: {
imageGalleryFoto: true,
},
});
return {
success: true,
message: "Success fetch gallery foto",
data,
};
} catch (e) {
console.error("Find many error:", e);
return {
success: false,
message: "Failed fetch gallery foto",
};
}
}
export default galleryFotoFindMany

View File

@@ -0,0 +1,49 @@
import prisma from "@/lib/prisma";
export default async function galleryFotoFindUnique(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.galleryFoto.findUnique({
where: { id },
include: {
imageGalleryFoto: true,
},
});
if (!data) {
return Response.json({
success: false,
message: "Gallery foto tidak ditemukan",
}, { status: 404 });
}
return Response.json({
success: true,
message: "Success fetch gallery foto by ID",
data,
}, { status: 200 });
} catch (e) {
console.error("Find by ID error:", e);
return Response.json({
success: false,
message: "Gagal mengambil gallery foto: " + (e instanceof Error ? e.message : 'Unknown error'),
}, { status: 500 });
}
}

View File

@@ -0,0 +1,34 @@
import Elysia, { t } from "elysia";
import galleryFotoCreate from "./create";
import galleryFotoDelete from "./del";
import galleryFotoFindMany from "./find-many";
import galleryFotoUpdate from "./updt";
import galleryFotoFindUnique from "./findUnique";
const GalleryFoto = new Elysia({ prefix: "/gallery/foto", tags: ["Desa/Gallery/Foto"] })
.get("/find-many", galleryFotoFindMany)
.get("/:id", async (context) => {
const response = await galleryFotoFindUnique(new Request(context.request));
return response;
})
.post("/create", galleryFotoCreate, {
body: t.Object({
name: t.String(),
deskripsi: t.String(),
imagesId: t.String(),
}),
})
.delete("/del/:id", galleryFotoDelete)
.put("/:id", async (context) => {
const response = await galleryFotoUpdate(context);
return response;
},
{
body: t.Object({
name: t.String(),
deskripsi: t.String(),
imagesId: t.String(),
}),
})
export default GalleryFoto

View File

@@ -0,0 +1,90 @@
import prisma from "@/lib/prisma";
import { Prisma } from "@prisma/client";
import { Context } from "elysia";
import fs from "fs/promises";
import path from "path";
type FormUpdate = Prisma.GalleryFotoGetPayload<{
select: {
id: true;
name: true;
deskripsi: true;
imagesId: true;
};
}>;
async function galleryFotoUpdate(context: Context) {
try {
const id = context.params?.id;
const body = (await context.body) as Omit<FormUpdate, "id">;
const { name, deskripsi, imagesId } = body;
if (!id) {
return new Response(
JSON.stringify({ success: false, message: "ID tidak diberikan" }),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
}
const existing = await prisma.galleryFoto.findUnique({
where: { id },
include: {
imageGalleryFoto: true,
},
});
if (!existing) {
return new Response(
JSON.stringify({
success: false,
message: "Gallery foto tidak ditemukan",
}),
{ status: 404, headers: { "Content-Type": "application/json" } }
);
}
if (existing.imagesId && existing.imagesId !== imagesId) {
const oldImage = existing.imageGalleryFoto;
if (oldImage) {
try {
const filePath = path.join(oldImage.path, oldImage.name);
await fs.unlink(filePath);
await prisma.fileStorage.delete({
where: { id: oldImage.id },
});
} catch (err) {
console.error("Gagal hapus gambar lama:", err);
}
}
}
const updated = await prisma.galleryFoto.update({
where: { id },
data: {
name,
deskripsi,
imagesId,
},
});
return new Response(
JSON.stringify({
success: true,
message: "Success update gallery foto",
data: updated,
}),
{ status: 200, headers: { "Content-Type": "application/json" } }
);
} catch (error) {
console.error("Error updating gallery foto:", error);
return new Response(
JSON.stringify({
success: false,
message: "Terjadi kesalahan saat mengupdate gallery foto",
}),
{ status: 500, headers: { "Content-Type": "application/json" } }
);
}
}
export default galleryFotoUpdate;

View File

@@ -0,0 +1,30 @@
import prisma from "@/lib/prisma";
import { Prisma } from "@prisma/client";
import { Context } from "elysia";
type FormCreate = Prisma.GalleryVideoGetPayload<{
select: {
name: true;
deskripsi: true;
linkVideo: true;
}
}>
async function galleryVideoCreate(context: Context) {
const body = context.body as FormCreate;
await prisma.galleryVideo.create({
data: {
name: body.name,
deskripsi: body.deskripsi,
linkVideo: body.linkVideo,
},
});
return {
success: true,
message: "Success create gallery video",
data: {
...body,
},
};
}
export default galleryVideoCreate;

View File

@@ -0,0 +1,34 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
const galleryVideoDelete = async (context: Context) => {
const id = context.params?.id as string;
if (!id) {
return {
status: 400,
body: "ID tidak diberikan",
};
}
const galleryVideo = await prisma.galleryVideo.findUnique({
where: { id },
});
if (!galleryVideo) {
return {
status: 404,
body: "Gallery video tidak ditemukan",
};
}
// Hapus gallery video dari database
await prisma.galleryVideo.delete({
where: { id },
});
return {
status: 200,
body: "Gallery video berhasil dihapus",
};
}
export default galleryVideoDelete;

View File

@@ -0,0 +1,22 @@
import prisma from "@/lib/prisma";
async function galleryVideoFindMany() {
try {
const data = await prisma.galleryVideo.findMany({
where: { isActive: true },
});
return {
success: true,
message: "Success fetch gallery video",
data,
};
} catch (e) {
console.error("Find many error:", e);
return {
success: false,
message: "Failed fetch gallery video",
};
}
}
export default galleryVideoFindMany;

View File

@@ -0,0 +1,46 @@
import prisma from "@/lib/prisma";
export default async function galleryVideoFindUnique(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.galleryVideo.findUnique({
where: { id },
});
if (!data) {
return Response.json({
success: false,
message: "Gallery video tidak ditemukan",
}, { status: 404 });
}
return Response.json({
success: true,
message: "Success fetch gallery video by ID",
data,
}, { status: 200 });
} catch (error) {
console.error("Find by ID error:", error);
return Response.json({
success: false,
message: "Gagal mengambil gallery video: " + (error instanceof Error ? error.message : 'Unknown error'),
}, { status: 500 });
}
}

View File

@@ -0,0 +1,39 @@
import Elysia, { t } from "elysia";
import galleryVideoFindMany from "./find-many";
import galleryVideoFindUnique from "./findUnique";
import galleryVideoCreate from "./create";
import galleryVideoDelete from "./del";
import galleryVideoUpdate from "./updt";
const GalleryVideo = new Elysia({
prefix: "/gallery/video",
tags: ["Desa/Gallery/Video"],
})
.get("/find-many", galleryVideoFindMany)
.get("/:id", async (context) => {
const response = await galleryVideoFindUnique(new Request(context.request));
return response;
})
.post("/create", galleryVideoCreate, {
body: t.Object({
name: t.String(),
deskripsi: t.String(),
linkVideo: t.String(),
}),
})
.delete("/del/:id", galleryVideoDelete)
.put(
"/:id",
async (context) => {
const response = await galleryVideoUpdate(context);
return response;
},
{
body: t.Object({
name: t.String(),
deskripsi: t.String(),
linkVideo: t.String(),
}),
}
);
export default GalleryVideo;

View File

@@ -0,0 +1,85 @@
import prisma from "@/lib/prisma";
import { Prisma } from "@prisma/client";
import { Context } from "elysia";
type FormUpdate = Prisma.GalleryVideoGetPayload<{
select: {
id: true;
name: true;
deskripsi: true;
linkVideo: true;
}
}>;
async function galleryVideoUpdate(context: Context) {
try {
const id = context.params?.id as string;
const body = (await context.body) as Omit<FormUpdate, "id">;
const {
name,
deskripsi,
linkVideo,
} = 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.galleryVideo.findUnique({
where: {id}
})
if (!existing) {
return new Response(JSON.stringify({
success: false,
message: "Gallery video tidak ditemukan",
}), {
status: 404,
headers: {
"Content-Type": "application/json",
},
});
}
const updated = await prisma.galleryVideo.update({
where: {id},
data: {
name,
deskripsi,
linkVideo,
}
})
return new Response(JSON.stringify({
success: true,
message: "Gallery video berhasil diupdate",
data: updated,
}), {
status: 200,
headers: {
"Content-Type": "application/json",
},
});
} catch (error) {
console.error("Update error:", error);
return new Response(JSON.stringify({
success: false,
message: "Gagal mengupdate gallery video: " + (error instanceof Error ? error.message : 'Unknown error'),
}), {
status: 500,
headers: {
"Content-Type": "application/json",
},
});
}
}
export default galleryVideoUpdate;

View File

@@ -3,11 +3,20 @@ import Berita from "./berita";
import Pengumuman from "./pengumuman";
import ProfileDesa from "./profile/profile_desa";
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"] })
.use(Berita)
.use(Pengumuman)
.use(ProfileDesa)
.use(PotensiDesa)
.use(GalleryFoto)
.use(GalleryVideo)
.use(LayananDesa)
.use(Penghargaan)
export default Desa;

View File

@@ -0,0 +1,13 @@
import Elysia from "elysia";
import PelayananSuratKeterangan from "./pelayanan_surat_keterangan";
import PelayananTelunjukSaktiDesa from "./pelayanan_telunjuk_sakti_desa";
import PelayananPerizinanBerusaha from "./pelayanan_perizinan_berusaha";
import PelayananPendudukNonPermanen from "./pelayanan_penduduk_non_permanen";
const LayananDesa = new Elysia({ prefix: "/layanan", tags: ["Desa/Layanan"] })
.use(PelayananSuratKeterangan)
.use(PelayananTelunjukSaktiDesa)
.use(PelayananPerizinanBerusaha)
.use(PelayananPendudukNonPermanen)
export default LayananDesa

View File

@@ -0,0 +1,60 @@
import prisma from "@/lib/prisma";
export default async function pelayananPendudukNonPermanenFindUnique(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.pelayananPendudukNonPermanen.findUnique({
where: { id },
});
if (!data) {
return Response.json(
{
success: false,
message: "Pelayanan penduduk non permanen tidak ditemukan",
},
{ status: 404 }
);
}
return Response.json(
{
success: true,
message: "Success fetch pelayanan penduduk non permanen by ID",
data,
},
{ status: 200 }
);
} catch (e) {
console.error("Find by ID error:", e);
return Response.json(
{
success: false,
message: "Gagal mengambil pelayanan penduduk non permanen: " + (e instanceof Error ? e.message : 'Unknown error'),
},
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,20 @@
import Elysia, { t } from "elysia";
import pelayananPendudukNonPermanenFindUnique from "./findUnique";
import pelayananPendudukNonPermanenUpdate from "./updt";
const PelayananPendudukNonPermanen = new Elysia({ prefix: "/pelayananpenduduknonpermanen", tags: ["Desa/Layanan/Pelayanan Penduduk Non Permanen"] })
.get("/:id", async (context) => {
const response = await pelayananPendudukNonPermanenFindUnique(new Request(context.request));
return response;
})
.put("/:id", async (context) => {
const response = await pelayananPendudukNonPermanenUpdate(context);
return response;
},
{
body: t.Object({
name: t.String(),
deskripsi: t.String(),
}),
})
export default PelayananPendudukNonPermanen;

View File

@@ -0,0 +1,72 @@
import prisma from "@/lib/prisma";
import { Prisma } from "@prisma/client";
import { Context } from "elysia";
type FormUpdate = Prisma.PelayananPendudukNonPermanenGetPayload<{
select: {
name: true;
deskripsi: true;
};
}>;
export default async function pelayananPendudukNonPermanenUpdate(context: Context) {
try {
const id = context.params?.id;
const body = (await context.body) as Omit<FormUpdate, "id">;
const { name, deskripsi } = body;
if (!id) {
return new Response(JSON.stringify({
success: false,
message: "ID tidak diberikan",
}), {
status: 400,
headers: {
"Content-Type": "application/json",
},
});
}
const existing = await prisma.pelayananPendudukNonPermanen.findUnique({
where: { id },
});
if (!existing) {
return new Response(JSON.stringify({
success: false,
message: "Pelayanan penduduk non permanen tidak ditemukan",
}), {
status: 404,
headers: {
"Content-Type": "application/json",
},
});
}
const updated = await prisma.pelayananPendudukNonPermanen.update({
where: { id },
data: { name, deskripsi },
});
return new Response(JSON.stringify({
success: true,
message: "Success update pelayanan penduduk non permanen",
data: updated,
}), {
status: 200,
headers: {
"Content-Type": "application/json",
},
});
} catch (e) {
console.error("Update error:", e);
return new Response(JSON.stringify({
success: false,
message: "Gagal mengupdate pelayanan penduduk non permanen: " + (e instanceof Error ? e.message : 'Unknown error'),
}), {
status: 500,
headers: {
"Content-Type": "application/json",
},
});
}
}

View File

@@ -0,0 +1,64 @@
import prisma from "@/lib/prisma";
export default async function pelayananPerizinanBerusahaFindUnique(
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.pelayananPerizinanBerusaha.findUnique({
where: { id },
});
if (!data) {
return Response.json(
{
success: false,
message: "Pelayanan perizinan berusaha tidak ditemukan",
},
{ status: 404 }
);
}
return Response.json(
{
success: true,
message: "Success fetch pelayanan perizinan berusaha by ID",
data,
},
{ status: 200 }
);
} catch (error) {
console.error("Find by ID error:", error);
return Response.json(
{
success: false,
message:
"Gagal mengambil pelayanan perizinan berusaha: " +
(error instanceof Error ? error.message : "Unknown error"),
},
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,21 @@
import Elysia, { t } from "elysia";
import pelayananPerizinanBerusahaFindUnique from "./findUnique";
import pelayananPerizinanBerusahaUpdate from "./updt";
const PelayananPerizinanBerusaha = new Elysia({ prefix: "/pelayananperizinanberusaha", tags: ["Desa/Layanan/Pelayanan Perizinan Berusaha"] })
.get("/:id", async (context) => {
const response = await pelayananPerizinanBerusahaFindUnique(new Request(context.request));
return response;
})
.put("/:id", async (context) => {
const response = await pelayananPerizinanBerusahaUpdate(context);
return response;
},
{
body: t.Object({
name: t.String(),
deskripsi: t.String(),
link: t.String(),
}),
})
export default PelayananPerizinanBerusaha;

View File

@@ -0,0 +1,77 @@
import prisma from "@/lib/prisma";
import { Prisma } from "@prisma/client";
import { Context } from "elysia";
type FormUpdate = Prisma.PelayananPerizinanBerusahaGetPayload<{
select: {
name: true;
deskripsi: true;
link: true;
};
}>;
export default async function pelayananPerizinanBerusahaUpdate(context: Context) {
try {
const id = context.params?.id;
const body = (await context.body) as Omit<FormUpdate, "id">;
const { name, deskripsi, link } = body;
if (!id) {
return new Response(JSON.stringify({
success: false,
message: "ID tidak diberikan",
}), {
status: 400,
headers: {
"Content-Type": "application/json",
},
});
}
const existing = await prisma.pelayananPerizinanBerusaha.findUnique({
where: { id },
});
if (!existing) {
return new Response(JSON.stringify({
success: false,
message: "Pelayanan perizinan berusaha tidak ditemukan",
}), {
status: 404,
headers: {
"Content-Type": "application/json",
},
});
}
const updated = await prisma.pelayananPerizinanBerusaha.update({
where: { id },
data: {
name,
deskripsi,
link,
},
});
return new Response(JSON.stringify({
success: true,
message: "Success update pelayanan perizinan berusaha",
data: updated,
}), {
status: 200,
headers: {
"Content-Type": "application/json",
},
});
} catch (error) {
console.error("Error updating pelayanan perizinan berusaha:", error);
return new Response(JSON.stringify({
success: false,
message: "Terjadi kesalahan saat mengupdate pelayanan perizinan berusaha",
}), {
status: 500,
headers: {
"Content-Type": "application/json",
},
});
}
}

View File

@@ -0,0 +1,30 @@
import prisma from "@/lib/prisma";
import { Prisma } from "@prisma/client";
import { Context } from "elysia";
type FormCreate = Prisma.PelayananSuratKeteranganGetPayload<{
select: {
name: true;
deskripsi: true;
imageId: true;
};
}>;
async function createPelayananSuratKeterangan(context: Context) {
const body = context.body as FormCreate;
await prisma.pelayananSuratKeterangan.create({
data: {
name: body.name,
deskripsi: body.deskripsi,
imageId: body.imageId,
},
});
return {
success: true,
message: "Success create pelayanan surat keterangan",
data: {
...body,
},
};
}
export default createPelayananSuratKeterangan

View File

@@ -0,0 +1,52 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
import path from "path";
import fs from "fs/promises";
const pelayananSuratKeteranganDelete = async (context: Context) => {
const id = context.params?.id as string;
if (!id) {
return {
status: 400,
body: "ID tidak diberikan",
};
}
const pelayananSuratKeterangan = await prisma.pelayananSuratKeterangan.findUnique({
where: { id },
include: {
image: true,
},
});
if (!pelayananSuratKeterangan) {
return {
status: 404,
body: "Pelayanan surat keterangan tidak ditemukan",
};
}
// Hapus file gambar dari filesystem jika ada
if (pelayananSuratKeterangan.image) {
try {
const filePath = path.join(pelayananSuratKeterangan.image.path, pelayananSuratKeterangan.image.name);
await fs.unlink(filePath);
await prisma.fileStorage.delete({
where: { id: pelayananSuratKeterangan.image.id },
});
} catch (err) {
console.error("Gagal hapus gambar lama:", err);
}
}
const deleted = await prisma.pelayananSuratKeterangan.delete({
where: { id },
});
return {
status: 200,
body: deleted,
};
};
export default pelayananSuratKeteranganDelete;

View File

@@ -0,0 +1,24 @@
import prisma from "@/lib/prisma";
export default async function pelayananSuratKeteranganFindMany() {
try {
const data = await prisma.pelayananSuratKeterangan.findMany({
where: { isActive: true },
include: {
image: true,
},
});
return {
success: true,
message: "Success fetch pelayanan surat keterangan",
data,
};
} catch (e) {
console.error("Find many error:", e);
return {
success: false,
message: "Failed fetch pelayanan surat keterangan",
};
}
}

View File

@@ -0,0 +1,49 @@
import prisma from "@/lib/prisma";
export default async function pelayananSuratKeteranganFindUnique(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.pelayananSuratKeterangan.findUnique({
where: { id },
include: {
image: true,
},
});
if (!data) {
return Response.json({
success: false,
message: "Pelayanan surat keterangan tidak ditemukan",
}, { status: 404 });
}
return Response.json({
success: true,
message: "Success fetch pelayanan surat keterangan by ID",
data,
}, { status: 200 });
} catch (error) {
console.error("Find by ID error:", error);
return Response.json({
success: false,
message: "Gagal mengambil pelayanan surat keterangan: " + (error instanceof Error ? error.message : 'Unknown error'),
}, { status: 500 });
}
}

View File

@@ -0,0 +1,35 @@
import Elysia from "elysia";
import pelayananSuratKeteranganFindMany from "./find-many";
import pelayananSuratKeteranganFindUnique from "./findUnique";
import pelayananSuratKeteranganCreate from "./create";
import pelayananSuratKeteranganUpdate from "./updt";
import pelayananSuratKeteranganDelete from "./del";
import { t } from "elysia";
const PelayananSuratKeterangan = new Elysia({ prefix: "/pelayanansuratketerangan", tags: ["Desa/Layanan/Pelayanan Surat Keterangan"] })
.get("/find-many", pelayananSuratKeteranganFindMany)
.get("/:id", async (context) => {
const response = await pelayananSuratKeteranganFindUnique(new Request(context.request));
return response;
})
.post("/create", pelayananSuratKeteranganCreate, {
body: t.Object({
name: t.String(),
deskripsi: t.String(),
imageId: t.String(),
}),
})
.delete("/del/:id", pelayananSuratKeteranganDelete)
.put("/:id", async (context) => {
const response = await pelayananSuratKeteranganUpdate(context);
return response;
},
{
body: t.Object({
name: t.String(),
deskripsi: t.String(),
imageId: t.String(),
}),
})
export default PelayananSuratKeterangan;

View File

@@ -0,0 +1,97 @@
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.PelayananSuratKeteranganGetPayload<{
select: {
name: true;
deskripsi: true;
imageId: true;
};
}>;
export default async function updatePelayananSuratKeterangan(context: Context) {
try {
const id = context.params?.id;
const body = (await context.body) as Omit<FormUpdate, "id">;
const { name, deskripsi, imageId } = body;
if (!id) {
return new Response(JSON.stringify({
success: false,
message: "ID tidak diberikan",
}), {
status: 400,
headers: {
"Content-Type": "application/json",
},
});
}
const existing = await prisma.pelayananSuratKeterangan.findUnique({
where: { id },
include: {
image: true,
}
});
if (!existing) {
return new Response(JSON.stringify({
success: false,
message: "Pelayanan surat keterangan 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 (err) {
console.error("Gagal hapus gambar lama:", err);
}
}
}
const updated = await prisma.pelayananSuratKeterangan.update({
where: { id },
data: {
name,
deskripsi,
imageId,
},
})
return new Response(JSON.stringify({
success: true,
message: "Success update pelayanan surat keterangan",
data: updated,
}), {
status: 200,
headers: {
"Content-Type": "application/json",
},
});
} catch (error) {
console.error("Error updating pelayanan surat keterangan:", error);
return new Response(JSON.stringify({
success: false,
message: "Terjadi kesalahan saat mengupdate pelayanan surat keterangan",
}), {
status: 500,
headers: {
"Content-Type": "application/json",
},
});
}
}

View File

@@ -0,0 +1,29 @@
import prisma from "@/lib/prisma";
import { Prisma } from "@prisma/client";
import { Context } from "elysia";
type FormCreate = Prisma.PelayananTelunjukSaktiDesaGetPayload<{
select: {
name: true;
deskripsi: true;
link: true;
};
}>;
export default async function pelayananTelunjukSaktiDesaCreate(context: Context) {
const body = context.body as FormCreate;
await prisma.pelayananTelunjukSaktiDesa.create({
data: {
name: body.name,
deskripsi: body.deskripsi,
link: body.link,
},
});
return {
success: true,
message: "Success create pelayanan telunjuk sakti desa",
data: {
...body,
},
};
}

View File

@@ -0,0 +1,33 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function pelayananTelunjukSaktiDesaDelete(context: Context) {
const id = context.params?.id as string;
if (!id) {
return {
status: 400,
body: "ID tidak diberikan",
};
}
const pelayananTelunjukSaktiDesa = await prisma.pelayananTelunjukSaktiDesa.findUnique({
where: { id },
});
if (!pelayananTelunjukSaktiDesa) {
return {
status: 404,
body: "Pelayanan telunjuk sakti desa tidak ditemukan",
};
}
await prisma.pelayananTelunjukSaktiDesa.delete({
where: { id },
});
return {
status: 200,
body: "Pelayanan telunjuk sakti desa berhasil dihapus",
};
}

View File

@@ -0,0 +1,21 @@
import prisma from "@/lib/prisma";
export default async function pelayananTelunjukSaktiDesaFindMany() {
try {
const data = await prisma.pelayananTelunjukSaktiDesa.findMany({
where: { isActive: true },
});
return {
success: true,
message: "Success fetch pelayanan telunjuk sakti desa",
data,
};
} catch (e) {
console.error("Find many error:", e);
return {
success: false,
message: "Failed fetch pelayanan telunjuk sakti desa",
};
}
}

View File

@@ -0,0 +1,46 @@
import prisma from "@/lib/prisma";
export default async function pelayananTelunjukSaktiDesaFindUnique(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.pelayananTelunjukSaktiDesa.findUnique({
where: { id },
});
if (!data) {
return Response.json({
success: false,
message: "Pelayanan telunjuk sakti desa tidak ditemukan",
}, { status: 404 });
}
return Response.json({
success: true,
message: "Success fetch pelayanan telunjuk sakti desa by ID",
data,
}, { status: 200 });
} catch (error) {
console.error("Find by ID error:", error);
return Response.json({
success: false,
message: "Gagal mengambil pelayanan telunjuk sakti desa: " + (error instanceof Error ? error.message : 'Unknown error'),
}, { status: 500 });
}
}

View File

@@ -0,0 +1,33 @@
import Elysia, { t } from "elysia";
import pelayananTelunjukSaktiDesaFindUnique from "./findUnique";
import pelayananTelunjukSaktiDesaUpdate from "./updt";
import pelayananTelunjukSaktiDesaCreate from "./create";
import pelayananTelunjukSaktiDesaDelete from "./del";
import pelayananTelunjukSaktiDesaFindMany from "./find-many";
const pelayananTelunjukSaktiDesa = new Elysia({ prefix: "/pelayanantelunjuksaktidesa", tags: ["Desa/Layanan/Pelayanan Telunjuk Sakti Desa"] })
.get("/find-many", pelayananTelunjukSaktiDesaFindMany)
.get("/:id", async (context) => {
const response = await pelayananTelunjukSaktiDesaFindUnique(new Request(context.request));
return response;
})
.put("/:id", async (context) => {
const response = await pelayananTelunjukSaktiDesaUpdate(context);
return response;
},
{
body: t.Object({
name: t.String(),
deskripsi: t.String(),
link: t.String(),
}),
})
.post("/create", pelayananTelunjukSaktiDesaCreate, {
body: t.Object({
name: t.String(),
deskripsi: t.String(),
link: t.String(),
}),
})
.delete("/:id", pelayananTelunjukSaktiDesaDelete)
export default pelayananTelunjukSaktiDesa;

View File

@@ -0,0 +1,83 @@
import prisma from "@/lib/prisma";
import { Prisma } from "@prisma/client";
import { Context } from "elysia";
type FormUpdate = Prisma.PelayananTelunjukSaktiDesaGetPayload<{
select: {
name: true;
deskripsi: true;
link: true;
};
}>;
export default async function pelayananTelunjukSaktiDesaUpdate(context: Context) {
try {
const id = context.params?.id;
const body = (await context.body) as Omit<FormUpdate, "id">;
const { name, deskripsi, link } = body;
if (!id) {
return new Response(JSON.stringify({
success: false,
message: "ID tidak diberikan",
}), {
status: 400,
headers: {
"Content-Type": "application/json",
},
});
}
const data = await prisma.pelayananTelunjukSaktiDesa.update({
where: { id },
data: {
name,
deskripsi,
link,
},
});
if (!data) {
return new Response(JSON.stringify({
success: false,
message: "Gagal update pelayanan telunjuk sakti desa",
}), {
status: 404,
headers: {
"Content-Type": "application/json",
},
});
}
const updated = await prisma.pelayananTelunjukSaktiDesa.update({
where: { id },
data: {
name,
deskripsi,
link,
},
});
return new Response(JSON.stringify({
success: true,
message: "Success update pelayanan telunjuk sakti desa",
data: updated,
}), {
status: 200,
headers: {
"Content-Type": "application/json",
},
});
} catch (error) {
console.error(error);
return new Response(JSON.stringify({
success: false,
message: "Gagal update pelayanan telunjuk sakti desa",
}), {
status: 500,
headers: {
"Content-Type": "application/json",
},
});
}
}

View File

@@ -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,
}
}
}

View File

@@ -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

Some files were not shown because too many files have changed in this diff Show More