Compare commits

...

16 Commits

373 changed files with 26264 additions and 1101 deletions

1
auth Submodule

Submodule auth added at 51d749567a

BIN
bun.lockb

Binary file not shown.

View File

@@ -14,8 +14,10 @@
},
"dependencies": {
"@cubejs-client/core": "^0.31.0",
"@elysiajs/cookie": "^0.8.0",
"@elysiajs/cors": "^1.2.0",
"@elysiajs/eden": "^1.3.2",
"@elysiajs/jwt": "^1.3.2",
"@elysiajs/static": "^1.3.0",
"@elysiajs/stream": "^1.1.0",
"@elysiajs/swagger": "^1.2.0",
@@ -44,6 +46,7 @@
"@types/lodash": "^4.17.16",
"add": "^2.0.6",
"animate.css": "^4.1.1",
"bcryptjs": "^3.0.2",
"bun": "^1.2.2",
"chart.js": "^4.4.8",
"dayjs": "^1.11.13",
@@ -54,6 +57,7 @@
"framer-motion": "^12.23.5",
"get-port": "^7.1.0",
"jotai": "^2.12.3",
"jsonwebtoken": "^9.0.2",
"leaflet": "^1.9.4",
"list": "^2.0.19",
"lodash": "^4.17.21",
@@ -78,15 +82,16 @@
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "15.1.6",
"parcel": "^2.6.2",
"postcss": "^8.5.1",
"postcss-preset-mantine": "^1.17.0",
"postcss-simple-vars": "^7.0.1",
"typescript": "^5",
"parcel": "^2.6.2"
"typescript": "^5"
}
}

View File

@@ -0,0 +1,8 @@
[
{
"id": "edit",
"name": "I.B Surya Prabhawa Manuaba, S.H., M.H.",
"position": "Perbekel Darmasaba periode 2021-2027"
}
]

View File

@@ -0,0 +1,7 @@
[
{
"id": "edit",
"judul": "Fasilitas yang Disediakan",
"deskripsi": "<ul><li><p>Buku-buku pelajaran dan alat tulis</p></li><li><p>Ruang belajar nyaman dan kondusif</p></li><li><p>Modul latihan dan pendampingan tugas</p></li><li><p>Minuman ringan dan dukungan motivasi belajar</p></li></ul>"
}
]

View File

@@ -0,0 +1,7 @@
[
{
"id": "edit",
"judul": "Lokasi dan Jadwal",
"deskripsi": "<ul><li><p>Lokasi: Balai Banjar / Balai Desa Darmasaba / Perpustakaan Desa</p></li><li><p>Jadwal: Setiap hari Senin, Rabu, dan Jumat pukul 16.0018.00 WITA</p></li><li><p>Peserta: Terbuka untuk semua siswa SDSMP di wilayah desa</p></li></ul>"
}
]

View File

@@ -0,0 +1,7 @@
[
{
"id": "edit",
"judul": "Tujuan Program",
"deskripsi": "<ul><li><p>Memberikan pendampingan belajar secara gratis bagi siswa SD hingga SMP</p></li><li><p>Membantu siswa dalam menghadapi ujian dan menyelesaikan tugas sekolah</p></li><li><p>Menumbuhkan kepercayaan diri dan kemandirian dalam belajar</p></li><li><p>Meningkatkan kesetaraan pendidikan untuk seluruh anak desa</p></li></ul>"
}
]

View File

@@ -0,0 +1,7 @@
[
{
"id": "edit",
"judul": "Tempat Kegiatan",
"deskripsi": "<p>Program Pendidikan Non Formal yang diselenggarakan di Desa Darmasaba meliputi:</p><p>1) Keaksaraan Fungsional</p><ul><li><p>Untuk warga yang belum bisa membaca dan menulis</p></li></ul><p>2) Pendidikan Kesetaraan (Paket A, B, C)</p><ul><li><p>Setara SD, SMP, dan SMA bagi yang tidak menyelesaikan pendidikan formal</p></li></ul><p>3) Pelatihan Keterampilan</p><ul><li><p>Menjahit, memasak, sablon, pertanian, peternakan, hingga teknologi digital</p></li></ul><p>4) Kursus &amp; Pelatihan Soft Skill</p><ul><li><p>Public speaking, pengelolaan keuangan, kepemimpinan pemuda</p></li></ul><p>5) Pendidikan Keluarga &amp; Parenting</p><ul><li><p>Untuk membekali orang tua dalam mendampingi tumbuh kembang anak</p></li></ul>"
}
]

View File

@@ -0,0 +1,7 @@
[
{
"id": "edit",
"judul": "Tempat Kegiatan",
"deskripsi": "<ul><li><p>Balai Desa Darmasaba</p></li><li><p>TPK, Perpustakaan Desa, atau Posyandu</p></li><li><p>Bisa juga dilakukan secara mobile atau door to door</p></li></ul>"
}
]

View File

@@ -0,0 +1,7 @@
[
{
"id": "edit",
"judul": "Tujuan Program",
"deskripsi": "<ul><li><p>Memberikan kesempatan belajar yang fleksibel bagi warga desa</p></li><li><p>Meningkatkan keterampilan hidup dan kemandirian ekonomi</p></li><li><p>Mendorong partisipasi masyarakat dalam pembangunan desa</p></li><li><p>Mengurangi angka putus sekolah dan meningkatkan kualitas SDM</p></li></ul>"
}
]

View File

@@ -0,0 +1,7 @@
[
{
"id": "edit",
"judul": "Program Unggulan",
"deskripsi": "<ul><li><p>Bimbingan Belajar Gratis: Untuk siswa kurang mampu</p></li><li><p>Gerakan Literasi Desa: Meningkatkan minat baca sejak dini</p></li><li><p>Pelatihan Digital untuk Anak dan Remaja</p></li><li><p>Beasiswa Anak Berprestasi &amp; Kurang Mampu</p></li></ul>"
}
]

View File

@@ -0,0 +1,7 @@
[
{
"id": "edit",
"judul": "Tujuan Program",
"deskripsi": "<ul><li><p>Meningkatkan akses pendidikan yang merata dan berkualitas</p></li><li><p>Menumbuhkan semangat belajar sejak dini</p></li><li><p>Membentuk karakter anak yang berakhlak dan berwawasan lingkungan</p></li><li><p>Mendukung tumbuh kembang anak melalui pendekatan pendidikan yang holistik</p></li></ul>"
}
]

View File

@@ -0,0 +1,144 @@
-- CreateTable
CREATE TABLE "ProgramPenghijauan" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"judul" TEXT NOT NULL,
"deskripsi" TEXT NOT NULL,
"icon" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"isActive" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "ProgramPenghijauan_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "DataLingkunganDesa" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"jumlah" TEXT NOT NULL,
"deskripsi" TEXT NOT NULL,
"icon" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"isActive" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "DataLingkunganDesa_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "KegiatanDesa" (
"id" TEXT NOT NULL,
"judul" TEXT NOT NULL,
"deskripsiSingkat" TEXT NOT NULL,
"deskripsiLengkap" TEXT NOT NULL,
"tanggal" TIMESTAMP(3) NOT NULL,
"lokasi" TEXT NOT NULL,
"partisipan" INTEGER NOT NULL,
"imageId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"kategoriKegiatanId" TEXT NOT NULL,
CONSTRAINT "KegiatanDesa_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "KategoriKegiatan" (
"id" TEXT NOT NULL,
"nama" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"isActive" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "KategoriKegiatan_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "TujuanEdukasiLingkungan" (
"id" TEXT NOT NULL,
"judul" TEXT NOT NULL,
"deskripsi" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"isActive" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "TujuanEdukasiLingkungan_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "MateriEdukasiLingkungan" (
"id" TEXT NOT NULL,
"judul" TEXT NOT NULL,
"deskripsi" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"isActive" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "MateriEdukasiLingkungan_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ContohEdukasiLingkungan" (
"id" TEXT NOT NULL,
"judul" TEXT NOT NULL,
"deskripsi" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"isActive" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "ContohEdukasiLingkungan_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "FilosofiTriHita" (
"id" TEXT NOT NULL,
"judul" TEXT NOT NULL,
"deskripsi" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"isActive" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "FilosofiTriHita_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "BentukKonservasiBerdasarkanAdat" (
"id" TEXT NOT NULL,
"judul" TEXT NOT NULL,
"deskripsi" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"isActive" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "BentukKonservasiBerdasarkanAdat_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "NilaiKonservasiAdat" (
"id" TEXT NOT NULL,
"judul" TEXT NOT NULL,
"deskripsi" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"isActive" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "NilaiKonservasiAdat_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "KegiatanDesa" ADD CONSTRAINT "KegiatanDesa_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "KegiatanDesa" ADD CONSTRAINT "KegiatanDesa_kategoriKegiatanId_fkey" FOREIGN KEY ("kategoriKegiatanId") REFERENCES "KategoriKegiatan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -0,0 +1,56 @@
-- CreateTable
CREATE TABLE "PejabatDesa" (
"id" TEXT NOT NULL,
"name" VARCHAR(255) NOT NULL,
"position" TEXT NOT NULL,
"imageId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"isActive" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "PejabatDesa_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ProgramInovasi" (
"id" TEXT NOT NULL,
"name" VARCHAR(255) NOT NULL,
"description" TEXT,
"imageId" TEXT,
"link" VARCHAR(255),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP,
"isActive" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "ProgramInovasi_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "MediaSosial" (
"id" TEXT NOT NULL,
"imageId" TEXT NOT NULL,
"iconUrl" VARCHAR(255),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3),
"isActive" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "MediaSosial_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "PejabatDesa_name_key" ON "PejabatDesa"("name");
-- CreateIndex
CREATE UNIQUE INDEX "ProgramInovasi_name_key" ON "ProgramInovasi"("name");
-- AddForeignKey
ALTER TABLE "PejabatDesa" ADD CONSTRAINT "PejabatDesa_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ProgramInovasi" ADD CONSTRAINT "ProgramInovasi_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "MediaSosial" ADD CONSTRAINT "MediaSosial_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -60,6 +60,7 @@ model FileStorage {
deletedAt DateTime?
isActive Boolean @default(true)
link String
category String // "image" / "document" / "other"
Berita Berita[]
PotensiDesa PotensiDesa[]
Posyandu Posyandu[]
@@ -86,6 +87,209 @@ model FileStorage {
KolaborasiInovasi KolaborasiInovasi[]
InfoTekno InfoTekno[]
PengaduanMasyarakat PengaduanMasyarakat[]
KegiatanDesa KegiatanDesa[]
ProgramInovasi ProgramInovasi[]
PejabatDesa PejabatDesa[]
MediaSosial MediaSosial[]
DesaAntiKorupsi DesaAntiKorupsi[]
SDGSDesa SDGSDesa[]
APBDesImage APBDes[] @relation("APBDesImage")
APBDesFile APBDes[] @relation("APBDesFile")
PrestasiDesa PrestasiDesa[]
DataPerpustakaan DataPerpustakaan[]
}
//========================================= MENU LANDING PAGE ========================================= //
//========================================= PROFILE ========================================= //
model PejabatDesa {
id String @id @default(cuid())
name String @unique @db.VarChar(255)
position String
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 ProgramInovasi {
id String @id @default(cuid())
name String @unique @db.VarChar(255)
description String? @db.Text
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
link String? @db.VarChar(255)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime? @default(now())
isActive Boolean @default(true)
}
model MediaSosial {
id String @id @default(cuid())
name String
image FileStorage @relation(fields: [imageId], references: [id])
imageId String
iconUrl String? @db.VarChar(255)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
isActive Boolean @default(true)
}
//========================================= PROFILE ========================================= //
model DesaAntiKorupsi {
id String @id @default(cuid())
name String @unique
deskripsi String @db.Text
kategori KategoriDesaAntiKorupsi @relation(fields: [kategoriId], references: [id])
kategoriId String
file FileStorage @relation(fields: [fileId], references: [id])
fileId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model KategoriDesaAntiKorupsi {
id String @id @default(cuid())
name String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
DesaAntiKorupsi DesaAntiKorupsi[]
}
//========================================= SDGS Desa ========================================= //
model SDGSDesa {
id String @id @default(cuid())
name String @unique
jumlah String
image FileStorage @relation(fields: [imageId], references: [id])
imageId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
//========================================= APBDes ========================================= //
model APBDes {
id String @id @default(cuid())
name String @unique
jumlah String
image FileStorage @relation("APBDesImage", fields: [imageId], references: [id])
imageId String
file FileStorage @relation("APBDesFile", fields: [fileId], references: [id])
fileId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
//========================================= PRESTASI DESA ========================================= //
model PrestasiDesa {
id String @id @default(cuid())
name String @unique
deskripsi String @db.Text
kategori KategoriPrestasiDesa @relation(fields: [kategoriId], references: [id])
kategoriId String
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 KategoriPrestasiDesa {
id String @id @default(cuid())
name String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
PrestasiDesa PrestasiDesa[]
}
//========================================= INDEKS KEPUASAN MASYARAKAT ========================================= //
// Entitas Survey
model Survey {
id String @id @default(cuid())
title String // Judul survei
totalRespondents Int // Total jumlah responden
averageScore Float // Rata-rata skor
monthlyStats MonthlyStat[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// Entitas Statistik Bulanan
model MonthlyStat {
id String @id @default(cuid())
month String // Nama bulan (e.g., "Januari", "Februari")
respondentsCount Int // Jumlah responden per bulan
surveyId String @unique(map: "monthly_stat_survey_id_month_key")
survey Survey @relation(fields: [surveyId], references: [id])
AgeStat AgeStat[]
ResponseStat ResponseStat[]
genderStat genderStat[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// Entitas Gender
model genderStat {
id String @id @default(cuid())
laki Int
perempuan Int
percentLaki Float
percentPerempuan Float
total Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
MonthlyStat MonthlyStat? @relation(fields: [monthlyStatId], references: [id])
monthlyStatId String?
}
// Entitas Age
model AgeStat {
id String @id @default(cuid())
group String // "18-44", "45+" dll
count Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
MonthlyStat MonthlyStat? @relation(fields: [monthlyStatId], references: [id])
monthlyStatId String?
}
// Entitas Response
model ResponseStat {
id String @id @default(cuid())
label String // BAIK / BURUK
count Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
MonthlyStat MonthlyStat? @relation(fields: [monthlyStatId], references: [id])
monthlyStatId String?
}
//========================================= MENU PPID ========================================= //
@@ -1499,6 +1703,35 @@ model DataLingkunganDesa {
isActive Boolean @default(true)
}
// ========================================= GOTONG ROYONG ========================================= //
model KegiatanDesa {
id String @id @default(uuid())
judul String
deskripsiSingkat String
deskripsiLengkap String
tanggal DateTime
lokasi String
partisipan Int
image FileStorage @relation(fields: [imageId], references: [id])
imageId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
kategoriKegiatan KategoriKegiatan @relation(fields: [kategoriKegiatanId], references: [id])
kategoriKegiatanId String
}
model KategoriKegiatan {
id String @id @default(cuid())
nama String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
KegiatanDesa KegiatanDesa[]
}
// ========================================= EDUKASI LINGKUNGAN ========================================= //
model TujuanEdukasiLingkungan {
id String @id @default(cuid())
@@ -1560,3 +1793,252 @@ model NilaiKonservasiAdat {
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= MENU PENDIDIKAN ========================================= //
// ========================================= INFO SEKOLAH & PAUD ========================================= //
model JenjangPendidikan {
id String @id @default(cuid())
nama String // TK/PAUD, SD, SMP, SMA/SMK
lembagas Lembaga[] // Relasi ke lembaga
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model Lembaga {
id String @id @default(cuid())
nama String
jenjangPendidikan JenjangPendidikan @relation(fields: [jenjangId], references: [id])
jenjangId String
siswa Siswa[]
pengajar Pengajar[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model Siswa {
id String @id @default(cuid())
nama String
lembaga Lembaga @relation(fields: [lembagaId], references: [id])
lembagaId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model Pengajar {
id String @id @default(cuid())
nama String
lembaga Lembaga @relation(fields: [lembagaId], references: [id])
lembagaId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= BEASISWA DESA ========================================= //
model KeunggulanProgram {
id String @id @default(cuid())
judul String
deskripsi String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model BeasiswaPendaftar {
id String @id @default(cuid())
namaLengkap String
nik String @unique
tempatLahir String
tanggalLahir DateTime
jenisKelamin JenisKelamin
kewarganegaraan String
agama Agama
alamatKTP String
alamatDomisili String?
noHp String
email String @unique
statusPernikahan StatusPernikahan
ukuranBaju UkuranBaju?
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
enum JenisKelamin {
LAKI_LAKI
PEREMPUAN
}
enum Agama {
ISLAM
KRISTEN_PROTESTAN
KRISTEN_KATOLIK
HINDU
BUDDHA
KONGHUCU
LAINNYA
}
enum StatusPernikahan {
BELUM_MENIKAH
MENIKAH
JANDA_DUDA
}
enum UkuranBaju {
S
M
L
XL
XXL
LAINNYA
}
// ========================================= PROGRAM PENDIDIKAN ANAK ========================================= //
model TujuanProgram {
id String @id @default(cuid())
judul String
deskripsi String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model ProgramUnggulan {
id String @id @default(cuid())
judul String
deskripsi String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= BIMBINGAN BELAJAR DESA ========================================= //
model TujuanBimbinganBelajarDesa {
id String @id @default(cuid())
judul String
deskripsi String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model LokasiJadwalBimbinganBelajarDesa {
id String @id @default(cuid())
judul String
deskripsi String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model FasilitasBimbinganBelajarDesa {
id String @id @default(cuid())
judul String
deskripsi String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= PENDIDIKAN NON FORMAL ========================================= //
model TujuanPendidikanNonFormal {
id String @id @default(cuid())
judul String
deskripsi String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model TempatKegiatan {
id String @id @default(cuid())
judul String
deskripsi String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model JenisProgramYangDiselenggarakan {
id String @id @default(cuid())
judul String
deskripsi String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= PERPUSTAKAAN ========================================= //
model DataPerpustakaan {
id String @id @default(cuid())
judul String
deskripsi String @db.Text
kategori KategoriBuku @relation(fields: [kategoriId], references: [id])
kategoriId String
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 KategoriBuku {
id String @id @default(cuid())
name String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
DataPerpustakaan DataPerpustakaan[]
}
model User {
id String @id @default(cuid())
nama String
email String @unique
password String
role Role @relation(fields: [roleId], references: [id])
roleId String
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Role {
id String @id @default(cuid())
name String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
User User[]
}
// ========================================= DATA PENDIDIKAN ========================================= //
model DataPendidikan {
id String @id @default(cuid())
name String
jumlah String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}

View File

@@ -28,6 +28,15 @@ import contohEdukasiLingkungan from './data/lingkungan/edukasi-lingkungan/contoh
import nilaiKonservasiAdat from './data/lingkungan/konservasi-adat-bali/nilai-konservasi-adat.json';
import bentukKonservasiBerdasarkanAdat from './data/lingkungan/konservasi-adat-bali/bentuk-konservasi.json';
import filosofiTriHita from './data/lingkungan/konservasi-adat-bali/filosofi-tri-hita.json';
import profilePejabatDesa from './data/landing-page/profile.json';
import tujuanProgram from './data/pendidikan/program-pendidikan-anak/tujuan-program.json';
import tujuanProgram2 from './data/pendidikan/pendidikan-non-formal/tujuan-program2.json';
import programUnggulan from './data/pendidikan/program-pendidikan-anak/program-unggulan.json';
import tujuanBimbinganBelajarDesa from './data/pendidikan/bimbingan-belajar-desa/tujuan-bimbingan-belajar-desa.json';
import lokasiJadwalBimbinganBelajarDesa from './data/pendidikan/bimbingan-belajar-desa/lokasi-dan-jadwal.json';
import fasilitasBimbinganBelajarDesa from './data/pendidikan/bimbingan-belajar-desa/fasilitas-yang-disediakan.json';
import tempatKegiatan from './data/pendidikan/pendidikan-non-formal/tempat-kegiatan.json';
import jenisProgramYangDiselenggarakan from './data/pendidikan/pendidikan-non-formal/jenis-program-yang-diselenggarakan.json';
(async () => {
@@ -578,6 +587,150 @@ import filosofiTriHita from './data/lingkungan/konservasi-adat-bali/filosofi-tri
console.log("nilai konservasi adat success ...");
for (const p of profilePejabatDesa) {
await prisma.pejabatDesa.upsert({
where: { id: p.id },
update: {
name: p.name,
position: p.position,
},
create: {
id: p.id,
name: p.name,
position: p.position,
},
});
}
console.log("✅ profilePejabatDesa seeded without imageId (editable later via UI)");
for (const t of tujuanProgram) {
await prisma.tujuanProgram.upsert({
where: { id: t.id },
update: {
judul: t.judul,
deskripsi: t.deskripsi,
},
create: {
id: t.id,
judul: t.judul,
deskripsi: t.deskripsi,
},
});
}
console.log("✅ tujuan program seeded (editable later via UI)");
for (const t of programUnggulan) {
await prisma.programUnggulan.upsert({
where: { id: t.id },
update: {
judul: t.judul,
deskripsi: t.deskripsi,
},
create: {
id: t.id,
judul: t.judul,
deskripsi: t.deskripsi,
},
});
}
console.log("✅ program unggulan seeded (editable later via UI)");
for (const t of tujuanBimbinganBelajarDesa) {
await prisma.tujuanBimbinganBelajarDesa.upsert({
where: { id: t.id },
update: {
judul: t.judul,
deskripsi: t.deskripsi,
},
create: {
id: t.id,
judul: t.judul,
deskripsi: t.deskripsi,
},
});
}
console.log("✅ tujuan bimbingan belajar desa seeded (editable later via UI)");
for (const t of lokasiJadwalBimbinganBelajarDesa) {
await prisma.lokasiJadwalBimbinganBelajarDesa.upsert({
where: { id: t.id },
update: {
judul: t.judul,
deskripsi: t.deskripsi,
},
create: {
id: t.id,
judul: t.judul,
deskripsi: t.deskripsi,
},
});
}
console.log("✅ lokasi jadwal bimbingan belajar desa seeded (editable later via UI)");
for (const t of fasilitasBimbinganBelajarDesa) {
await prisma.fasilitasBimbinganBelajarDesa.upsert({
where: { id: t.id },
update: {
judul: t.judul,
deskripsi: t.deskripsi,
},
create: {
id: t.id,
judul: t.judul,
deskripsi: t.deskripsi,
},
});
}
console.log("✅ fasilitas bimbingan belajar desa seeded (editable later via UI)");
for (const t of tujuanProgram2) {
await prisma.tujuanPendidikanNonFormal.upsert({
where: { id: t.id },
update: {
judul: t.judul,
deskripsi: t.deskripsi,
},
create: {
id: t.id,
judul: t.judul,
deskripsi: t.deskripsi,
},
});
}
console.log("✅ fasilitas bimbingan belajar desa seeded (editable later via UI)");
for (const t of tempatKegiatan) {
await prisma.tempatKegiatan.upsert({
where: { id: t.id },
update: {
judul: t.judul,
deskripsi: t.deskripsi,
},
create: {
id: t.id,
judul: t.judul,
deskripsi: t.deskripsi,
},
});
}
console.log("✅ fasilitas bimbingan belajar desa seeded (editable later via UI)");
for (const t of jenisProgramYangDiselenggarakan) {
await prisma.jenisProgramYangDiselenggarakan.upsert({
where: { id: t.id },
update: {
judul: t.judul,
deskripsi: t.deskripsi,
},
create: {
id: t.id,
judul: t.judul,
deskripsi: t.deskripsi,
},
});
}
console.log("✅ fasilitas bimbingan belajar desa seeded (editable later via UI)");
})()
.then(() => prisma.$disconnect())
.catch((e) => {

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -5,176 +5,190 @@ import { proxy } from "valtio";
import { z } from "zod";
const templateJumlahPendudukMiskin = z.object({
year: z.number().min(1, "Data tahun harus diisi"),
totalPoorPopulation: z.number().min(1, "Data total penduduk miskin harus diisi"),
year: z.number().min(1, "Data tahun harus diisi"),
totalPoorPopulation: z
.number()
.min(1, "Data total penduduk miskin harus diisi"),
});
type JumlahPendudukMiskin = Prisma.GrafikJumlahPendudukMiskinGetPayload<{
select: {
id: true;
year: true;
totalPoorPopulation: true;
};
select: {
id: true;
year: true;
totalPoorPopulation: true;
};
}>;
const defaultForm: Omit<JumlahPendudukMiskin, 'id'> & { id?: string } = {
year: 0,
totalPoorPopulation: 0,
const defaultForm: Omit<JumlahPendudukMiskin, "id"> & { id?: string } = {
year: 0,
totalPoorPopulation: 0,
};
const jumlahPendudukMiskin = proxy({
create: {
form: defaultForm,
loading: false,
async create() {
const cek = templateJumlahPendudukMiskin.safeParse(
jumlahPendudukMiskin.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
jumlahPendudukMiskin.create.loading = true;
const res = await ApiFetch.api.ekonomi.jumlahpendudukmiskin[
"create"
].post(jumlahPendudukMiskin.create.form);
if (res.status === 200) {
const id = res.data?.data?.id;
if (id) {
toast.success("Success create");
jumlahPendudukMiskin.create.form = {
year: 0,
totalPoorPopulation: 0,
};
jumlahPendudukMiskin.findMany.load();
return id;
}
}
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
} finally {
jumlahPendudukMiskin.create.loading = false;
}
},
},
findMany: {
data: null as
| Prisma.GrafikJumlahPendudukMiskinGetPayload<{
select: { id: true; year: true; totalPoorPopulation: true; };
}>[]
| null,
loading: false,
async load() {
const res = await ApiFetch.api.ekonomi.jumlahpendudukmiskin[
"find-many"
].get();
if (res.status === 200) {
jumlahPendudukMiskin.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.GrafikJumlahPendudukMiskinGetPayload<{
select: { id: true; year: true; totalPoorPopulation: true; };
}> | null,
async load(id: string) {
try {
const res = await fetch(
`/api/ekonomi/jumlahpendudukmiskin/${id}`
);
if (res.ok) {
const data = await res.json();
jumlahPendudukMiskin.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
jumlahPendudukMiskin.findUnique.data = null;
}
} catch (error) {
console.error("Error loading grafik jumlah penduduk miskin:", error);
jumlahPendudukMiskin.findUnique.data = null;
}
},
},
update: {
id: "",
form: {...defaultForm},
loading: false,
async byId() {
// Method implementation if needed
},
async submit() {
const id = this.id;
if (!id) {
toast.warn("ID tidak valid");
return null;
}
const cek = templateJumlahPendudukMiskin.safeParse(this.form);
if (!cek.success) {
const err = `[${cek.error.issues.map((v) => (v.path as string[]).join(".")).join("\n")}] required`;
toast.error(err);
return null;
}
this.loading = true;
try {
const response = await fetch(
`/api/ekonomi/jumlahpendudukmiskin/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
});
const result = await response.json();
if (!response.ok || !result?.success) {
throw new Error(result?.message || "Gagal update data");
}
toast.success("Berhasil update data!");
await jumlahPendudukMiskin.findMany.load();
return result.data;
} catch (error) {
console.error("Error update data grafik jumlah penduduk miskin:", error);
toast.error("Gagal update data grafik jumlah penduduk miskin");
} finally {
this.loading = false;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
jumlahPendudukMiskin.delete.loading = true;
const response = await fetch(`/api/ekonomi/jumlahpendudukmiskin/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Grafik jumlah penduduk miskin berhasil dihapus");
await jumlahPendudukMiskin.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus grafik jumlah penduduk miskin");
}
} catch (error) {
console.error("Gagal delete grafik jumlah penduduk miskin:", error);
toast.error("Terjadi kesalahan saat menghapus grafik jumlah penduduk miskin");
} finally {
jumlahPendudukMiskin.delete.loading = false;
}
},
create: {
form: defaultForm,
loading: false,
async create() {
const cek = templateJumlahPendudukMiskin.safeParse(
jumlahPendudukMiskin.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
})
export default jumlahPendudukMiskin
try {
jumlahPendudukMiskin.create.loading = true;
const res = await ApiFetch.api.ekonomi.jumlahpendudukmiskin[
"create"
].post(jumlahPendudukMiskin.create.form);
if (res.status === 200) {
const id = res.data?.data?.id;
if (id) {
toast.success("Success create");
jumlahPendudukMiskin.create.form = {
year: 0,
totalPoorPopulation: 0,
};
jumlahPendudukMiskin.findMany.load();
return id;
}
}
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
} finally {
jumlahPendudukMiskin.create.loading = false;
}
},
},
findMany: {
data: null as
| Prisma.GrafikJumlahPendudukMiskinGetPayload<{
select: { id: true; year: true; totalPoorPopulation: true };
}>[]
| null,
loading: false,
async load() {
const res = await ApiFetch.api.ekonomi.jumlahpendudukmiskin[
"find-many"
].get();
if (res.status === 200) {
jumlahPendudukMiskin.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.GrafikJumlahPendudukMiskinGetPayload<{
select: { id: true; year: true; totalPoorPopulation: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/ekonomi/jumlahpendudukmiskin/${id}`);
if (res.ok) {
const data = await res.json();
jumlahPendudukMiskin.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
jumlahPendudukMiskin.findUnique.data = null;
}
} catch (error) {
console.error("Error loading grafik jumlah penduduk miskin:", error);
jumlahPendudukMiskin.findUnique.data = null;
}
},
},
update: {
id: "",
form: { ...defaultForm },
loading: false,
async byId() {
// Method implementation if needed
},
async submit() {
const id = this.id;
if (!id) {
toast.warn("ID tidak valid");
return null;
}
const cek = templateJumlahPendudukMiskin.safeParse(this.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => (v.path as string[]).join("."))
.join("\n")}] required`;
toast.error(err);
return null;
}
this.loading = true;
try {
const response = await fetch(
`/api/ekonomi/jumlahpendudukmiskin/${id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
}
);
const result = await response.json();
if (!response.ok || !result?.success) {
throw new Error(result?.message || "Gagal update data");
}
toast.success("Berhasil update data!");
await jumlahPendudukMiskin.findMany.load();
return result.data;
} catch (error) {
console.error(
"Error update data grafik jumlah penduduk miskin:",
error
);
toast.error("Gagal update data grafik jumlah penduduk miskin");
} finally {
this.loading = false;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
jumlahPendudukMiskin.delete.loading = true;
const response = await fetch(
`/api/ekonomi/jumlahpendudukmiskin/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(
result.message || "Grafik jumlah penduduk miskin berhasil dihapus"
);
await jumlahPendudukMiskin.findMany.load(); // refresh list
} else {
toast.error(
result?.message || "Gagal menghapus grafik jumlah penduduk miskin"
);
}
} catch (error) {
console.error("Gagal delete grafik jumlah penduduk miskin:", error);
toast.error(
"Terjadi kesalahan saat menghapus grafik jumlah penduduk miskin"
);
} finally {
jumlahPendudukMiskin.delete.loading = false;
}
},
},
});
export default jumlahPendudukMiskin;

View File

@@ -0,0 +1,224 @@
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 templateapbDesaForm = z.object({
name: z.string().min(1, "Judul minimal 1 karakter"),
jumlah: z.string().min(1, "Deskripsi minimal 1 karakter"),
imageId: z.string().min(1, "File minimal 1"),
fileId: z.string().min(1, "File minimal 1"),
});
const defaultapbdesForm = {
name: "",
jumlah: "",
imageId: "",
fileId: "",
};
const apbdes = proxy({
create: {
form: { ...defaultapbdesForm },
loading: false,
async create() {
const cek = templateapbDesaForm.safeParse(apbdes.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
apbdes.create.loading = true;
const res = await ApiFetch.api.landingpage.apbdes["create"].post({
...apbdes.create.form,
});
if (res.status === 200) {
apbdes.findMany.load();
return toast.success("Data berhasil ditambahkan");
}
return toast.error("Gagal menambahkan data");
} catch (error) {
console.log(error);
toast.error("Gagal menambahkan data");
} finally {
apbdes.create.loading = false;
}
},
},
findMany: {
data: null as Array<
Prisma.APBDesGetPayload<{
include: {
image: true;
file: true;
};
}>
> | null,
async load() {
const res = await ApiFetch.api.landingpage.apbdes["find-many"].get();
if (res.status === 200) {
apbdes.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.APBDesGetPayload<{
include: {
image: true;
file: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/landingpage/apbdes/${id}`);
if (res.ok) {
const data = await res.json();
apbdes.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
apbdes.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
apbdes.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
apbdes.delete.loading = true;
const response = await fetch(`/api/landingpage/apbdes/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "apbdes berhasil dihapus");
await apbdes.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus apbdes");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus apbdes");
} finally {
apbdes.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...defaultapbdesForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
apbdes.edit.loading = true;
const response = await fetch(`/api/landingpage/apbdes/${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,
jumlah: data.jumlah,
imageId: data.imageId,
fileId: data.fileId,
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading apbdes:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
} finally {
apbdes.edit.loading = false;
}
},
async update() {
const cek = templateapbDesaForm.safeParse(apbdes.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
apbdes.edit.loading = true;
const response = await fetch(`/api/landingpage/apbdes/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
jumlah: this.form.jumlah,
imageId: this.form.imageId,
fileId: this.form.fileId,
}),
});
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 apbdes");
await apbdes.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal mengupdate apbdes");
}
} catch (error) {
console.error("Error updating apbdes:", error);
toast.error(
error instanceof Error ? error.message : "Gagal mengupdate apbdes"
);
return false;
} finally {
apbdes.edit.loading = false;
}
},
reset() {
apbdes.edit.id = "";
apbdes.edit.form = { ...defaultapbdesForm };
},
},
});
export default apbdes;

View File

@@ -0,0 +1,485 @@
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 templateDesaAntiKorupsiForm = z.object({
name: z.string().min(1, "Judul minimal 1 karakter"),
deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
kategoriId: z.string().min(1, "Kategori minimal 1"),
fileId: z.string().min(1, "File minimal 1"),
});
const defaultDesaAntiKorupsiForm = {
name: "",
deskripsi: "",
kategoriId: "",
fileId: "",
};
const desaAntikorupsi = proxy({
create: {
form: { ...defaultDesaAntiKorupsiForm },
loading: false,
async create() {
const cek = templateDesaAntiKorupsiForm.safeParse(
desaAntikorupsi.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
desaAntikorupsi.create.loading = true;
const res = await ApiFetch.api.landingpage.desaantikorupsi[
"create"
].post({
...desaAntikorupsi.create.form,
});
if (res.status === 200) {
desaAntikorupsi.findMany.load();
return toast.success("Data berhasil ditambahkan");
}
return toast.error("Gagal menambahkan data");
} catch (error) {
console.log(error);
toast.error("Gagal menambahkan data");
} finally {
desaAntikorupsi.create.loading = false;
}
},
},
findMany: {
data: null as Array<
Prisma.DesaAntiKorupsiGetPayload<{
include: {
file: true;
kategori: true;
};
}>
> | null,
async load() {
const res = await ApiFetch.api.landingpage.desaantikorupsi[
"find-many"
].get();
if (res.status === 200) {
desaAntikorupsi.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.DesaAntiKorupsiGetPayload<{
include: {
file: true;
kategori: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/landingpage/desaantikorupsi/${id}`);
if (res.ok) {
const data = await res.json();
desaAntikorupsi.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
desaAntikorupsi.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
desaAntikorupsi.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
desaAntikorupsi.delete.loading = true;
const response = await fetch(
`/api/landingpage/desaantikorupsi/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "desa anti korupsi berhasil dihapus");
await desaAntikorupsi.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus desa anti korupsi");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus desa anti korupsi");
} finally {
desaAntikorupsi.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...defaultDesaAntiKorupsiForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
desaAntikorupsi.edit.loading = true;
const response = await fetch(`/api/landingpage/desaantikorupsi/${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,
kategoriId: data.kategoriId,
fileId: data.fileId,
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading desa anti korupsi:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
} finally {
desaAntikorupsi.edit.loading = false;
}
},
async update() {
const cek = templateDesaAntiKorupsiForm.safeParse(
desaAntikorupsi.edit.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
desaAntikorupsi.edit.loading = true;
const response = await fetch(
`/api/landingpage/desaantikorupsi/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
deskripsi: this.form.deskripsi,
kategoriId: this.form.kategoriId,
fileId: this.form.fileId,
}),
}
);
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 desa anti korupsi");
await desaAntikorupsi.findMany.load(); // refresh list
return true;
} else {
throw new Error(
result.message || "Gagal mengupdate desa anti korupsi"
);
}
} catch (error) {
console.error("Error updating desa anti korupsi:", error);
toast.error(
error instanceof Error
? error.message
: "Gagal mengupdate desa anti korupsi"
);
return false;
} finally {
desaAntikorupsi.edit.loading = false;
}
},
reset() {
desaAntikorupsi.edit.id = "";
desaAntikorupsi.edit.form = { ...defaultDesaAntiKorupsiForm };
},
},
});
// ========================================= KATEGORI desa anti korupsi ========================================= //
const kategoriDesaAntiKorupsiForm = z.object({
name: z.string().min(1, "Nama minimal 1 karakter"),
});
const kategoriDesaAntiKorupsiDefaultForm = {
name: "",
};
const kategoriDesaAntiKorupsi = proxy({
create: {
form: { ...kategoriDesaAntiKorupsiDefaultForm },
loading: false,
async create() {
const cek = kategoriDesaAntiKorupsiForm.safeParse(
kategoriDesaAntiKorupsi.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
kategoriDesaAntiKorupsi.create.loading = true;
const res = await ApiFetch.api.landingpage.kategoridak["create"].post(
kategoriDesaAntiKorupsi.create.form
);
if (res.status === 200) {
kategoriDesaAntiKorupsi.findMany.load();
return toast.success("Data berhasil ditambahkan");
}
return toast.error("Gagal menambahkan data");
} catch (error) {
console.log(error);
toast.error("Gagal menambahkan data");
} finally {
kategoriDesaAntiKorupsi.create.loading = false;
}
},
},
findMany: {
data: null as Array<{
id: string;
name: string;
}> | null,
async load() {
const res = await ApiFetch.api.landingpage.kategoridak["find-many"].get();
if (res.status === 200) {
kategoriDesaAntiKorupsi.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.KategoriDesaAntiKorupsiGetPayload<{
omit: { isActive: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/landingpage/kategoridak/${id}`);
if (res.ok) {
const data = await res.json();
kategoriDesaAntiKorupsi.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
kategoriDesaAntiKorupsi.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
kategoriDesaAntiKorupsi.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
kategoriDesaAntiKorupsi.delete.loading = true;
const response = await fetch(
`/api/landingpage/kategoridak/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Kategori desa anti korupsi berhasil dihapus");
await kategoriDesaAntiKorupsi.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus kategori desa anti korupsi");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus kategori desa anti korupsi");
} finally {
kategoriDesaAntiKorupsi.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...kategoriDesaAntiKorupsiDefaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/landingpage/kategoridak/${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,
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading kategori desa anti korupsi:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = kategoriDesaAntiKorupsiForm.safeParse(
kategoriDesaAntiKorupsi.edit.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
kategoriDesaAntiKorupsi.edit.loading = true;
const response = await fetch(
`/api/landingpage/kategoridak/${kategoriDesaAntiKorupsi.edit.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: kategoriDesaAntiKorupsi.edit.form.name,
}),
}
);
// Clone the response to avoid 'body already read' error
const responseClone = response.clone();
try {
const result = await response.json();
if (!response.ok) {
console.error(
"Update failed with status:",
response.status,
"Response:",
result
);
throw new Error(
result?.message ||
`Gagal mengupdate kategori desa anti korupsi (${response.status})`
);
}
if (result.success) {
toast.success(
result.message ||
"Berhasil memperbarui kategori desa anti korupsi"
);
await kategoriDesaAntiKorupsi.findMany.load(); // refresh list
return true;
} else {
throw new Error(
result.message || "Gagal mengupdate kategori desa anti korupsi"
);
}
} catch (error) {
// If JSON parsing fails, try to get the response text for better error messages
try {
const text = await responseClone.text();
console.error("Error response text:", text);
throw new Error(`Gagal memproses respons dari server: ${text}`);
} catch (textError) {
console.error("Error parsing response as text:", textError);
console.error("Original error:", error);
throw new Error("Gagal memproses respons dari server");
}
}
} catch (error) {
console.error("Error updating kategori desa anti korupsi:", error);
toast.error(
error instanceof Error
? error.message
: "Gagal mengupdate kategori desa anti korupsi"
);
return false;
} finally {
kategoriDesaAntiKorupsi.edit.loading = false;
}
},
reset() {
kategoriDesaAntiKorupsi.edit.id = "";
kategoriDesaAntiKorupsi.edit.form = {
...kategoriDesaAntiKorupsiDefaultForm,
};
},
},
});
const korupsiState = proxy({
desaAntikorupsi,
kategoriDesaAntiKorupsi,
});
export default korupsiState;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,486 @@
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 templateprestasiDesaForm = z.object({
name: z.string().min(1, "Judul minimal 1 karakter"),
deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
imageId: z.string().min(1, "File minimal 1"),
kategoriId: z.string().min(1, "Kategori minimal 1 karakter"),
});
const defaultprestasiDesaForm = {
name: "",
deskripsi: "",
imageId: "",
kategoriId: "",
};
const prestasiDesa = proxy({
create: {
form: { ...defaultprestasiDesaForm },
loading: false,
async create() {
const cek = templateprestasiDesaForm.safeParse(
prestasiDesa.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
prestasiDesa.create.loading = true;
const res = await ApiFetch.api.landingpage.prestasidesa[
"create"
].post({
...prestasiDesa.create.form,
});
if (res.status === 200) {
prestasiDesa.findMany.load();
return toast.success("Data berhasil ditambahkan");
}
return toast.error("Gagal menambahkan data");
} catch (error) {
console.log(error);
toast.error("Gagal menambahkan data");
} finally {
prestasiDesa.create.loading = false;
}
},
},
findMany: {
data: null as Array<
Prisma.PrestasiDesaGetPayload<{
include: {
image: true;
kategori: true;
};
}>
> | null,
async load() {
const res = await ApiFetch.api.landingpage.prestasidesa[
"find-many"
].get();
if (res.status === 200) {
prestasiDesa.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.PrestasiDesaGetPayload<{
include: {
image: true;
kategori: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/landingpage/prestasidesa/${id}`);
if (res.ok) {
const data = await res.json();
prestasiDesa.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
prestasiDesa.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
prestasiDesa.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
prestasiDesa.delete.loading = true;
const response = await fetch(
`/api/landingpage/prestasidesa/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "prestasi desa berhasil dihapus");
await prestasiDesa.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus prestasi desa");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus prestasi desa");
} finally {
prestasiDesa.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...defaultprestasiDesaForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
prestasiDesa.edit.loading = true;
const response = await fetch(`/api/landingpage/prestasidesa/${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,
kategoriId: data.kategoriId,
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading prestasi desa:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
} finally {
prestasiDesa.edit.loading = false;
}
},
async update() {
const cek = templateprestasiDesaForm.safeParse(
prestasiDesa.edit.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
prestasiDesa.edit.loading = true;
const response = await fetch(
`/api/landingpage/prestasidesa/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
deskripsi: this.form.deskripsi,
imageId: this.form.imageId,
kategoriId: this.form.kategoriId,
}),
}
);
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 prestasi desa");
await prestasiDesa.findMany.load(); // refresh list
return true;
} else {
throw new Error(
result.message || "Gagal mengupdate prestasi desa"
);
}
} catch (error) {
console.error("Error updating prestasi desa:", error);
toast.error(
error instanceof Error
? error.message
: "Gagal mengupdate prestasi desa"
);
return false;
} finally {
prestasiDesa.edit.loading = false;
}
},
reset() {
prestasiDesa.edit.id = "";
prestasiDesa.edit.form = { ...defaultprestasiDesaForm };
},
},
});
// ========================================= KATEGORI kegiatan ========================================= //
const kategoriPrestasiForm = z.object({
name: z.string().min(1, "Nama minimal 1 karakter"),
});
const kategoriPrestasiDefaultForm = {
name: "",
};
const kategoriPrestasi = proxy({
create: {
form: { ...kategoriPrestasiDefaultForm },
loading: false,
async create() {
const cek = kategoriPrestasiForm.safeParse(kategoriPrestasi.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
kategoriPrestasi.create.loading = true;
const res = await ApiFetch.api.landingpage.kategoriprestasi[
"create"
].post(kategoriPrestasi.create.form);
if (res.status === 200) {
kategoriPrestasi.findMany.load();
return toast.success("Data berhasil ditambahkan");
}
return toast.error("Gagal menambahkan data");
} catch (error) {
console.log(error);
toast.error("Gagal menambahkan data");
} finally {
kategoriPrestasi.create.loading = false;
}
},
},
findMany: {
data: null as Array<{
id: string;
name: string;
}> | null,
async load() {
const res = await ApiFetch.api.landingpage.kategoriprestasi[
"find-many"
].get();
if (res.status === 200) {
kategoriPrestasi.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.KategoriPrestasiDesaGetPayload<{
omit: { isActive: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(
`/api/landingpage/kategoriprestasi/${id}`
);
if (res.ok) {
const data = await res.json();
kategoriPrestasi.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
kategoriPrestasi.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
kategoriPrestasi.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
kategoriPrestasi.delete.loading = true;
const response = await fetch(
`/api/landingpage/kategoriprestasi/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Kategori prestasi berhasil dihapus");
await kategoriPrestasi.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus kategori prestasi");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus kategori prestasi");
} finally {
kategoriPrestasi.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...kategoriPrestasiDefaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(
`/api/landingpage/kategoriprestasi/${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,
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading kategori prestasi:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = kategoriPrestasiForm.safeParse(kategoriPrestasi.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
kategoriPrestasi.edit.loading = true;
const response = await fetch(
`/api/landingpage/kategoriprestasi/${kategoriPrestasi.edit.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: kategoriPrestasi.edit.form.name,
}),
}
);
// Clone the response to avoid 'body already read' error
const responseClone = response.clone();
try {
const result = await response.json();
if (!response.ok) {
console.error(
"Update failed with status:",
response.status,
"Response:",
result
);
throw new Error(
result?.message ||
`Gagal mengupdate kategori prestasi (${response.status})`
);
}
if (result.success) {
toast.success(
result.message || "Berhasil memperbarui kategori prestasi"
);
await kategoriPrestasi.findMany.load(); // refresh list
return true;
} else {
throw new Error(
result.message || "Gagal mengupdate kategori prestasi"
);
}
} catch (error) {
// If JSON parsing fails, try to get the response text for better error messages
try {
const text = await responseClone.text();
console.error("Error response text:", text);
throw new Error(`Gagal memproses respons dari server: ${text}`);
} catch (textError) {
console.error("Error parsing response as text:", textError);
console.error("Original error:", error);
throw new Error("Gagal memproses respons dari server");
}
}
} catch (error) {
console.error("Error updating kategori prestasi:", error);
toast.error(
error instanceof Error
? error.message
: "Gagal mengupdate kategori prestasi"
);
return false;
} finally {
kategoriPrestasi.edit.loading = false;
}
},
reset() {
kategoriPrestasi.edit.id = "";
kategoriPrestasi.edit.form = { ...kategoriPrestasiDefaultForm };
},
},
});
const prestasiState = proxy({
prestasiDesa,
kategoriPrestasi,
});
export default prestasiState;

View File

@@ -0,0 +1,634 @@
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 templateProgramInovasi = z.object({
name: z.string().min(3, "Nama minimal 3 karakter"),
description: z.string().min(3, "Deskripsi minimal 3 karakter"),
imageId: z.string().min(1, "Gambar wajib dipilih"),
link: z.string().min(3, "Link minimal 3 karakter"),
});
type ProgramInovasiForm = Prisma.ProgramInovasiGetPayload<{
select: {
name: true;
description: true;
imageId: true;
link: true;
};
}>;
const programInovasi = proxy({
create: {
form: {} as ProgramInovasiForm,
loading: false,
async create() {
// Ensure all required fields are non-null
const formData = {
name: programInovasi.create.form.name || "",
description: programInovasi.create.form.description || "",
imageId: programInovasi.create.form.imageId || "",
link: programInovasi.create.form.link || "",
};
const cek = templateProgramInovasi.safeParse(formData);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
programInovasi.create.loading = true;
const res = await ApiFetch.api.landingpage.programinovasi[
"create"
].post(formData);
if (res.status === 200) {
programInovasi.findMany.load();
return toast.success("success create");
}
console.log(res);
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
} finally {
programInovasi.create.loading = false;
}
},
},
findMany: {
data: null as
| Prisma.ProgramInovasiGetPayload<{ include: { image: true } }>[]
| null,
async load() {
const res = await ApiFetch.api.landingpage.programinovasi[
"find-many"
].get();
if (res.status === 200) {
programInovasi.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.ProgramInovasiGetPayload<{
include: {
image: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/landingpage/programinovasi/${id}`);
if (res.ok) {
const data = await res.json();
programInovasi.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch program inovasi:", res.statusText);
programInovasi.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching program inovasi:", error);
programInovasi.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
programInovasi.delete.loading = true;
const response = await fetch(
`/api/landingpage/programinovasi/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Program inovasi berhasil dihapus");
await programInovasi.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus program inovasi");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus program inovasi");
} finally {
programInovasi.delete.loading = false;
}
},
},
update: {
id: "",
form: {} as ProgramInovasiForm,
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/landingpage/programinovasi/${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,
description: data.description,
imageId: data.imageId,
link: data.link,
};
return data;
} else {
throw new Error(
result?.message || "Gagal mengambil data program inovasi"
);
}
} catch (error) {
console.error((error as Error).message);
toast.error("Terjadi kesalahan saat mengambil data program inovasi");
} finally {
programInovasi.update.loading = false;
}
},
async update() {
const cek = templateProgramInovasi.safeParse(programInovasi.update.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
programInovasi.update.loading = true;
const response = await fetch(
`/api/landingpage/programinovasi/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
description: this.form.description,
imageId: this.form.imageId,
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("Berhasil update program inovasi");
await programInovasi.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal update program inovasi");
}
} catch (error) {
console.error("Error updating program inovasi:", error);
toast.error(
error instanceof Error
? error.message
: "Terjadi kesalahan saat update program inovasi"
);
return false;
} finally {
programInovasi.update.loading = false;
}
},
},
});
const templatePejabatDesa = z.object({
name: z.string().min(3, "Nama minimal 3 karakter"),
position: z.string().min(3, "Posisi minimal 3 karakter"),
imageId: z.string().min(1, "Gambar wajib dipilih"),
});
const defaultFormPejabatDesa = {
name: "",
position: "",
imageId: "",
};
type PejabatDesaForm = {
id: string;
name: string;
position: string;
imageId: string | null;
image?: {
id: string;
name: string;
link: string;
path: string;
mimeType: string;
realName: string;
isActive: boolean;
createdAt: Date;
updatedAt: Date;
deletedAt: Date | null;
} | null;
createdAt: Date;
updatedAt: Date;
deletedAt: Date | null;
isActive: boolean;
};
const pejabatDesa = proxy({
findUnique: {
data: null as PejabatDesaForm | null,
loading: false,
error: null as string | null,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
this.loading = true;
this.error = null;
try {
const response = await fetch(`/api/landingpage/pejabatdesa/${id}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result.success) {
this.data = result.data;
return result.data;
} else {
throw new Error(
result.message || "Gagal mengambil data pejabat desa"
);
}
} catch (error) {
const errorMessage = (error as Error).message;
this.error = errorMessage;
console.error("Load pejabat desa error:", errorMessage);
toast.error("Terjadi kesalahan saat mengambil data pejabat desa");
return null;
} finally {
this.loading = false;
}
},
reset() {
this.data = null;
this.error = null;
this.loading = false;
},
},
edit: {
id: "",
form: { ...defaultFormPejabatDesa },
loading: false,
error: null as string | null,
isReadOnly: false,
initialize(profileData: PejabatDesaForm) {
this.id = profileData.id;
this.isReadOnly = false; // Semua data bisa diedit
this.form = {
name: profileData.name || "",
position: profileData.position || "",
imageId: profileData.imageId || "",
};
},
// Update form field
updateField(field: keyof typeof defaultFormPejabatDesa, value: string) {
this.form[field] = value;
},
// Submit form
async submit() {
// Validate form
const validation = templatePejabatDesa.safeParse(this.form);
if (!validation.success) {
const errors = validation.error.issues
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
.join(", ");
toast.error(`Form tidak valid: ${errors}`);
return false;
}
this.loading = true;
this.error = null;
try {
const response = await fetch(
`/api/landingpage/pejabatdesa/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update profile");
// Refresh profile data
await pejabatDesa.findUnique.load(this.id);
return true;
} else {
throw new Error(result.message || "Gagal update profile");
}
} catch (error) {
const errorMessage = (error as Error).message;
this.error = errorMessage;
console.error("Update profile error:", errorMessage);
toast.error("Terjadi kesalahan saat update profile");
return false;
} finally {
this.loading = false;
}
},
reset() {
this.id = "";
this.form = { ...defaultFormPejabatDesa };
this.error = null;
this.loading = false;
this.isReadOnly = false;
},
},
});
const templateMediaSosial = z.object({
name: z.string().min(3, "Nama minimal 3 karakter"),
imageId: z.string().min(1, "Gambar wajib dipilih"),
iconUrl: z.string().min(3, "Icon URL minimal 3 karakter"),
});
type MediaSosialForm = {
name: string;
imageId: string;
iconUrl: string;
};
const mediaSosial = proxy({
create: {
form: {} as MediaSosialForm,
loading: false,
async create() {
// Ensure all required fields are non-null
const formData = {
name: mediaSosial.create.form.name || "",
imageId: mediaSosial.create.form.imageId || "",
iconUrl: mediaSosial.create.form.iconUrl || "",
};
const cek = templateMediaSosial.safeParse(formData);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
mediaSosial.create.loading = true;
const res = await ApiFetch.api.landingpage.mediasosial["create"].post(
formData
);
if (res.status === 200) {
mediaSosial.findMany.load();
return toast.success("success create");
}
console.log(res);
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
} finally {
mediaSosial.create.loading = false;
}
},
},
findMany: {
data: null as
| Prisma.MediaSosialGetPayload<{ include: { image: true } }>[]
| null,
async load() {
const res = await ApiFetch.api.landingpage.mediasosial["find-many"].get();
if (res.status === 200) {
mediaSosial.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.MediaSosialGetPayload<{
include: {
image: true;
};
}> | null,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
mediaSosial.update.loading = true;
try {
const res = await fetch(`/api/landingpage/mediasosial/${id}`);
if (res.ok) {
const data = await res.json();
mediaSosial.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch media sosial:", res.statusText);
mediaSosial.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching media sosial:", error);
mediaSosial.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
mediaSosial.delete.loading = true;
const response = await fetch(`/api/landingpage/mediasosial/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Media Sosial berhasil dihapus");
await mediaSosial.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus media sosial");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus media sosial");
} finally {
mediaSosial.delete.loading = false;
}
},
},
update: {
id: "",
form: {} as MediaSosialForm,
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
mediaSosial.update.loading = true; // ✅ Tambahkan ini di awal
try {
const response = await fetch(`/api/landingpage/mediasosial/${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 || "",
imageId: data.imageId || "",
iconUrl: data.iconUrl || "",
};
return data;
} else {
throw new Error(result?.message || "Gagal mengambil data media sosial");
}
} catch (error) {
console.error((error as Error).message);
toast.error("Terjadi kesalahan saat mengambil data media sosial");
} finally {
mediaSosial.update.loading = false; // ✅ Supaya berhenti loading walau error
}
},
async update() {
const cek = templateMediaSosial.safeParse(mediaSosial.update.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
mediaSosial.update.loading = true;
const response = await fetch(`/api/landingpage/mediasosial/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
imageId: this.form.imageId,
iconUrl: this.form.iconUrl,
}),
});
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 media sosial");
await mediaSosial.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal update media sosial");
}
} catch (error) {
console.error("Error updating media sosial:", error);
toast.error(
error instanceof Error
? error.message
: "Terjadi kesalahan saat update media sosial"
);
return false;
} finally {
mediaSosial.update.loading = false;
}
},
},
});
const profileLandingPageState = proxy({
programInovasi,
pejabatDesa,
mediaSosial,
});
export default profileLandingPageState;

View File

@@ -0,0 +1,236 @@
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 templatesdgsDesaForm = z.object({
name: z.string().min(1, "Judul minimal 1 karakter"),
jumlah: z.string().min(1, "Deskripsi minimal 1 karakter"),
imageId: z.string().min(1, "File minimal 1"),
});
const defaultsdgsDesaForm = {
name: "",
jumlah: "",
imageId: "",
};
const sdgsDesa = proxy({
create: {
form: { ...defaultsdgsDesaForm },
loading: false,
async create() {
const cek = templatesdgsDesaForm.safeParse(
sdgsDesa.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
sdgsDesa.create.loading = true;
const res = await ApiFetch.api.landingpage.sdgsdesa[
"create"
].post({
...sdgsDesa.create.form,
});
if (res.status === 200) {
sdgsDesa.findMany.load();
return toast.success("Data berhasil ditambahkan");
}
return toast.error("Gagal menambahkan data");
} catch (error) {
console.log(error);
toast.error("Gagal menambahkan data");
} finally {
sdgsDesa.create.loading = false;
}
},
},
findMany: {
data: null as Array<
Prisma.SDGSDesaGetPayload<{
include: {
image: true;
};
}>
> | null,
async load() {
const res = await ApiFetch.api.landingpage.sdgsdesa[
"find-many"
].get();
if (res.status === 200) {
sdgsDesa.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.SDGSDesaGetPayload<{
include: {
image: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/landingpage/sdgsdesa/${id}`);
if (res.ok) {
const data = await res.json();
sdgsDesa.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
sdgsDesa.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
sdgsDesa.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
sdgsDesa.delete.loading = true;
const response = await fetch(
`/api/landingpage/sdgsdesa/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "sdgs desa berhasil dihapus");
await sdgsDesa.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus sdgs desa");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus sdgs desa");
} finally {
sdgsDesa.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...defaultsdgsDesaForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
sdgsDesa.edit.loading = true;
const response = await fetch(`/api/landingpage/sdgsdesa/${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,
jumlah: data.jumlah,
imageId: data.imageId,
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading sdgs desa:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
} finally {
sdgsDesa.edit.loading = false;
}
},
async update() {
const cek = templatesdgsDesaForm.safeParse(
sdgsDesa.edit.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
sdgsDesa.edit.loading = true;
const response = await fetch(
`/api/landingpage/sdgsdesa/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
jumlah: this.form.jumlah,
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 sdgs desa");
await sdgsDesa.findMany.load(); // refresh list
return true;
} else {
throw new Error(
result.message || "Gagal mengupdate sdgs desa"
);
}
} catch (error) {
console.error("Error updating sdgs desa:", error);
toast.error(
error instanceof Error
? error.message
: "Gagal mengupdate sdgs desa"
);
return false;
} finally {
sdgsDesa.edit.loading = false;
}
},
reset() {
sdgsDesa.edit.id = "";
sdgsDesa.edit.form = { ...defaultsdgsDesaForm };
},
},
});
export default sdgsDesa;

View File

@@ -0,0 +1,492 @@
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 templateKegiatanDesaForm = z.object({
judul: z.string().min(1, "Judul minimal 1 karakter"),
deskripsiSingkat: z.string().min(1, "Deskripsi singkat minimal 1 karakter"),
deskripsiLengkap: z.string().min(1, "Deskripsi lengkap minimal 1 karakter"),
tanggal: z.date(),
lokasi: z.string().min(1, "Lokasi minimal 1 karakter"),
partisipan: z.number().min(1, "Partisipan minimal 1"),
imageId: z.string().min(1, "Gambar wajib dipilih"),
kategoriKegiatanId: z.string().min(1, "Kategori kegiatan minimal 1"),
});
const defaultKegiatanDesaForm = {
judul: "",
deskripsiSingkat: "",
deskripsiLengkap: "",
tanggal: new Date(),
lokasi: "",
partisipan: 0,
imageId: "",
kategoriKegiatanId: "",
};
const kegiatanDesa = proxy({
create: {
form: { ...defaultKegiatanDesaForm },
loading: false,
async create() {
const cek = templateKegiatanDesaForm.safeParse(kegiatanDesa.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
kegiatanDesa.create.loading = true;
const res = await ApiFetch.api.lingkungan.kegiatandesa["create"].post({
...kegiatanDesa.create.form,
tanggal: kegiatanDesa.create.form.tanggal.toISOString(), // ✅ convert Date -> string
});
if (res.status === 200) {
kegiatanDesa.findMany.load();
return toast.success("Data berhasil ditambahkan");
}
return toast.error("Gagal menambahkan data");
} catch (error) {
console.log(error);
toast.error("Gagal menambahkan data");
} finally {
kegiatanDesa.create.loading = false;
}
},
},
findMany: {
data: null as Array<
Prisma.KegiatanDesaGetPayload<{
include: {
image: true;
kategoriKegiatan: true;
};
}>
> | null,
async load() {
const res = await ApiFetch.api.lingkungan.kegiatandesa["find-many"].get();
if (res.status === 200) {
kegiatanDesa.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.KegiatanDesaGetPayload<{
include: {
image: true;
kategoriKegiatan: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/lingkungan/kegiatandesa/${id}`);
if (res.ok) {
const data = await res.json();
kegiatanDesa.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
kegiatanDesa.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
kegiatanDesa.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
kegiatanDesa.delete.loading = true;
const response = await fetch(`/api/lingkungan/kegiatandesa/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "kegiatan desa berhasil dihapus");
await kegiatanDesa.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus pasar desa");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus pasar desa");
} finally {
kegiatanDesa.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...defaultKegiatanDesaForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
kegiatanDesa.edit.loading = true;
const response = await fetch(`/api/lingkungan/kegiatandesa/${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 = {
judul: data.judul,
deskripsiSingkat: data.deskripsiSingkat,
deskripsiLengkap: data.deskripsiLengkap,
tanggal: data.tanggal,
lokasi: data.lokasi,
partisipan: data.partisipan,
imageId: data.imageId,
kategoriKegiatanId: data.kategoriKegiatanId,
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading kegiatan desa:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
} finally {
kegiatanDesa.edit.loading = false;
}
},
async update() {
const cek = templateKegiatanDesaForm.safeParse(kegiatanDesa.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
kegiatanDesa.edit.loading = true;
const response = await fetch(
`/api/lingkungan/kegiatandesa/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
judul: this.form.judul,
deskripsiSingkat: this.form.deskripsiSingkat,
deskripsiLengkap: this.form.deskripsiLengkap,
tanggal:
typeof this.form.tanggal === "string"
? this.form.tanggal
: this.form.tanggal.toISOString(),
lokasi: this.form.lokasi,
partisipan: this.form.partisipan,
imageId: this.form.imageId,
kategoriKegiatanId: this.form.kategoriKegiatanId,
}),
}
);
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 kegiatan desa");
await kegiatanDesa.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal mengupdate kegiatan desa");
}
} catch (error) {
console.error("Error updating kegiatan desa:", error);
toast.error(
error instanceof Error
? error.message
: "Gagal mengupdate kegiatan desa"
);
return false;
} finally {
kegiatanDesa.edit.loading = false;
}
},
reset() {
kegiatanDesa.edit.id = "";
kegiatanDesa.edit.form = { ...defaultKegiatanDesaForm };
},
},
});
// ========================================= KATEGORI kegiatan ========================================= //
const kategoriKegiatanForm = z.object({
nama: z.string().min(1, "Nama minimal 1 karakter"),
});
const kategoriKegiatanDefaultForm = {
nama: "",
};
const kategoriKegiatan = proxy({
create: {
form: { ...kategoriKegiatanDefaultForm },
loading: false,
async create() {
const cek = kategoriKegiatanForm.safeParse(kategoriKegiatan.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
kategoriKegiatan.create.loading = true;
const res = await ApiFetch.api.lingkungan.kategorikegiatan[
"create"
].post(kategoriKegiatan.create.form);
if (res.status === 200) {
kategoriKegiatan.findMany.load();
return toast.success("Data berhasil ditambahkan");
}
return toast.error("Gagal menambahkan data");
} catch (error) {
console.log(error);
toast.error("Gagal menambahkan data");
} finally {
kategoriKegiatan.create.loading = false;
}
},
},
findMany: {
data: null as Array<{
id: string;
nama: string;
}> | null,
async load() {
const res = await ApiFetch.api.lingkungan.kategorikegiatan[
"find-many"
].get();
if (res.status === 200) {
kategoriKegiatan.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.KategoriKegiatanGetPayload<{
omit: { isActive: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(
`/api/lingkungan/kategorikegiatan/${id}`
);
if (res.ok) {
const data = await res.json();
kategoriKegiatan.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
kategoriKegiatan.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
kategoriKegiatan.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
kategoriKegiatan.delete.loading = true;
const response = await fetch(
`/api/lingkungan/kategorikegiatan/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Kategori kegiatan berhasil dihapus");
await kategoriKegiatan.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus kategori kegiatan");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus kategori kegiatan");
} finally {
kategoriKegiatan.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...kategoriKegiatanDefaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(
`/api/lingkungan/kategorikegiatan/${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 = {
nama: data.nama,
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading kategori kegiatan:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = kategoriKegiatanForm.safeParse(kategoriKegiatan.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
kategoriKegiatan.edit.loading = true;
const response = await fetch(
`/api/lingkungan/kategorikegiatan/${kategoriKegiatan.edit.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
nama: kategoriKegiatan.edit.form.nama,
}),
}
);
// Clone the response to avoid 'body already read' error
const responseClone = response.clone();
try {
const result = await response.json();
if (!response.ok) {
console.error(
"Update failed with status:",
response.status,
"Response:",
result
);
throw new Error(
result?.message ||
`Gagal mengupdate kategori kegiatan (${response.status})`
);
}
if (result.success) {
toast.success(
result.message || "Berhasil memperbarui kategori kegiatan"
);
await kategoriKegiatan.findMany.load(); // refresh list
return true;
} else {
throw new Error(
result.message || "Gagal mengupdate kategori kegiatan"
);
}
} catch (error) {
// If JSON parsing fails, try to get the response text for better error messages
try {
const text = await responseClone.text();
console.error("Error response text:", text);
throw new Error(`Gagal memproses respons dari server: ${text}`);
} catch (textError) {
console.error("Error parsing response as text:", textError);
console.error("Original error:", error);
throw new Error("Gagal memproses respons dari server");
}
}
} catch (error) {
console.error("Error updating kategori kegiatan:", error);
toast.error(
error instanceof Error
? error.message
: "Gagal mengupdate kategori kegiatan"
);
return false;
} finally {
kategoriKegiatan.edit.loading = false;
}
},
reset() {
kategoriKegiatan.edit.id = "";
kategoriKegiatan.edit.form = { ...kategoriKegiatanDefaultForm };
},
},
});
const gotongRoyongState = proxy({
kegiatanDesa,
kategoriKegiatan,
});
export default gotongRoyongState;

View File

@@ -0,0 +1,282 @@
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 templateBeasiswaPendaftar = z.object({
namaLengkap: z.string().min(1, "Nama harus diisi"),
nik: z.string().min(1, "NIK harus diisi"),
tempatLahir: z.string().min(1, "Tempat lahir harus diisi"),
tanggalLahir: z.string().min(1, "Tanggal lahir harus diisi"),
jenisKelamin: z.string().min(1, "Jenis kelamin harus diisi"),
kewarganegaraan: z.string().min(1, "Kewarganegaraan harus diisi"),
agama: z.string().min(1, "Agama harus diisi"),
alamatKTP: z.string().min(1, "Alamat KTP harus diisi"),
alamatDomisili: z.string().min(1, "Alamat domisili harus diisi"),
noHp: z.string().min(1, "No HP harus diisi"),
email: z.string().min(1, "Email harus diisi"),
statusPernikahan: z.string().min(1, "Status pernikahan harus diisi"),
ukuranBaju: z.string().min(1, "Ukuran baju harus diisi"),
});
const defaultBeasiswaPendaftar = {
namaLengkap: "",
nik: "",
tempatLahir: "",
tanggalLahir: "",
jenisKelamin: "",
kewarganegaraan: "",
agama: "",
alamatKTP: "",
alamatDomisili: "",
noHp: "",
email: "",
statusPernikahan: "",
ukuranBaju: "",
};
const beasiswaPendaftar = proxy({
create: {
form: { ...defaultBeasiswaPendaftar },
loading: false,
async create() {
const cek = templateBeasiswaPendaftar.safeParse(
beasiswaPendaftar.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
beasiswaPendaftar.create.loading = true;
const res = await ApiFetch.api.pendidikan.beasiswa.beasiswapendaftar[
"create"
].post(beasiswaPendaftar.create.form);
if (res.status === 200) {
beasiswaPendaftar.findMany.load();
return toast.success("Data Berhasil Dibuat, Silahkan Menunggu Konfirmasi dari Admin di WhatsApp");
}
console.log(res);
return toast.error("failed create");
} catch (error) {
console.log(error);
return toast.error("failed create");
} finally {
beasiswaPendaftar.create.loading = false;
}
},
},
findMany: {
data: [] as Prisma.BeasiswaPendaftarGetPayload<{
omit: {
isActive: true;
};
}>[],
loading: false,
async load() {
const res = await ApiFetch.api.pendidikan.beasiswa.beasiswapendaftar[
"findMany"
].get();
if (res.status === 200) {
beasiswaPendaftar.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.BeasiswaPendaftarGetPayload<{
omit: {
isActive: true;
};
}> | null,
loading: false,
async load(id: string) {
try {
const res = await fetch(
`/api/pendidikan/beasiswa/beasiswapendaftar/${id}`
);
if (res.ok) {
const data = await res.json();
beasiswaPendaftar.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
beasiswaPendaftar.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
beasiswaPendaftar.findUnique.data = null;
}
},
},
delete: {
loading: false,
async delete(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
beasiswaPendaftar.delete.loading = true;
const response = await fetch(
`/api/pendidikan/beasiswa/beasiswapendaftar/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Beasiswa berhasil dihapus");
await beasiswaPendaftar.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus beasiswa");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus beasiswa");
} finally {
beasiswaPendaftar.delete.loading = false;
}
},
},
update: {
id: "",
form: { ...defaultBeasiswaPendaftar },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(
`/api/pendidikan/beasiswa/beasiswapendaftar/${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 = {
namaLengkap: data.namaLengkap,
nik: data.nik,
tempatLahir: data.tempatLahir,
tanggalLahir: data.tanggalLahir,
jenisKelamin: data.jenisKelamin,
kewarganegaraan: data.kewarganegaraan,
agama: data.agama,
alamatKTP: data.alamatKTP,
alamatDomisili: data.alamatDomisili,
noHp: data.noHp,
email: data.email,
statusPernikahan: data.statusPernikahan,
ukuranBaju: data.ukuranBaju,
};
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading beasiswa pendaftar:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateBeasiswaPendaftar.safeParse(
beasiswaPendaftar.update.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
beasiswaPendaftar.update.loading = true;
const response = await fetch(
`/api/pendidikan/beasiswa/beasiswapendaftar/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
namaLengkap: this.form.namaLengkap,
nik: this.form.nik,
tanggalLahir: this.form.tanggalLahir,
jenisKelamin: this.form.jenisKelamin,
kewarganegaraan: this.form.kewarganegaraan,
agama: this.form.agama,
alamatKTP: this.form.alamatKTP,
alamatDomisili: this.form.alamatDomisili,
noHp: this.form.noHp,
email: this.form.email,
statusPernikahan: this.form.statusPernikahan,
ukuranBaju: this.form.ukuranBaju,
}),
}
);
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 beasiswa pendaftar");
await beasiswaPendaftar.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal update beasiswa pendaftar");
}
} catch (error) {
console.error("Error updating beasiswa pendaftar:", error);
toast.error(
error instanceof Error
? error.message
: "Terjadi kesalahan saat update beasiswa pendaftar"
);
return false;
} finally {
beasiswaPendaftar.update.loading = false;
}
},
reset() {
beasiswaPendaftar.update.id = "";
beasiswaPendaftar.update.form = { ...defaultBeasiswaPendaftar };
},
},
});
const beasiswaDesaState = proxy({
beasiswaPendaftar,
});
export default beasiswaDesaState;

View File

@@ -0,0 +1,260 @@
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
// ========================================= TUJUAN PROGRAM ========================================= //
const templateTujuanProgramForm = z.object({
judul: z.string().min(3, "Judul minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
});
type TujuanProgramForm = Prisma.TujuanBimbinganBelajarDesaGetPayload<{
select: {
id: true;
judul: true;
deskripsi: true;
};
}>;
const stateTujuanProgram = proxy({
findById: {
data: null as TujuanProgramForm | null,
loading: false,
initialize() {
stateTujuanProgram.findById.data = {
id: "",
judul: "",
deskripsi: "",
} as TujuanProgramForm;
},
async load(id: string) {
try {
stateTujuanProgram.findById.loading = true;
const res =
await ApiFetch.api.pendidikan.bimbinganbelajardesa.tujuanprogram[
"find-by-id"
].get({
query: { id },
});
if (res.status === 200) {
stateTujuanProgram.findById.data = res.data?.data ?? null;
} else {
toast.error("Gagal mengambil data tujuan program");
}
} catch (error) {
console.error((error as Error).message);
toast.error("Terjadi kesalahan saat mengambil data tujuan program");
} finally {
stateTujuanProgram.findById.loading = false;
}
},
},
update: {
loading: false,
async save(data: TujuanProgramForm) {
const cek = templateTujuanProgramForm.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 {
stateTujuanProgram.update.loading = true;
const res =
await ApiFetch.api.pendidikan.bimbinganbelajardesa.tujuanprogram[
"update"
].post(data);
if (res.status === 200) {
toast.success("Data tujuan program berhasil diubah");
await stateTujuanProgram.findById.load(data.id);
} else {
toast.error("Gagal mengubah data tujuan program");
}
} catch (error) {
console.error((error as Error).message);
toast.error("Terjadi kesalahan saat mengubah data tujuan program");
} finally {
stateTujuanProgram.update.loading = false;
}
},
},
});
// ========================================= LOKASI DAN JADWAL ========================================= //
const templateLokasiDanJadwalForm = z.object({
judul: z.string().min(3, "Judul minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
});
type LokasiDanJadwalForm = Prisma.LokasiJadwalBimbinganBelajarDesaGetPayload<{
select: {
id: true;
judul: true;
deskripsi: true;
};
}>;
const lokasiDanJadwalState = proxy({
findById: {
data: null as LokasiDanJadwalForm | null,
loading: false,
initialize() {
lokasiDanJadwalState.findById.data = {
id: "",
judul: "",
deskripsi: "",
} as LokasiDanJadwalForm;
},
async load(id: string) {
try {
lokasiDanJadwalState.findById.loading = true;
const res =
await ApiFetch.api.pendidikan.bimbinganbelajardesa.lokasidanjadwal[
"find-by-id"
].get({
query: { id },
});
if (res.status === 200) {
lokasiDanJadwalState.findById.data = res.data?.data ?? null;
} else {
toast.error("Gagal mengambil data lokasi dan jadwal");
}
} catch (error) {
console.error((error as Error).message);
toast.error("Terjadi kesalahan saat mengambil data lokasi dan jadwal");
} finally {
lokasiDanJadwalState.findById.loading = false;
}
},
},
update: {
loading: false,
async save(data: LokasiDanJadwalForm) {
const cek = templateLokasiDanJadwalForm.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 {
lokasiDanJadwalState.update.loading = true;
const res =
await ApiFetch.api.pendidikan.bimbinganbelajardesa.lokasidanjadwal[
"update"
].post(data);
if (res.status === 200) {
toast.success("Data lokasi dan jadwal berhasil diubah");
await lokasiDanJadwalState.findById.load(data.id);
} else {
toast.error("Gagal mengubah data lokasi dan jadwal");
}
} catch (error) {
console.error((error as Error).message);
toast.error("Terjadi kesalahan saat mengubah data lokasi dan jadwal");
} finally {
lokasiDanJadwalState.update.loading = false;
}
},
},
});
// ========================================= FASILITAS YANG DISEDIAKAN ========================================= //
const templateFasilitasYangDisediakanForm = z.object({
judul: z.string().min(3, "Judul minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
});
type FasilitasYangDisediakanForm = Prisma.FasilitasBimbinganBelajarDesaGetPayload<{
select: {
id: true;
judul: true;
deskripsi: true;
};
}>;
const fasilitasYangDisediakanState = proxy({
findById: {
data: null as FasilitasYangDisediakanForm | null,
loading: false,
initialize() {
fasilitasYangDisediakanState.findById.data = {
id: "",
judul: "",
deskripsi: "",
} as FasilitasYangDisediakanForm;
},
async load(id: string) {
try {
fasilitasYangDisediakanState.findById.loading = true;
const res =
await ApiFetch.api.pendidikan.bimbinganbelajardesa.fasilitasyangdisediakan[
"find-by-id"
].get({
query: { id },
});
if (res.status === 200) {
fasilitasYangDisediakanState.findById.data = res.data?.data ?? null;
} else {
toast.error("Gagal mengambil data fasilitas yang disediakan");
}
} catch (error) {
console.error((error as Error).message);
toast.error("Terjadi kesalahan saat mengambil data fasilitas yang disediakan");
} finally {
fasilitasYangDisediakanState.findById.loading = false;
}
},
},
update: {
loading: false,
async save(data: FasilitasYangDisediakanForm) {
const cek = templateFasilitasYangDisediakanForm.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 {
fasilitasYangDisediakanState.update.loading = true;
const res =
await ApiFetch.api.pendidikan.bimbinganbelajardesa.fasilitasyangdisediakan[
"update"
].post(data);
if (res.status === 200) {
toast.success("Data fasilitas yang disediakan berhasil diubah");
await fasilitasYangDisediakanState.findById.load(data.id);
} else {
toast.error("Gagal mengubah data fasilitas yang disediakan");
}
} catch (error) {
console.error((error as Error).message);
toast.error("Terjadi kesalahan saat mengubah data fasilitas yang disediakan");
} finally {
fasilitasYangDisediakanState.update.loading = false;
}
},
},
});
const stateBimbinganBelajarDesa = proxy({
stateTujuanProgram,
lokasiDanJadwalState,
fasilitasYangDisediakanState,
});
export default stateBimbinganBelajarDesa;

View File

@@ -0,0 +1,178 @@
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 templateDataPendidikan = z.object({
name: z.string().min(1, "Data nama harus diisi"),
jumlah: z.string().min(1, "Data jumlah harus diisi"),
});
type DataPendidikan = Prisma.DataPendidikanGetPayload<{
select: {
id: true;
name: true;
jumlah: true;
};
}>;
const defaultForm: Omit<DataPendidikan, "id"> & { id?: string } = {
name: "",
jumlah: "",
};
const dataPendidikan = proxy({
create: {
form: defaultForm,
loading: false,
async create() {
const cek = templateDataPendidikan.safeParse(dataPendidikan.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
dataPendidikan.create.loading = true;
const res = await ApiFetch.api.pendidikan.datapendidikan["create"].post(
dataPendidikan.create.form
);
if (res.status === 200) {
const id = res.data?.data?.id;
if (id) {
toast.success("Success create");
dataPendidikan.create.form = {
name: "",
jumlah: "",
};
dataPendidikan.findMany.load();
return id;
}
}
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
} finally {
dataPendidikan.create.loading = false;
}
},
},
findMany: {
data: null as
| Prisma.DataPendidikanGetPayload<{
select: { id: true; name: true; jumlah: true };
}>[]
| null,
loading: false,
async load() {
const res = await ApiFetch.api.pendidikan.datapendidikan[
"findMany"
].get();
if (res.status === 200) {
dataPendidikan.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.DataPendidikanGetPayload<{
select: { id: true; name: true; jumlah: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/pendidikan/datapendidikan/${id}`);
if (res.ok) {
const data = await res.json();
dataPendidikan.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
dataPendidikan.findUnique.data = null;
}
} catch (error) {
console.error("Error loading data pendidikan:", error);
dataPendidikan.findUnique.data = null;
}
},
},
update: {
id: "",
form: { ...defaultForm },
loading: false,
async byId() {
// Method implementation if needed
},
async submit() {
const id = this.id;
if (!id) {
toast.warn("ID tidak valid");
return null;
}
const cek = templateDataPendidikan.safeParse(this.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => (v.path as string[]).join("."))
.join("\n")}] required`;
toast.error(err);
return null;
}
this.loading = true;
try {
const response = await fetch(`/api/pendidikan/datapendidikan/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
});
const result = await response.json();
if (!response.ok || !result?.success) {
throw new Error(result?.message || "Gagal update data");
}
toast.success("Berhasil update data!");
await dataPendidikan.findMany.load();
return result.data;
} catch (error) {
console.error("Error update data data pendidikan:", error);
toast.error("Gagal update data data pendidikan");
} finally {
this.loading = false;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
dataPendidikan.delete.loading = true;
const response = await fetch(
`/api/pendidikan/datapendidikan/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Data berhasil dihapus");
await dataPendidikan.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus data");
}
} catch (error) {
console.error("Gagal delete data pendidikan:", error);
toast.error("Terjadi kesalahan saat menghapus data pendidikan");
} finally {
dataPendidikan.delete.loading = false;
}
},
},
});
export default dataPendidikan;

View File

@@ -0,0 +1,998 @@
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
// ========================================= JENJANG PENDIDIKAN ========================================= //
const jenjangPendidikanForm = z.object({
nama: z.string().min(1, "Nama minimal 1 karakter"),
});
const jenjangPendidikanDefaultForm = {
nama: "",
};
const jenjangPendidikan = proxy({
create: {
form: { ...jenjangPendidikanDefaultForm },
loading: false,
async create() {
const cek = jenjangPendidikanForm.safeParse(
jenjangPendidikan.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
jenjangPendidikan.create.loading = true;
const res =
await ApiFetch.api.pendidikan.infosekolahpaud.jenjangpendidikan[
"create"
].post(jenjangPendidikan.create.form);
if (res.status === 200) {
jenjangPendidikan.findMany.load();
return toast.success("Data berhasil ditambahkan");
}
return toast.error("Gagal menambahkan data");
} catch (error) {
console.log(error);
toast.error("Gagal menambahkan data");
} finally {
jenjangPendidikan.create.loading = false;
}
},
},
findMany: {
data: null as Array<{
id: string;
nama: string;
}> | null,
async load() {
const res =
await ApiFetch.api.pendidikan.infosekolahpaud.jenjangpendidikan[
"find-many"
].get();
if (res.status === 200) {
jenjangPendidikan.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.JenjangPendidikanGetPayload<{
omit: { isActive: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(
`/api/pendidikan/infosekolahpaud/jenjangpendidikan/${id}`
);
if (res.ok) {
const data = await res.json();
jenjangPendidikan.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
jenjangPendidikan.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
jenjangPendidikan.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
jenjangPendidikan.delete.loading = true;
const response = await fetch(
`/api/pendidikan/infosekolahpaud/jenjangpendidikan/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(
result.message || "jenjang pendidikan berhasil dihapus"
);
await jenjangPendidikan.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus jenjang pendidikan");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus jenjang pendidikan");
} finally {
jenjangPendidikan.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...jenjangPendidikanDefaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(
`/api/pendidikan/infosekolahpaud/jenjangpendidikan/${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 = {
nama: data.nama,
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading jenjang pendidikan:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = jenjangPendidikanForm.safeParse(jenjangPendidikan.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
jenjangPendidikan.edit.loading = true;
const response = await fetch(
`/api/pendidikan/infosekolahpaud/jenjangpendidikan/${jenjangPendidikan.edit.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
nama: jenjangPendidikan.edit.form.nama,
}),
}
);
// Clone the response to avoid 'body already read' error
const responseClone = response.clone();
try {
const result = await response.json();
if (!response.ok) {
console.error(
"Update failed with status:",
response.status,
"Response:",
result
);
throw new Error(
result?.message ||
`Gagal mengupdate jenjang pendidikan (${response.status})`
);
}
if (result.success) {
toast.success(
result.message || "Berhasil memperbarui jenjang pendidikan"
);
await jenjangPendidikan.findMany.load(); // refresh list
return true;
} else {
throw new Error(
result.message || "Gagal mengupdate jenjang pendidikan"
);
}
} catch (error) {
// If JSON parsing fails, try to get the response text for better error messages
try {
const text = await responseClone.text();
console.error("Error response text:", text);
throw new Error(`Gagal memproses respons dari server: ${text}`);
} catch (textError) {
console.error("Error parsing response as text:", textError);
console.error("Original error:", error);
throw new Error("Gagal memproses respons dari server");
}
}
} catch (error) {
console.error("Error updating jenjang pendidikan:", error);
toast.error(
error instanceof Error
? error.message
: "Gagal mengupdate jenjang pendidikan"
);
return false;
} finally {
jenjangPendidikan.edit.loading = false;
}
},
reset() {
jenjangPendidikan.edit.id = "";
jenjangPendidikan.edit.form = { ...jenjangPendidikanDefaultForm };
},
},
});
// ========================================= LEMBAGA PENDIDIKAN ========================================= //
const lembagaPendidikanForm = z.object({
nama: z.string().min(1, "Nama minimal 1 karakter"),
jenjangId: z.string().min(1, "Jenjang pendidikan minimal 1"),
});
const lembagaPendidikanDefaultForm = {
nama: "",
jenjangId: "",
};
const lembagaPendidikan = proxy({
create: {
form: { ...lembagaPendidikanDefaultForm },
loading: false,
async create() {
const cek = lembagaPendidikanForm.safeParse(
lembagaPendidikan.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
lembagaPendidikan.create.loading = true;
const res =
await ApiFetch.api.pendidikan.infosekolahpaud.lembagapendidikan[
"create"
].post(lembagaPendidikan.create.form);
if (res.status === 200) {
lembagaPendidikan.findMany.load();
return toast.success("Data berhasil ditambahkan");
}
return toast.error("Gagal menambahkan data");
} catch (error) {
console.log(error);
toast.error("Gagal menambahkan data");
} finally {
lembagaPendidikan.create.loading = false;
}
},
},
findMany: {
data: null as Array<
Prisma.LembagaGetPayload<{
include: {
jenjangPendidikan: true;
siswa: true;
pengajar: true;
};
}>
> | null,
async load() {
const res =
await ApiFetch.api.pendidikan.infosekolahpaud.lembagapendidikan[
"find-many"
].get();
if (res.status === 200) {
lembagaPendidikan.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.LembagaGetPayload<{
include: {
jenjangPendidikan: true;
siswa: true;
pengajar: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(
`/api/pendidikan/infosekolahpaud/lembagapendidikan/${id}`
);
if (res.ok) {
const data = await res.json();
lembagaPendidikan.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
lembagaPendidikan.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
lembagaPendidikan.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
lembagaPendidikan.delete.loading = true;
const response = await fetch(
`/api/pendidikan/infosekolahpaud/lembagapendidikan/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(
result.message || "lembaga pendidikan berhasil dihapus"
);
await lembagaPendidikan.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus lembaga pendidikan");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus lembaga pendidikan");
} finally {
lembagaPendidikan.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...lembagaPendidikanDefaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(
`/api/pendidikan/infosekolahpaud/lembagapendidikan/${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 = {
nama: data.nama,
jenjangId: data.jenjangId,
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading lembaga pendidikan:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = lembagaPendidikanForm.safeParse(lembagaPendidikan.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
lembagaPendidikan.edit.loading = true;
const response = await fetch(
`/api/pendidikan/infosekolahpaud/lembagapendidikan/${lembagaPendidikan.edit.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
nama: lembagaPendidikan.edit.form.nama,
jenjangId: lembagaPendidikan.edit.form.jenjangId,
}),
}
);
// Clone the response to avoid 'body already read' error
const responseClone = response.clone();
try {
const result = await response.json();
if (!response.ok) {
console.error(
"Update failed with status:",
response.status,
"Response:",
result
);
throw new Error(
result?.message ||
`Gagal mengupdate lembaga pendidikan (${response.status})`
);
}
if (result.success) {
toast.success(
result.message || "Berhasil memperbarui lembaga pendidikan"
);
await lembagaPendidikan.findMany.load(); // refresh list
return true;
} else {
throw new Error(
result.message || "Gagal mengupdate lembaga pendidikan"
);
}
} catch (error) {
// If JSON parsing fails, try to get the response text for better error messages
try {
const text = await responseClone.text();
console.error("Error response text:", text);
throw new Error(`Gagal memproses respons dari server: ${text}`);
} catch (textError) {
console.error("Error parsing response as text:", textError);
console.error("Original error:", error);
throw new Error("Gagal memproses respons dari server");
}
}
} catch (error) {
console.error("Error updating lembaga pendidikan:", error);
toast.error(
error instanceof Error
? error.message
: "Gagal mengupdate lembaga pendidikan"
);
return false;
} finally {
lembagaPendidikan.edit.loading = false;
}
},
reset() {
lembagaPendidikan.edit.id = "";
lembagaPendidikan.edit.form = { ...lembagaPendidikanDefaultForm };
},
},
});
// ========================================= SISWA ========================================= //
const siswaForm = z.object({
nama: z.string().min(1, "Nama minimal 1 karakter"),
lembagaId: z.string().min(1, "lembaga pendidikan minimal 1"),
});
const siswaDefaultForm = {
nama: "",
lembagaId: "",
};
const siswa = proxy({
create: {
form: { ...siswaDefaultForm },
loading: false,
async create() {
const cek = siswaForm.safeParse(siswa.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
siswa.create.loading = true;
const res = await ApiFetch.api.pendidikan.infosekolahpaud.siswa[
"create"
].post(siswa.create.form);
if (res.status === 200) {
siswa.findMany.load();
return toast.success("Data berhasil ditambahkan");
}
return toast.error("Gagal menambahkan data");
} catch (error) {
console.log(error);
toast.error("Gagal menambahkan data");
} finally {
siswa.create.loading = false;
}
},
},
findMany: {
data: null as Array<
Prisma.SiswaGetPayload<{
include: {
lembaga: true;
};
}>
> | null,
async load() {
const res = await ApiFetch.api.pendidikan.infosekolahpaud.siswa[
"find-many"
].get();
if (res.status === 200) {
siswa.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.SiswaGetPayload<{
include: {
lembaga: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/pendidikan/infosekolahpaud/siswa/${id}`);
if (res.ok) {
const data = await res.json();
siswa.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
siswa.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
siswa.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
siswa.delete.loading = true;
const response = await fetch(
`/api/pendidikan/infosekolahpaud/siswa/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "siswa berhasil dihapus");
await siswa.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus siswa");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus siswa");
} finally {
siswa.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...siswaDefaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(
`/api/pendidikan/infosekolahpaud/siswa/${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 = {
nama: data.nama,
lembagaId: data.lembagaId,
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading siswa:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = siswaForm.safeParse(siswa.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
siswa.edit.loading = true;
const response = await fetch(
`/api/pendidikan/infosekolahpaud/siswa/${siswa.edit.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
nama: siswa.edit.form.nama,
lembagaId: siswa.edit.form.lembagaId,
}),
}
);
// Clone the response to avoid 'body already read' error
const responseClone = response.clone();
try {
const result = await response.json();
if (!response.ok) {
console.error(
"Update failed with status:",
response.status,
"Response:",
result
);
throw new Error(
result?.message || `Gagal mengupdate siswa (${response.status})`
);
}
if (result.success) {
toast.success(result.message || "Berhasil memperbarui siswa");
await siswa.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal mengupdate siswa");
}
} catch (error) {
// If JSON parsing fails, try to get the response text for better error messages
try {
const text = await responseClone.text();
console.error("Error response text:", text);
throw new Error(`Gagal memproses respons dari server: ${text}`);
} catch (textError) {
console.error("Error parsing response as text:", textError);
console.error("Original error:", error);
throw new Error("Gagal memproses respons dari server");
}
}
} catch (error) {
console.error("Error updating siswa:", error);
toast.error(
error instanceof Error ? error.message : "Gagal mengupdate siswa"
);
return false;
} finally {
siswa.edit.loading = false;
}
},
reset() {
siswa.edit.id = "";
siswa.edit.form = { ...siswaDefaultForm };
},
},
});
// ========================================= PENGAJAR ========================================= //
const pengajarForm = z.object({
nama: z.string().min(1, "Nama minimal 1 karakter"),
lembagaId: z.string().min(1, "lembaga pendidikan minimal 1"),
});
const pengajarDefaultForm = {
nama: "",
lembagaId: "",
};
const pengajar = proxy({
create: {
form: { ...pengajarDefaultForm },
loading: false,
async create() {
const cek = pengajarForm.safeParse(pengajar.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
pengajar.create.loading = true;
const res = await ApiFetch.api.pendidikan.infosekolahpaud.pengajar[
"create"
].post(pengajar.create.form);
if (res.status === 200) {
pengajar.findMany.load();
return toast.success("Data berhasil ditambahkan");
}
return toast.error("Gagal menambahkan data");
} catch (error) {
console.log(error);
toast.error("Gagal menambahkan data");
} finally {
pengajar.create.loading = false;
}
},
},
findMany: {
data: null as Array<
Prisma.PengajarGetPayload<{
include: {
lembaga: true;
};
}>
> | null,
async load() {
const res = await ApiFetch.api.pendidikan.infosekolahpaud.pengajar[
"find-many"
].get();
if (res.status === 200) {
pengajar.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.PengajarGetPayload<{
include: {
lembaga: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/pendidikan/infosekolahpaud/pengajar/${id}`);
if (res.ok) {
const data = await res.json();
pengajar.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
pengajar.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
pengajar.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
pengajar.delete.loading = true;
const response = await fetch(
`/api/pendidikan/infosekolahpaud/pengajar/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "pengajar berhasil dihapus");
await pengajar.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus pengajar");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus pengajar");
} finally {
pengajar.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...pengajarDefaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(
`/api/pendidikan/infosekolahpaud/pengajar/${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 = {
nama: data.nama,
lembagaId: data.lembagaId,
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading siswa:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = pengajarForm.safeParse(pengajar.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
pengajar.edit.loading = true;
const response = await fetch(
`/api/pendidikan/infosekolahpaud/pengajar/${pengajar.edit.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
nama: pengajar.edit.form.nama,
lembagaId: pengajar.edit.form.lembagaId,
}),
}
);
// Clone the response to avoid 'body already read' error
const responseClone = response.clone();
try {
const result = await response.json();
if (!response.ok) {
console.error(
"Update failed with status:",
response.status,
"Response:",
result
);
throw new Error(
result?.message || `Gagal mengupdate pengajar (${response.status})`
);
}
if (result.success) {
toast.success(result.message || "Berhasil memperbarui pengajar");
await pengajar.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal mengupdate pengajar");
}
} catch (error) {
// If JSON parsing fails, try to get the response text for better error messages
try {
const text = await responseClone.text();
console.error("Error response text:", text);
throw new Error(`Gagal memproses respons dari server: ${text}`);
} catch (textError) {
console.error("Error parsing response as text:", textError);
console.error("Original error:", error);
throw new Error("Gagal memproses respons dari server");
}
}
} catch (error) {
console.error("Error updating pengajar:", error);
toast.error(
error instanceof Error ? error.message : "Gagal mengupdate pengajar"
);
return false;
} finally {
pengajar.edit.loading = false;
}
},
reset() {
pengajar.edit.id = "";
pengajar.edit.form = { ...pengajarDefaultForm };
},
},
});
const infoSekolahPaud = proxy({
jenjangPendidikan,
lembagaPendidikan,
siswa,
pengajar,
});
export default infoSekolahPaud;

View File

@@ -0,0 +1,267 @@
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
// ========================================= TUJUAN PENDIDIKAN NON FORMAL ========================================= //
const templateTujuanPendidikanNonFormalForm = z.object({
judul: z.string().min(3, "Judul minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
});
type TujuanPendidikanNonFormalForm =
Prisma.TujuanPendidikanNonFormalGetPayload<{
select: {
id: true;
judul: true;
deskripsi: true;
};
}>;
const stateTujuanPendidikanNonFormal = proxy({
findById: {
data: null as TujuanPendidikanNonFormalForm | null,
loading: false,
initialize() {
stateTujuanPendidikanNonFormal.findById.data = {
id: "",
judul: "",
deskripsi: "",
} as TujuanPendidikanNonFormalForm;
},
async load(id: string) {
try {
stateTujuanPendidikanNonFormal.findById.loading = true;
const res =
await ApiFetch.api.pendidikan.pendidikannonformal.tujuanpendidikannonformal[
"find-by-id"
].get({
query: { id },
});
if (res.status === 200) {
stateTujuanPendidikanNonFormal.findById.data = res.data?.data ?? null;
} else {
toast.error("Gagal mengambil data tujuan pendidikan non formal");
}
} catch (error) {
console.error((error as Error).message);
toast.error(
"Terjadi kesalahan saat mengambil data tujuan pendidikan non formal"
);
} finally {
stateTujuanPendidikanNonFormal.findById.loading = false;
}
},
},
update: {
loading: false,
async save(data: TujuanPendidikanNonFormalForm) {
const cek = templateTujuanPendidikanNonFormalForm.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 {
stateTujuanPendidikanNonFormal.update.loading = true;
const res =
await ApiFetch.api.pendidikan.pendidikannonformal.tujuanpendidikannonformal[
"update"
].post(data);
if (res.status === 200) {
toast.success("Data tujuan pendidikan non formal berhasil diubah");
await stateTujuanPendidikanNonFormal.findById.load(data.id);
} else {
toast.error("Gagal mengubah data tujuan pendidikan non formal");
}
} catch (error) {
console.error((error as Error).message);
toast.error(
"Terjadi kesalahan saat mengubah data tujuan pendidikan non formal"
);
} finally {
stateTujuanPendidikanNonFormal.update.loading = false;
}
},
},
});
// ========================================= TEMPAT KEGIATAN ========================================= //
const templateTempatKegiatanForm = z.object({
judul: z.string().min(3, "Judul minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
});
type TempatKegiatanForm = Prisma.TempatKegiatanGetPayload<{
select: {
id: true;
judul: true;
deskripsi: true;
};
}>;
const stateTempatKegiatan = proxy({
findById: {
data: null as TempatKegiatanForm | null,
loading: false,
initialize() {
stateTempatKegiatan.findById.data = {
id: "",
judul: "",
deskripsi: "",
} as TempatKegiatanForm;
},
async load(id: string) {
try {
stateTempatKegiatan.findById.loading = true;
const res =
await ApiFetch.api.pendidikan.pendidikannonformal.tempatkegiatan[
"find-by-id"
].get({
query: { id },
});
if (res.status === 200) {
stateTempatKegiatan.findById.data = res.data?.data ?? null;
} else {
toast.error("Gagal mengambil data tempat kegiatan");
}
} catch (error) {
console.error((error as Error).message);
toast.error("Terjadi kesalahan saat mengambil data tempat kegiatan");
} finally {
stateTempatKegiatan.findById.loading = false;
}
},
},
update: {
loading: false,
async save(data: TempatKegiatanForm) {
const cek = templateTempatKegiatanForm.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 {
stateTempatKegiatan.update.loading = true;
const res =
await ApiFetch.api.pendidikan.pendidikannonformal.tempatkegiatan[
"update"
].post(data);
if (res.status === 200) {
toast.success("Data tempat kegiatan berhasil diubah");
await stateTempatKegiatan.findById.load(data.id);
} else {
toast.error("Gagal mengubah data tempat kegiatan");
}
} catch (error) {
console.error((error as Error).message);
toast.error("Terjadi kesalahan saat mengubah data tempat kegiatan");
} finally {
stateTempatKegiatan.update.loading = false;
}
},
},
});
// ========================================= JENIS PROGRAM YANG DISELENGGARAKAN ========================================= //
const templateJenisProgramForm = z.object({
judul: z.string().min(3, "Judul minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
});
type JenisProgramForm = Prisma.JenisProgramYangDiselenggarakanGetPayload<{
select: {
id: true;
judul: true;
deskripsi: true;
};
}>;
const stateJenisProgram = proxy({
findById: {
data: null as JenisProgramForm | null,
loading: false,
initialize() {
stateJenisProgram.findById.data = {
id: "",
judul: "",
deskripsi: "",
} as JenisProgramForm;
},
async load(id: string) {
try {
stateJenisProgram.findById.loading = true;
const res =
await ApiFetch.api.pendidikan.pendidikannonformal.jenisprogramyangdiselenggarakan[
"find-by-id"
].get({
query: { id },
});
if (res.status === 200) {
stateJenisProgram.findById.data = res.data?.data ?? null;
} else {
toast.error("Gagal mengambil data jenis program");
}
} catch (error) {
console.error((error as Error).message);
toast.error("Terjadi kesalahan saat mengambil data jenis program");
} finally {
stateJenisProgram.findById.loading = false;
}
},
},
update: {
loading: false,
async save(data: JenisProgramForm) {
const cek = templateJenisProgramForm.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 {
stateJenisProgram.update.loading = true;
const res =
await ApiFetch.api.pendidikan.pendidikannonformal.jenisprogramyangdiselenggarakan[
"update"
].post(data);
if (res.status === 200) {
toast.success("Data jenis program berhasil diubah");
await stateJenisProgram.findById.load(data.id);
} else {
toast.error("Gagal mengubah data jenis program");
}
} catch (error) {
console.error((error as Error).message);
toast.error("Terjadi kesalahan saat mengubah data jenis program");
} finally {
stateJenisProgram.update.loading = false;
}
},
},
});
const pendidikanNonFormalState = proxy({
stateTujuanPendidikanNonFormal,
stateTempatKegiatan,
stateJenisProgram,
});
export default pendidikanNonFormalState;

View File

@@ -0,0 +1,478 @@
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 templateDataPerpustakaan = z.object({
judul: z.string().min(1, "Judul harus diisi"),
deskripsi: z.string().min(1, "Deskripsi harus diisi"),
imageId: z.string().min(1, "Image ID harus diisi"),
kategoriId: z.string().min(1, "Kategori ID harus diisi"),
});
const defaultDataPerpustakaan = {
judul: "",
deskripsi: "",
imageId: "",
kategoriId: "",
};
const dataPerpustakaan = proxy({
create: {
form: { ...defaultDataPerpustakaan },
loading: false,
async create() {
const cek = templateDataPerpustakaan.safeParse(
dataPerpustakaan.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
dataPerpustakaan.create.loading = true;
const res =
await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan[
"create"
].post(dataPerpustakaan.create.form);
if (res.status === 200) {
dataPerpustakaan.findMany.load();
return toast.success("Data Data Perpustakaan Berhasil Dibuat");
}
console.log(res);
return toast.error("failed create");
} catch (error) {
console.log(error);
return toast.error("failed create");
} finally {
dataPerpustakaan.create.loading = false;
}
},
},
findMany: {
data: [] as Prisma.DataPerpustakaanGetPayload<{
include: {
kategori: true;
image: true;
};
}>[],
loading: false,
async load() {
const res =
await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan[
"findMany"
].get();
if (res.status === 200) {
dataPerpustakaan.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.DataPerpustakaanGetPayload<{
include: {
kategori: true;
image: true;
};
}> | null,
loading: false,
async load(id: string) {
try {
const res = await fetch(
`/api/pendidikan/perpustakaandigital/dataperpustakaan/${id}`
);
if (res.ok) {
const data = await res.json();
dataPerpustakaan.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
dataPerpustakaan.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
dataPerpustakaan.findUnique.data = null;
}
},
},
delete: {
loading: false,
async delete(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
dataPerpustakaan.delete.loading = true;
const response = await fetch(
`/api/pendidikan/perpustakaandigital/dataperpustakaan/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Data Perpustakaan berhasil dihapus");
await dataPerpustakaan.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus Data Perpustakaan");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus Data Perpustakaan");
} finally {
dataPerpustakaan.delete.loading = false;
}
},
},
update: {
id: "",
form: { ...defaultDataPerpustakaan },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(
`/api/pendidikan/perpustakaandigital/dataperpustakaan/${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 = {
judul: data.judul,
deskripsi: data.deskripsi,
imageId: data.imageId,
kategoriId: data.kategoriId,
};
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading perpustakaan digital:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateDataPerpustakaan.safeParse(
dataPerpustakaan.update.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
dataPerpustakaan.update.loading = true;
const response = await fetch(
`/api/pendidikan/perpustakaandigital/dataperpustakaan/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
judul: this.form.judul,
deskripsi: this.form.deskripsi,
imageId: this.form.imageId,
kategoriId: this.form.kategoriId,
}),
}
);
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 data perpustakaan digital");
await dataPerpustakaan.findMany.load(); // refresh list
return true;
} else {
throw new Error(
result.message || "Gagal update data perpustakaan digital"
);
}
} catch (error) {
console.error("Error updating data perpustakaan digital:", error);
toast.error(
error instanceof Error
? error.message
: "Terjadi kesalahan saat update data perpustakaan digital"
);
return false;
} finally {
dataPerpustakaan.update.loading = false;
}
},
reset() {
dataPerpustakaan.update.id = "";
dataPerpustakaan.update.form = { ...defaultDataPerpustakaan };
},
},
});
const templateKategoriBuku = z.object({
name: z.string().min(1, "Nama harus diisi"),
});
const defaultKategoriBuku = {
name: "",
};
const kategoriBuku = proxy({
create: {
form: { ...defaultKategoriBuku },
loading: false,
async create() {
const cek = templateKategoriBuku.safeParse(kategoriBuku.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
kategoriBuku.create.loading = true;
const res =
await ApiFetch.api.pendidikan.perpustakaandigital.kategoribuku[
"create"
].post(kategoriBuku.create.form);
if (res.status === 200) {
kategoriBuku.findMany.load();
return toast.success("Data Kategori Buku Berhasil Dibuat");
}
console.log(res);
return toast.error("failed create");
} catch (error) {
console.log(error);
return toast.error("failed create");
} finally {
kategoriBuku.create.loading = false;
}
},
},
findMany: {
data: [] as Prisma.KategoriBukuGetPayload<{
omit: {
isActive: true;
};
}>[],
loading: false,
async load() {
const res =
await ApiFetch.api.pendidikan.perpustakaandigital.kategoribuku[
"findMany"
].get();
if (res.status === 200) {
kategoriBuku.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.KategoriBukuGetPayload<{
omit: {
isActive: true;
};
}> | null,
loading: false,
async load(id: string) {
try {
const res = await fetch(
`/api/pendidikan/perpustakaandigital/kategoribuku/${id}`
);
if (res.ok) {
const data = await res.json();
kategoriBuku.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
kategoriBuku.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
kategoriBuku.findUnique.data = null;
}
},
},
delete: {
loading: false,
async delete(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
kategoriBuku.delete.loading = true;
const response = await fetch(
`/api/pendidikan/perpustakaandigital/kategoribuku/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(
result.message || "Data Kategori Buku berhasil dihapus"
);
await kategoriBuku.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus Data Kategori Buku");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus Data Kategori Buku");
} finally {
kategoriBuku.delete.loading = false;
}
},
},
update: {
id: "",
form: { ...defaultKategoriBuku },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(
`/api/pendidikan/perpustakaandigital/kategoribuku/${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,
};
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading kategori buku:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateKategoriBuku.safeParse(kategoriBuku.update.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
kategoriBuku.update.loading = true;
const response = await fetch(
`/api/pendidikan/perpustakaandigital/kategoribuku/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
}),
}
);
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 data kategori buku");
await kategoriBuku.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal update data kategori buku");
}
} catch (error) {
console.error("Error updating data kategori buku:", error);
toast.error(
error instanceof Error
? error.message
: "Terjadi kesalahan saat update data kategori buku"
);
return false;
} finally {
kategoriBuku.update.loading = false;
}
},
reset() {
kategoriBuku.update.id = "";
kategoriBuku.update.form = { ...defaultKategoriBuku };
},
},
});
const perpustakaanDigitalState = proxy({
dataPerpustakaan,
kategoriBuku,
});
export default perpustakaanDigitalState;

View File

@@ -0,0 +1,181 @@
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
// ========================================= TUJUAN PROGRAM ========================================= //
const templateTujuanProgramForm = z.object({
judul: z.string().min(3, "Judul minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
});
type TujuanProgramForm = Prisma.TujuanProgramGetPayload<{
select: {
id: true;
judul: true;
deskripsi: true;
};
}>;
const stateTujuanProgram = proxy({
findById: {
data: null as TujuanProgramForm | null,
loading: false,
initialize() {
stateTujuanProgram.findById.data = {
id: "",
judul: "",
deskripsi: "",
} as TujuanProgramForm;
},
async load(id: string) {
try {
stateTujuanProgram.findById.loading = true;
const res =
await ApiFetch.api.pendidikan.programpendidikananak.tujuanprogram[
"find-by-id"
].get({
query: { id },
});
if (res.status === 200) {
stateTujuanProgram.findById.data = res.data?.data ?? null;
} else {
toast.error("Gagal mengambil data tujuan program");
}
} catch (error) {
console.error((error as Error).message);
toast.error("Terjadi kesalahan saat mengambil data tujuan program");
} finally {
stateTujuanProgram.findById.loading = false;
}
},
},
update: {
loading: false,
async save(data: TujuanProgramForm) {
const cek = templateTujuanProgramForm.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 {
stateTujuanProgram.update.loading = true;
const res =
await ApiFetch.api.pendidikan.programpendidikananak.tujuanprogram[
"update"
].post(data);
if (res.status === 200) {
toast.success("Data tujuan program berhasil diubah");
await stateTujuanProgram.findById.load(data.id);
} else {
toast.error("Gagal mengubah data tujuan program");
}
} catch (error) {
console.error((error as Error).message);
toast.error("Terjadi kesalahan saat mengubah data tujuan program");
} finally {
stateTujuanProgram.update.loading = false;
}
},
},
});
// ========================================= PROGRAM UNGGULAN ========================================= //
const templateProgramUnggulanForm = z.object({
judul: z.string().min(3, "Judul minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
});
type ProgramUnggulanForm = Prisma.ProgramUnggulanGetPayload<{
select: {
id: true;
judul: true;
deskripsi: true;
};
}>;
const programUnggulanState = proxy({
findById: {
data: null as ProgramUnggulanForm | null,
loading: false,
initialize() {
programUnggulanState.findById.data = {
id: "",
judul: "",
deskripsi: "",
} as ProgramUnggulanForm;
},
async load(id: string) {
try {
programUnggulanState.findById.loading = true;
const res =
await ApiFetch.api.pendidikan.programpendidikananak.programunggulan[
"find-by-id"
].get({
query: { id },
});
if (res.status === 200) {
programUnggulanState.findById.data = res.data?.data ?? null;
} else {
toast.error("Gagal mengambil data program pendidikan anak");
}
} catch (error) {
console.error((error as Error).message);
toast.error(
"Terjadi kesalahan saat mengambil data program pendidikan anak"
);
} finally {
programUnggulanState.findById.loading = false;
}
},
},
update: {
loading: false,
async save(data: ProgramUnggulanForm) {
const cek = templateProgramUnggulanForm.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 {
programUnggulanState.update.loading = true;
const res =
await ApiFetch.api.pendidikan.programpendidikananak.programunggulan[
"update"
].post(data);
if (res.status === 200) {
toast.success("Data program pendidikan anak berhasil diubah");
await programUnggulanState.findById.load(data.id);
} else {
toast.error("Gagal mengubah data program pendidikan anak");
}
} catch (error) {
console.error((error as Error).message);
toast.error(
"Terjadi kesalahan saat mengubah data program pendidikan anak"
);
} finally {
programUnggulanState.update.loading = false;
}
},
},
});
const stateProgramPendidikanAnak = proxy({
stateTujuanProgram,
programUnggulanState,
});
export default stateProgramPendidikanAnak;

View File

@@ -0,0 +1,220 @@
import { proxy } from "valtio";
import { toast } from "react-toastify";
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { z } from "zod";
const userSchema = z.object({
nama: z.string().min(1, "Nama harus diisi"),
email: z.string().email("Email tidak valid"),
password: z.string().min(6, "Password minimal 6 karakter"),
roleId: z.string().optional(),
});
const defaultForm = {
nama: "",
email: "",
password: "",
roleId: "",
};
const userState = proxy({
create: {
form: { ...defaultForm },
loading: false,
async create(isAdmin: boolean = false) {
const valid = userSchema.safeParse(userState.create.form);
if (!valid.success) {
const err = valid.error.issues.map((i) => i.message).join(", ");
return toast.error(err);
}
try {
userState.create.loading = true;
const res = await ApiFetch.api.user[
isAdmin ? "create" : "register"
].post(userState.create.form);
if (res.status === 200) {
toast.success("User berhasil dibuat");
userState.findMany.load();
} else {
toast.error(res.data?.message || "Gagal membuat user");
}
} catch (e) {
console.error(e);
toast.error("Terjadi kesalahan saat membuat user");
} finally {
userState.create.loading = false;
}
},
},
login: {
form: { email: "", password: "" },
loading: false,
async submit() {
try {
userState.login.loading = true;
const res = await ApiFetch.api.user.login.post(userState.login.form);
if (res.status === 200) {
toast.success("Login berhasil");
const token = res.data?.data?.token;
if (typeof token === "string") {
localStorage.setItem("token", token);
}
} else {
toast.error(res.data?.message || "Login gagal");
}
} catch (e) {
console.error(e);
toast.error("Terjadi kesalahan saat login");
} finally {
userState.login.loading = false;
}
},
},
register: {
form: { ...defaultForm },
loading: false,
async submit() {
const valid = userSchema.safeParse(userState.register.form);
if (!valid.success) {
const err = valid.error.issues.map(i => i.message).join(", ");
return toast.error(err);
}
try {
userState.register.loading = true;
const res = await ApiFetch.api.user.register.post(userState.register.form);
if (res.status === 200) {
toast.success("Registrasi berhasil, silakan login");
userState.register.form = { ...defaultForm }; // Reset form
} else {
toast.error(res.data?.message || "Gagal registrasi");
}
} catch (e) {
console.error(e);
toast.error("Terjadi kesalahan saat registrasi");
} finally {
userState.register.loading = false;
}
},
},
findMany: {
data: [] as Prisma.UserGetPayload<{ include: { role: true } }>[],
loading: false,
async load() {
userState.findMany.loading = true;
const res = await ApiFetch.api.user.findMany.get();
if (res.status === 200) {
userState.findMany.data = res.data?.data ?? [];
}
userState.findMany.loading = false;
},
},
findUnique: {
data: null as Prisma.UserGetPayload<{ include: { role: true } }> | null,
loading: false,
async load(id: string) {
try {
userState.findUnique.loading = true;
const res = await fetch(`/api/user/findUnique/${id}`);
const data = await res.json();
if (res.status === 200) {
userState.findUnique.data = data.data ?? null;
}
} catch (e) {
console.error(e);
toast.error("Terjadi kesalahan saat mengambil data user");
} finally {
userState.findUnique.loading = false;
}
},
},
update: {
id: "",
form: { ...defaultForm },
loading: false,
async load(id: string) {
try {
userState.update.loading = true;
const res = await fetch(`/api/user/findUnique/${id}`);
const data = await res.json();
if (res.status === 200) {
const user = data.data;
userState.update.id = user.id;
userState.update.form = {
nama: user.nama,
email: user.email,
password: "",
roleId: user.roleId,
};
}
} catch (e) {
console.error(e);
toast.error("Terjadi kesalahan saat mengambil data user");
} finally {
userState.update.loading = false;
}
},
async submit() {
try {
userState.update.loading = true;
const res = await fetch(`/api/user/update/${userState.update.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(userState.update.form),
});
const data = await res.json();
if (res.status === 200) {
toast.success("Berhasil update user");
userState.findMany.load();
} else {
toast.error(data?.message || "Gagal update user");
}
} catch (e) {
console.error(e);
toast.error("Terjadi kesalahan saat update user");
} finally {
userState.update.loading = false;
}
},
},
delete: {
loading: false,
async submit(id: string) {
try {
userState.delete.loading = true;
const res = await fetch(`/api/user/del/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({}),
});
const data = await res.json();
if (res.status === 200) {
toast.success("User berhasil dihapus");
userState.findMany.load();
} else {
toast.error(data?.message || "Gagal hapus user");
}
} catch (e) {
console.error(e);
toast.error("Terjadi kesalahan saat hapus user");
} finally {
userState.delete.loading = false;
}
},
},
});
export default userState;

View File

@@ -148,7 +148,6 @@ function Page() {
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box w={{ base: '100%', md: '50%' }}>
<Dropzone
onDrop={(files) => {
const newImages = files.map((file) => ({
file,

View File

@@ -12,17 +12,17 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
{
label: "Posisi Organisasi",
value: "posisiorganisasi",
href: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi"
href: "/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/posisi-organisasi"
},
{
label: "Pegawai",
value: "pegawai",
href: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai"
href: "/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/pegawai"
},
{
label: "Hubungan Organisasi",
value: "hubunganorganisasi",
href: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/hubungan-organisasi"
href: "/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/hubungan-organisasi"
},
];
const curentTab = tabs.find(tab => tab.href === pathname)

View File

@@ -21,7 +21,7 @@ function KeamananLingkungan() {
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListKeamananLingkungan search={search}/>
<ListKeamananLingkungan search={search} />
</Box>
);
}

View File

@@ -0,0 +1,258 @@
/* eslint-disable react-hooks/exhaustive-deps */
"use client";
import apbdes from "@/app/admin/(dashboard)/_state/landing-page/apbdes";
import colors from "@/con/colors";
import ApiFetch from "@/lib/api-fetch";
import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title
} from "@mantine/core";
import { Dropzone } from "@mantine/dropzone";
import { IconArrowBack, IconFile, IconImageInPicture, IconUpload, IconX } from "@tabler/icons-react";
import { useParams, useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { toast } from "react-toastify";
import { useProxy } from "valtio/utils";
function EditAPBDes() {
const apbdesState = useProxy(apbdes);
const router = useRouter();
const params = useParams();
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [previewDoc, setPreviewDoc] = useState<string | null>(null);
const [imageFile, setImageFile] = useState<File | null>(null);
const [docFile, setDocFile] = useState<File | null>(null);
const [formData, setFormData] = useState({
name: apbdesState.edit.form.name || '',
jumlah: apbdesState.edit.form.jumlah || '',
imageId: apbdesState.edit.form.imageId || '',
fileId: apbdesState.edit.form.fileId || ''
});
// Load sdgs desa by id saat pertama kali
useEffect(() => {
const loadKolaborasi = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await apbdesState.edit.load(id); // akses langsung, bukan dari proxy
if (data) {
setFormData({
name: data.name || '',
jumlah: data.jumlah || '',
imageId: data.imageId || '',
fileId: data.fileId || ''
});
if (data.image) {
if (data?.image?.link) {
setPreviewImage(data.image.link);
}
}
if (data.file) {
if (data?.file?.link) {
setPreviewDoc(data.file.link);
}
}
}
} catch (error) {
console.error("Error loading apbdes:", error);
toast.error("Gagal memuat data apbdes");
}
};
loadKolaborasi();
}, [params?.id]);
const handleSubmit = async () => {
try {
// edit global state with form data
apbdesState.edit.form = {
...apbdesState.edit.form,
name: formData.name,
jumlah: formData.jumlah,
imageId: formData.imageId // Keep existing imageId if not changed
};
// Jika ada file image baru, upload
if (imageFile) {
const res = await ApiFetch.api.fileStorage.create.post({ file: imageFile, name: imageFile.name });
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
// edit imageId in global state
apbdesState.edit.form.imageId = uploaded.id;
}
// Jika ada file doc baru, upload
if (docFile) {
const res = await ApiFetch.api.fileStorage.create.post({ file: docFile, name: docFile.name });
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload doc");
}
// edit fileId in global state
apbdesState.edit.form.fileId = uploaded.id;
}
await apbdesState.edit.update();
toast.success("apbdes berhasil diperbarui!");
router.push("/admin/landing-page/APBDes");
} catch (error) {
console.error("Error updating apbdes:", error);
toast.error("Terjadi kesalahan saat memperbarui apbdes");
}
};
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 APBDes</Title>
<TextInput
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Nama</Text>}
placeholder="masukkan nama"
/>
<TextInput
value={formData.jumlah}
onChange={(e) => setFormData({ ...formData, jumlah: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Jumlah</Text>}
placeholder="masukkan jumlah"
/>
<Box>
<Text fz={"md"} fw={"bold"}>File Image</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setImageFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{
'application/*': ['.jpeg', '.jpg', '.png', '.gif', '.webp', '.svg'],
}}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconImageInPicture size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag file ke sini atau klik untuk pilih image
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format image
</Text>
</div>
</Group>
</Dropzone>
<Box>
<Text fw={"bold"} fz={"lg"}>Image</Text>
{previewImage ? (
<iframe
src={previewImage}
width="100%"
height="500px"
style={{ border: "1px solid #ccc", borderRadius: "8px" }}
/>
) : (
<Text>Tidak ada image tersedia</Text>
)}
</Box>
</Box>
</Box>
<Box>
<Text fz={"md"} fw={"bold"}>File Doc</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setDocFile(selectedFile);
setPreviewDoc(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{
'application/*': ['.pdf', '.doc', '.docx'],
}}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconFile size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag file ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format doc
</Text>
</div>
</Group>
</Dropzone>
<Box>
<Text fw={"bold"} fz={"lg"}>Dokumen</Text>
{previewDoc ? (
<iframe
src={previewDoc}
width="100%"
height="500px"
style={{ border: "1px solid #ccc", borderRadius: "8px" }}
/>
) : (
<Text>Tidak ada doc tersedia</Text>
)}
</Box>
</Box>
</Box>
<Button onClick={handleSubmit}>Simpan</Button>
</Stack>
</Paper>
</Box>
);
}
export default EditAPBDes;

View File

@@ -0,0 +1,126 @@
'use client'
import { useProxy } from 'valtio/utils';
import { ActionIcon, Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconFile, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import colors from '@/con/colors';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import apbdes from '../../../_state/landing-page/apbdes';
function DetailAPBDes() {
const apbdesState = useProxy(apbdes)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter()
useShallowEffect(() => {
apbdesState.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
apbdesState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/landing-page/APBDes")
}
}
if (!apbdesState.findUnique.data) {
return (
<Stack py={10}>
<Skeleton 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 APBDes</Text>
{apbdesState.findUnique.data ? (
<Paper key={apbdesState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Nama APBDes</Text>
<Text fz={"lg"}>{apbdesState.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Jumlah</Text>
<Text fz={"lg"}>{apbdesState.findUnique.data?.jumlah}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
<Image w={{ base: 150, md: 150, lg: 150 }} src={apbdesState.findUnique.data?.image?.link} alt="gambar" />
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Dokumen</Text>
{apbdesState.findUnique.data?.file?.link ? (
<ActionIcon
component="a"
href={apbdesState.findUnique.data.file.link}
target="_blank"
rel="noopener noreferrer"
variant="transparent"
>
<IconFile size={25} color={colors['blue-button']}/>
</ActionIcon>
) : (
<Text>Tidak ada dokumen tersedia</Text>
)}
</Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (apbdesState.findUnique.data) {
setSelectedId(apbdesState.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={apbdesState.delete.loading || !apbdesState.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (apbdesState.findUnique.data) {
router.push(`/admin/landing-page/APBDes/${apbdesState.findUnique.data.id}/edit`);
}
}}
disabled={!apbdesState.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 APBDes ini?'
/>
</Box>
);
}
export default DetailAPBDes;

View File

@@ -0,0 +1,215 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconFile, IconImageInPicture, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import apbdes from '../../../_state/landing-page/apbdes';
function CreateAPBDes() {
const router = useRouter();
const stateAPBDes = useProxy(apbdes)
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [previewDoc, setPreviewDoc] = useState<string | null>(null);
const [imageFile, setImageFile] = useState<File | null>(null);
const [docFile, setDocFile] = useState<File | null>(null);
useEffect(() => {
stateAPBDes.findMany.load();
}, []);
const resetForm = () => {
stateAPBDes.create.form = {
name: "",
jumlah: "",
imageId: "",
fileId: "",
};
setImageFile(null);
setDocFile(null);
setPreviewImage(null);
};
const handleSubmit = async () => {
if (!imageFile || !docFile) {
return toast.warn("Pilih gambar dan dokumen terlebih dahulu");
}
try {
const [uploadImageRes, uploadDocRes] = await Promise.all([
ApiFetch.api.fileStorage.create.post({ file: imageFile, name: imageFile.name }),
ApiFetch.api.fileStorage.create.post({ file: docFile, name: docFile.name }),
]);
const imageId = uploadImageRes?.data?.data?.id;
const fileId = uploadDocRes?.data?.data?.id;
if (!imageId || !fileId) {
return toast.error("Gagal mengupload file");
}
stateAPBDes.create.form.imageId = imageId;
stateAPBDes.create.form.fileId = fileId;
await stateAPBDes.create.create();
toast.success("Berhasil menambahkan APBDes");
resetForm();
router.push("/admin/landing-page/APBDes");
} catch (error) {
console.error("Gagal submit:", error);
toast.error("Gagal menyimpan data");
}
};
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}>Create APBDes</Title>
<Box>
<Text fz={"md"} fw={"bold"}>File Image</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setImageFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{
'application/*': ['.jpeg', '.jpg', '.png', '.gif', '.webp', '.svg'],
}}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconImageInPicture size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag file ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format image
</Text>
</div>
</Group>
</Dropzone>
<Box>
<Text fw={"bold"} fz={"lg"}>Dokumen</Text>
{previewImage ? (
<iframe
src={previewImage}
width="100%"
height="500px"
style={{ border: "1px solid #ccc", borderRadius: "8px" }}
/>
) : (
<Text>Tidak ada image tersedia</Text>
)}
</Box>
</Box>
</Box>
<Box>
<Text fz={"md"} fw={"bold"}>File Dokumen</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setDocFile(selectedFile);
setPreviewDoc(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{
'application/*': ['.pdf', '.doc', '.docx'],
}}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconFile size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag file ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format dokumen
</Text>
</div>
</Group>
</Dropzone>
<Box>
<Text fw={"bold"} fz={"lg"}>Dokumen</Text>
{previewDoc ? (
<iframe
src={previewDoc}
width="100%"
height="500px"
style={{ border: "1px solid #ccc", borderRadius: "8px" }}
/>
) : (
<Text>Tidak ada dokumen tersedia</Text>
)}
</Box>
</Box>
</Box>
<TextInput
value={stateAPBDes.create.form.name}
onChange={(val) => {
stateAPBDes.create.form.name = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Judul</Text>}
placeholder='Masukkan judul'
/>
<TextInput
type='number'
value={stateAPBDes.create.form.jumlah}
onChange={(val) => {
stateAPBDes.create.form.jumlah = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Jumlah</Text>}
placeholder='Masukkan jumlah'
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreateAPBDes;

View File

@@ -1,11 +1,113 @@
import React from 'react';
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { ActionIcon, Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImacCog, IconFile, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import apbdes from '../../_state/landing-page/apbdes';
function Page() {
function APBDes() {
const [search, setSearch] = useState('');
return (
<div>
APBDes
</div>
<Box>
<HeaderSearch
title='APBDes'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListAPBDes search={search} />
</Box>
);
}
export default Page;
function ListAPBDes({ search }: { search: string }) {
const listState = useProxy(apbdes)
const router = useRouter();
useEffect(() => {
listState.findMany.load()
}, [])
const filteredData = (listState.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.jumlah.toLowerCase().includes(keyword)
)
});
if (!listState.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<JudulList
title='List APBDes'
href='/admin/landing-page/APBDes/create'
/>
<Box style={{ overflowX: 'auto' }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
<TableThead>
<TableTr>
<TableTh>Nama APBDes</TableTh>
<TableTh>Jumlah APBDes</TableTh>
<TableTh>Document</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>{item.name}</Text>
</Box>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"}>{item.jumlah}</Text>
</TableTd>
<TableTd>
{item.file?.link ? (
<ActionIcon
component="a"
href={item.file.link}
target="_blank"
rel="noopener noreferrer"
variant='transparent'
>
<IconFile size={25} color={colors['blue-button']}/>
</ActionIcon>
) : (
<Text>Tidak ada dokumen tersedia</Text>
)}
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/landing-page/APBDes/${item.id}`)}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
</Stack>
</Paper>
</Box>
)
}
export default APBDes;

View File

@@ -0,0 +1,62 @@
/* 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 LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter()
const pathname = usePathname()
const tabs = [
{
label: "List Desa Anti Korupsi",
value: "listDesaAntiKorupsi",
href: "/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi"
},
{
label: "Kategori Desa Anti Korupsi",
value: "kategoriDesaAntiKorupsi",
href: "/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi"
},
];
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}>Desa Anti Korupsi</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 LayoutTabs;

View File

@@ -0,0 +1,98 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi';
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 EditKategoriDesaAntiKorupsi() {
const router = useRouter();
const params = useParams();
const id = params?.id as string;
const stateKategori = useProxy(korupsiState.kategoriDesaAntiKorupsi);
const [formData, setFormData] = useState({
name: "",
});
useEffect(() => {
const loadKategorikegiatan = async () => {
if (!id) return;
try {
const data = await stateKategori.edit.load(id);
if (data) {
// pastikan id-nya masuk ke state edit
stateKategori.edit.id = id;
setFormData({
name: data.name || '',
});
}
} catch (error) {
console.error("Error loading kategori desa anti korupsi:", error);
toast.error("Gagal memuat data kategori desa anti korupsi");
}
};
loadKategorikegiatan();
}, [id]);
const handleSubmit = async () => {
try {
if (!formData.name.trim()) {
toast.error('Nama kategori desa anti korupsi tidak boleh kosong');
return;
}
stateKategori.edit.form = {
name: formData.name.trim(),
};
// Safety check tambahan: pastikan ID tidak kosong
if (!stateKategori.edit.id) {
stateKategori.edit.id = id; // fallback
}
const success = await stateKategori.edit.update();
if (success) {
router.push("/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi");
}
} catch (error) {
console.error("Error updating kategori desa anti korupsi:", error);
// toast akan ditampilkan dari fungsi update
}
};
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 Kategori Desa Anti Korupsi</Title>
<TextInput
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Desa Anti Korupsi</Text>}
placeholder='Masukkan nama kategori desa anti korupsi'
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditKategoriDesaAntiKorupsi;

View File

@@ -0,0 +1,61 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
import korupsiState from '../../../../_state/landing-page/desa-anti-korupsi';
function CreateKategoriDesaAntiKorupsi() {
const router = useRouter();
const stateKategori = useProxy(korupsiState.kategoriDesaAntiKorupsi)
useEffect(() => {
stateKategori.findMany.load();
}, []);
const resetForm = () => {
stateKategori.create.form = {
name: "",
};
}
const handleSubmit = async () => {
await stateKategori.create.create();
resetForm();
router.push("/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi")
}
return (
<Box>
<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}>Create Kategori Desa Anti Korupsi</Title>
<TextInput
value={stateKategori.create.form.name}
onChange={(val) => {
stateKategori.create.form.name = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Desa Anti Korupsi</Text>}
placeholder='Masukkan nama kategori desa anti korupsi'
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
</Box>
);
}
export default CreateKategoriDesaAntiKorupsi;

View File

@@ -0,0 +1,112 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import korupsiState from '../../../_state/landing-page/desa-anti-korupsi';
function KategoriDesaAntiKorupsi() {
const [search, setSearch] = useState("")
return (
<Box>
<HeaderSearch
title='Kategori Desa Anti Korupsi'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListKategoriKegiatan search={search} />
</Box>
);
}
function ListKategoriKegiatan({ search }: { search: string }) {
const stateKategori = useProxy(korupsiState.kategoriDesaAntiKorupsi)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter()
const handleHapus = () => {
if (selectedId) {
stateKategori.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
}
}
useShallowEffect(() => {
stateKategori.findMany.load()
}, [])
const filteredData = (stateKategori.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword)
);
});
if (!stateKategori.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Kategori Kegiatan'
href='/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Kategori</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>
<Button color="green" onClick={() => router.push(`/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<Button color="red" onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconX size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus kategori kegiatan ini?'
/>
</Box>
);
}
export default KategoriDesaAntiKorupsi

View File

@@ -0,0 +1,12 @@
import React from 'react';
import LayoutTabs from './_lib/layouTabs';
function Layout({ children }: { children: React.ReactNode }) {
return (
<LayoutTabs>
{children}
</LayoutTabs>
);
}
export default Layout;

View File

@@ -0,0 +1,229 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import { useProxy } from 'valtio/utils';
import { Box, Button, Group, Paper, Select, Stack, Text, TextInput } from '@mantine/core';
import { IconArrowBack, IconFile, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import colors from '@/con/colors';
import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi';
import ApiFetch from '@/lib/api-fetch';
import { Dropzone } from '@mantine/dropzone';
import { toast } from 'react-toastify';
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
interface FormDesaAntiKorupsi {
name: string;
deskripsi: string;
kategoriId: string;
fileId: string;
}
function EditDesaAntiKorupsi() {
const desaAntiKorupsiState = useProxy(korupsiState.desaAntikorupsi)
const [previewFile, setPreviewFile] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const params = useParams()
const router = useRouter()
const [formData, setFormData] = useState<FormDesaAntiKorupsi>({
name: '',
deskripsi: '',
kategoriId: '',
fileId: '',
})
useEffect(() => {
const loadDesaAntiKorupsi = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await desaAntiKorupsiState.edit.load(id);
if (data) {
// ⬇️ FIX PENTING: tambahkan ini
desaAntiKorupsiState.edit.id = id;
desaAntiKorupsiState.edit.form = {
name: data.name,
deskripsi: data.deskripsi,
kategoriId: data.kategoriId,
fileId: data.fileId,
};
setFormData({
name: data.name,
deskripsi: data.deskripsi,
kategoriId: data.kategoriId,
fileId: data.fileId,
});
if (data?.file?.link) {
setPreviewFile(data.file.link)
}
}
} catch (error) {
console.error("Error loading program penghijauan:", error);
toast.error("Gagal memuat data program penghijauan");
}
}
loadDesaAntiKorupsi();
}, [params?.id]);
const handleSubmit = async () => {
try {
// Update global state with form data
desaAntiKorupsiState.edit.form = {
...desaAntiKorupsiState.edit.form,
name: formData.name,
deskripsi: formData.deskripsi,
kategoriId: formData.kategoriId || '',
fileId: formData.fileId // Keep existing imageId if not changed
};
// Jika ada file baru, upload
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");
}
// Update imageId in global state
desaAntiKorupsiState.edit.form.fileId = uploaded.id;
}
await desaAntiKorupsiState.edit.update();
toast.success("desa anti korupsi berhasil diperbarui!");
router.push("/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi");
} catch (error) {
console.error("Error updating desa anti korupsi:", error);
toast.error("Terjadi kesalahan saat memperbarui desa anti korupsi");
}
};
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"}>Edit List Desa Anti Korupsi</Text>
{desaAntiKorupsiState.findUnique.data ? (
<Paper key={desaAntiKorupsiState.findUnique.data.id}>
<Stack gap={"xs"}>
<TextInput
value={formData.name}
onChange={(val) => {
setFormData({
...formData,
name: val.target.value
})
}}
label={<Text fw={"bold"} fz={"sm"}>Judul</Text>}
placeholder='Masukkan judul'
/>
<Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
<EditEditor
value={formData.deskripsi}
onChange={(val) => {
setFormData({
...formData,
deskripsi: val
})
}}
/>
</Box>
<Select
value={formData.kategoriId}
onChange={(val) => {
setFormData({
...formData,
kategoriId: val ?? ""
})
}}
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>}
placeholder="Pilih kategori"
data={
korupsiState.kategoriDesaAntiKorupsi.findMany.data?.map((v) => ({
value: v.id,
label: v.name,
})) || []
}
/>
<Box>
<Text fz={"md"} fw={"bold"}>File Document</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewFile(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{
'application/*': ['.pdf', '.doc', '.docx'],
}}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconFile size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag file ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format document
</Text>
</div>
</Group>
</Dropzone>
<Box>
<Text fw={"bold"} fz={"lg"}>Dokumen</Text>
{previewFile ? (
<iframe
src={previewFile}
width="100%"
height="500px"
style={{ border: "1px solid #ccc", borderRadius: "8px" }}
/>
) : (
<Text>Tidak ada dokumen tersedia</Text>
)}
</Box>
</Box>
</Box>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
</Box>
);
}
export default EditDesaAntiKorupsi;

View File

@@ -0,0 +1,122 @@
'use client'
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi';
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 DetailKegiatanDesa() {
const detailState = useProxy(korupsiState.desaAntikorupsi)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter()
useShallowEffect(() => {
detailState.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
detailState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi")
}
}
if (!detailState.findUnique.data) {
return (
<Stack py={10}>
<Skeleton 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 List Desa Anti Korupsi</Text>
{detailState.findUnique.data ? (
<Paper key={detailState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Judul</Text>
<Text fz={"lg"}>{detailState.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: detailState.findUnique.data?.deskripsi }} />
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Kategori</Text>
<Text fz={"lg"}>{detailState.findUnique.data?.kategori?.name}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Dokumen</Text>
{detailState.findUnique.data?.file?.link ? (
<iframe
src={detailState.findUnique.data.file.link}
width="100%"
height="500px"
style={{ border: "1px solid #ccc", borderRadius: "8px" }}
/>
) : (
<Text>Tidak ada dokumen tersedia</Text>
)}
</Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (detailState.findUnique.data) {
setSelectedId(detailState.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={detailState.delete.loading || !detailState.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (detailState.findUnique.data) {
router.push(`/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/${detailState.findUnique.data.id}/edit`);
}
}}
disabled={!detailState.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 desa anti korupsi ini?'
/>
</Box>
);
}
export default DetailKegiatanDesa;

View File

@@ -0,0 +1,166 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconFile, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function CreateDesaAntiKorupsi() {
const router = useRouter();
const stateKorupsi = useProxy(korupsiState.desaAntikorupsi)
const [previewFile, setPreviewFile] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
useEffect(() => {
stateKorupsi.findMany.load();
korupsiState.kategoriDesaAntiKorupsi.findMany.load();
}, []);
const resetForm = () => {
stateKorupsi.create.form = {
name: "",
deskripsi: "",
kategoriId: "",
fileId: "",
};
setFile(null);
setPreviewFile(null);
};
const handleSubmit = async () => {
if (!file) {
return toast.warn("Pilih file pdf terlebih dahulu");
}
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
})
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal mengupload file");
}
stateKorupsi.create.form.fileId = uploaded.id;
await stateKorupsi.create.create();
resetForm();
router.push("/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi")
}
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}>Create Kegiatan Desa</Title>
<Box>
<Text fz={"md"} fw={"bold"}>File Document</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewFile(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{
'application/*': ['.pdf', '.doc', '.docx'],
}}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconFile size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag file ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format document
</Text>
</div>
</Group>
</Dropzone>
<Box>
<Text fw={"bold"} fz={"lg"}>Dokumen</Text>
{previewFile ? (
<iframe
src={previewFile}
width="100%"
height="500px"
style={{ border: "1px solid #ccc", borderRadius: "8px" }}
/>
) : (
<Text>Tidak ada dokumen tersedia</Text>
)}
</Box>
</Box>
</Box>
<TextInput
value={stateKorupsi.create.form.name}
onChange={(val) => {
stateKorupsi.create.form.name = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Judul</Text>}
placeholder='Masukkan judul'
/>
<Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
<CreateEditor
value={stateKorupsi.create.form.deskripsi}
onChange={(val) => {
stateKorupsi.create.form.deskripsi = val;
}}
/>
</Box>
<Select
value={stateKorupsi.create.form.kategoriId}
onChange={(val) => {
stateKorupsi.create.form.kategoriId = val ?? "";
}}
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>}
placeholder="Pilih kategori"
data={
korupsiState.kategoriDesaAntiKorupsi.findMany.data?.map((v) => ({
value: v.id,
label: v.name,
})) || []
}
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreateDesaAntiKorupsi;

View File

@@ -0,0 +1,99 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import korupsiState from '../../../_state/landing-page/desa-anti-korupsi';
function DesaAntiKorupsi() {
const [search, setSearch] = useState('');
return (
<Box>
<HeaderSearch
title='List Desa Anti Korupsi'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListDesaAntiKorupsi search={search} />
</Box>
);
}
function ListDesaAntiKorupsi({ search }: { search: string }) {
const listState = useProxy(korupsiState.desaAntikorupsi)
const router = useRouter();
useEffect(() => {
listState.findMany.load()
}, [])
const filteredData = (listState.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.deskripsi.toLowerCase().includes(keyword) ||
item.kategori?.name?.toLowerCase().includes(keyword)
);
});
if (!listState.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<JudulList
title='List Desa Anti Korupsi'
href='/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/create'
/>
<Box style={{ overflowX: 'auto' }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
<TableThead>
<TableTr>
<TableTh>Nama Desa Anti Korupsi</TableTh>
<TableTh>Deskripsi Desa Anti Korupsi</TableTh>
<TableTh>Kategori Desa Anti Korupsi</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>{item.name}</Text>
</Box>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd>
<TableTd>{item.kategori?.name}</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/${item.id}`)}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
</Stack>
</Paper>
</Box>
)
}
export default DesaAntiKorupsi;

View File

@@ -1,11 +0,0 @@
import React from 'react';
function Page() {
return (
<div>
Desa Anti Korupsi
</div>
);
}
export default Page;

View File

@@ -0,0 +1,67 @@
/* 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 LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter()
const pathname = usePathname()
const tabs = [
{
label: "List Survey",
value: "listSurvey",
href: "/admin/landing-page/indeks-kepuasan-masyarakat/list-survey"
},
{
label: "List Bulanan",
value: "listBulanan",
href: "/admin/landing-page/indeks-kepuasan-masyarakat/list-bulanan"
},
{
label: "List Gender Stat",
value: "listGenderStat",
href: "/admin/landing-page/indeks-kepuasan-masyarakat/list-gender-stat"
}
];
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}>Indeks Kepuasan Masyarakat</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 LayoutTabs;

View File

@@ -0,0 +1,12 @@
import React from 'react';
import LayoutTabs from './_lib/layoutTabs';
function Layout({children} : {children: React.ReactNode}) {
return (
<LayoutTabs>
{children}
</LayoutTabs>
);
}
export default Layout;

View File

@@ -0,0 +1,128 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan-masyarakat';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Select, Stack, Text, TextInput } 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 EditListBulanan() {
const editState = useProxy(indeksKepuasanState.monthlyStatState)
const params = useParams()
const router = useRouter()
const [formData, setFormData] = useState({
month: editState.edit.form.month || '',
respondentsCount: editState.edit.form.respondentsCount || 0,
surveyId: editState.edit.form.surveyId || '',
})
useEffect(() => {
const loadSurvey = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await editState.edit.load(id); // akses langsung, bukan dari proxy
if (data) {
setFormData({
month: data.month,
respondentsCount: data.respondentsCount,
surveyId: data.surveyId,
});
}
} catch (error) {
console.error("Error loading list bulanan:", error);
toast.error("Gagal memuat data list bulanan");
}
};
indeksKepuasanState.surveyState.findMany.load();
loadSurvey();
}, [params?.id]);
const handleSubmit = async () => {
try {
// edit global state with form data
editState.edit.form = {
...editState.edit.form,
month: formData.month,
respondentsCount: formData.respondentsCount,
surveyId: formData.surveyId,
};
await editState.edit.update();
toast.success("list bulanan berhasil diperbarui!");
router.push("/admin/landing-page/indeks-kepuasan-masyarakat/list-bulanan");
} catch (error) {
console.error("Error updating list bulanan:", error);
toast.error("Terjadi kesalahan saat memperbarui list bulanan");
}
};
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"}>Edit List Survey</Text>
<Paper>
<Stack gap={"xs"}>
<TextInput
value={formData.month}
onChange={(val) => {
setFormData({
...formData,
month: val.target.value
})
}}
label={<Text fw={"bold"} fz={"sm"}>Bulan</Text>}
placeholder='Masukkan bulan'
/>
<TextInput
value={formData.respondentsCount}
onChange={(val) => {
setFormData({
...formData,
respondentsCount: Number(val.target.value)
})
}}
label={<Text fw={"bold"} fz={"sm"}>Total Responden</Text>}
placeholder='Masukkan total responden'
/>
<Select
label={<Text fw="bold" fz="sm">Pilih Survey</Text>}
placeholder="Pilih survey"
value={formData.surveyId}
onChange={(value) => {
if (value) setFormData({
...formData,
surveyId: value
})
}}
data={
indeksKepuasanState.surveyState.findMany.data?.map((survey) => ({
value: survey.id,
label: `${survey.title} (${survey.totalRespondents} responden)`,
})) || []
}
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Stack>
</Paper>
</Box>
);
}
export default EditListBulanan;

View File

@@ -0,0 +1,109 @@
'use client'
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan-masyarakat';
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 DetailListBulanan() {
const detailState = useProxy(indeksKepuasanState.monthlyStatState)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter()
useShallowEffect(() => {
detailState.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
detailState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/landing-page/indeks-kepuasan-masyarakat/list-bulanan")
}
}
if (!detailState.findUnique.data) {
return (
<Stack py={10}>
<Skeleton 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 List Survey</Text>
{detailState.findUnique.data ? (
<Paper key={detailState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Bulan</Text>
<Text fz={"lg"}>{detailState.findUnique.data?.month}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Total Responden</Text>
<Text fz={"lg"}>{detailState.findUnique.data?.respondentsCount}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Survey</Text>
<Text fz={"lg"}>{detailState.findUnique.data?.survey.title }</Text>
</Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (detailState.findUnique.data) {
setSelectedId(detailState.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={detailState.delete.loading || !detailState.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (detailState.findUnique.data) {
router.push(`/admin/landing-page/indeks-kepuasan-masyarakat/list-bulanan/${detailState.findUnique.data.id}/edit`);
}
}}
disabled={!detailState.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 list bulanan ini?'
/>
</Box>
);
}
export default DetailListBulanan;

View File

@@ -0,0 +1,84 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan-masyarakat';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
function CreateListBulanan() {
const router = useRouter();
const stateCreate = useProxy(indeksKepuasanState.monthlyStatState)
useEffect(() => {
stateCreate.findMany.load();
indeksKepuasanState.surveyState.findMany.load();
}, []);
const resetForm = () => {
stateCreate.create.form = {
month: "",
respondentsCount: 0,
surveyId: "",
};
};
const handleSubmit = async () => {
await stateCreate.create.create();
resetForm();
router.push("/admin/landing-page/indeks-kepuasan-masyarakat/list-bulanan")
}
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}>Create List Bulanan</Title>
<TextInput
value={stateCreate.create.form.month}
onChange={(val) => {
stateCreate.create.form.month = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Bulan</Text>}
placeholder='Masukkan bulan'
/>
<TextInput
value={stateCreate.create.form.respondentsCount}
onChange={(val) => {
stateCreate.create.form.respondentsCount = Number(val.target.value);
}}
label={<Text fw={"bold"} fz={"sm"}>Total Responden</Text>}
placeholder='Masukkan total responden'
/>
<Select
label={<Text fw="bold" fz="sm">Pilih Survey</Text>}
placeholder="Pilih survey"
value={stateCreate.create.form.surveyId}
onChange={(value) => {
if (value) stateCreate.create.form.surveyId = value;
}}
data={
indeksKepuasanState.surveyState.findMany.data?.map((survey) => ({
value: survey.id,
label: `${survey.title} (${survey.totalRespondents} responden)`,
})) || []
}
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreateListBulanan;

View File

@@ -0,0 +1,98 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import indeksKepuasanState from '../../../_state/landing-page/indeks-kepuasan-masyarakat';
import JudulList from '../../../_com/judulList';
function ListBulananLandingPage() {
const [search, setSearch] = useState('');
return (
<Box>
<HeaderSearch
title='List Bulanan'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListBUlanan search={search} />
</Box>
);
}
function ListBUlanan({ search }: { search: string }) {
const listState = useProxy(indeksKepuasanState.monthlyStatState)
const router = useRouter();
useEffect(() => {
listState.findMany.load()
}, [])
const filteredData = (listState.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.month.toLowerCase().includes(keyword) ||
item.respondentsCount.toString().includes(keyword)
);
});
if (!listState.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<JudulList
title='List Bulanan'
href='/admin/landing-page/indeks-kepuasan-masyarakat/list-bulanan/create'
/>
<Box style={{ overflowX: 'auto' }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
<TableThead>
<TableTr>
<TableTh>Bulan</TableTh>
<TableTh>Total Responden</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>{item.month}</Text>
</Box>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"}>{item.respondentsCount}</Text>
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/landing-page/indeks-kepuasan-masyarakat/list-bulanan/${item.id}`)}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
</Stack>
</Paper>
</Box>
)
}
export default ListBulananLandingPage;

View File

@@ -0,0 +1,186 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan-masyarakat';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Select, Stack, Text, TextInput } 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 EditListSurvey() {
const editState = useProxy(indeksKepuasanState.genderStatState)
const params = useParams()
const router = useRouter()
const [formData, setFormData] = useState({
laki: editState.edit.form.laki || 0,
perempuan: editState.edit.form.perempuan || 0,
monthlyStatId: editState.edit.form.monthlyStatId || '',
total: editState.edit.form.total || 0,
percentLaki: editState.edit.form.percentLaki || 0,
percentPerempuan: editState.edit.form.percentPerempuan || 0,
})
useEffect(() => {
indeksKepuasanState.monthlyStatState.findMany.load();
const loadSurvey = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await editState.edit.load(id); // akses langsung, bukan dari proxy
if (data) {
setFormData({
laki: data.laki || 0,
perempuan: data.perempuan || 0,
monthlyStatId: data.monthlyStatId || '',
total: data.total || 0,
percentLaki: data.percentLaki || 0,
percentPerempuan: data.percentPerempuan || 0,
});
}
} catch (error) {
console.error("Error loading list survey:", error);
toast.error("Gagal memuat data list survey");
}
};
loadSurvey();
}, [params?.id]);
// Hitung total dan persentase saat nilai laki atau perempuan berubah
useEffect(() => {
const total = formData.laki + formData.perempuan;
const percentLaki = total > 0 ? Math.round((formData.laki / total) * 100) : 0;
const percentPerempuan = 100 - percentLaki;
setFormData(prev => ({
...prev,
total,
percentLaki,
percentPerempuan
}));
}, [formData.laki, formData.perempuan]);
const handleSubmit = async () => {
if (!formData.monthlyStatId) {
return toast.error("Silakan pilih bulan terlebih dahulu");
}
if (formData.laki < 0 || formData.perempuan < 0) {
return toast.error("Nilai tidak boleh negatif");
}
try {
// edit global state with form data
editState.edit.form = {
...editState.edit.form,
laki: formData.laki,
perempuan: formData.perempuan,
monthlyStatId: formData.monthlyStatId,
total: formData.total,
percentLaki: formData.percentLaki,
percentPerempuan: formData.percentPerempuan,
};
await editState.edit.update();
toast.success("Data gender berhasil diperbarui!");
router.push("/admin/landing-page/indeks-kepuasan-masyarakat/list-gender-stat");
} catch (error) {
console.error("Error updating list gender:", error);
toast.error("Terjadi kesalahan saat memperbarui data gender");
}
};
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"}>Edit List Survey</Text>
<Paper>
<Stack gap={"xs"}>
<TextInput
type="number"
value={formData.laki}
onChange={(val) => {
setFormData({
...formData,
laki: Number(val.target.value)
})
}}
label={<Text fw={"bold"} fz={"sm"}>Laki - Laki</Text>}
placeholder='Masukkan laki - laki'
/>
<TextInput
type="number"
value={formData.perempuan}
onChange={(val) => {
setFormData({
...formData,
perempuan: Number(val.target.value)
})
}}
label={<Text fw={"bold"} fz={"sm"}>Perempuan</Text>}
placeholder='Masukkan perempuan'
/>
<TextInput
type="number"
value={formData.total}
label={<Text fw="bold" fz="sm">Total</Text>}
disabled
/>
<TextInput
type="number"
value={formData.percentLaki}
label={<Text fw="bold" fz="sm">Persentase Laki Laki</Text>}
disabled
/>
<TextInput
type="number"
value={formData.percentPerempuan}
label={<Text fw="bold" fz="sm">Persentase Perempuan</Text>}
disabled
/>
<Select
label={"Pilih Bulan"}
placeholder="Pilih bulanan"
value={formData.monthlyStatId}
onChange={(value) => {
setFormData(prev => ({
...prev,
monthlyStatId: value || ''
}));
}}
data={
indeksKepuasanState.monthlyStatState.findMany.data?.map((monthlyStat) => ({
value: monthlyStat.id,
label: `${monthlyStat.month} (${monthlyStat.respondentsCount} responden)`,
})) || []
}
required
error={!formData.monthlyStatId ? 'Bulan harus dipilih' : undefined}
/>
<Group>
<Button
bg={colors['blue-button']}
onClick={handleSubmit}
loading={editState.edit.loading}
>
{editState.edit.loading ? 'Menyimpan...' : 'Simpan Perubahan'}
</Button>
</Group>
</Stack>
</Paper>
</Stack>
</Paper>
</Box>
);
}
export default EditListSurvey;

View File

@@ -0,0 +1,113 @@
'use client'
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan-masyarakat';
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 DetailGenderStat() {
const detailState = useProxy(indeksKepuasanState.genderStatState)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter()
useShallowEffect(() => {
detailState.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
detailState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/landing-page/indeks-kepuasan-masyarakat/list-gender-stat")
}
}
if (!detailState.findUnique.data) {
return (
<Stack py={10}>
<Skeleton 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 List Survey</Text>
{detailState.findUnique.data ? (
<Paper key={detailState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Laki Laki</Text>
<Text fz={"lg"}>{detailState.findUnique.data?.laki}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Perempuan</Text>
<Text fz={"lg"}>{detailState.findUnique.data?.perempuan}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Persentase Laki Laki</Text>
<Text fz={"lg"}>{detailState.findUnique.data?.percentLaki}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Persentase Perempuan</Text>
<Text fz={"lg"}>{detailState.findUnique.data?.percentPerempuan}</Text>
</Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (detailState.findUnique.data) {
setSelectedId(detailState.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={detailState.delete.loading || !detailState.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (detailState.findUnique.data) {
router.push(`/admin/landing-page/indeks-kepuasan-masyarakat/list-gender-stat/${detailState.findUnique.data.id}/edit`);
}
}}
disabled={!detailState.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 list gender stat ini?'
/>
</Box>
);
}
export default DetailGenderStat;

View File

@@ -0,0 +1,103 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan-masyarakat';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
function CreateListGenderStat() {
const router = useRouter();
const stateCreate = useProxy(indeksKepuasanState.genderStatState)
useEffect(() => {
stateCreate.findMany.load();
indeksKepuasanState.monthlyStatState.findMany.load();
}, []);
const resetForm = () => {
stateCreate.create.form = {
laki: 0,
perempuan: 0,
monthlyStatId: "",
total: 0,
percentLaki: 0,
percentPerempuan: 0,
};
};
const handleSubmit = async () => {
await stateCreate.create.create();
resetForm();
router.push("/admin/landing-page/indeks-kepuasan-masyarakat/list-gender-stat")
}
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}>Create List Gender Stat</Title>
<TextInput
value={stateCreate.create.form.laki}
onChange={(val) => {
stateCreate.create.form.laki = Number(val.target.value);
}}
label={<Text fw={"bold"} fz={"sm"}>Laki Laki</Text>}
placeholder='Masukkan laki laki'
/>
<TextInput
value={stateCreate.create.form.perempuan}
onChange={(val) => {
stateCreate.create.form.perempuan = Number(val.target.value);
}}
label={<Text fw={"bold"} fz={"sm"}>Perempuan</Text>}
placeholder='Masukkan perempuan'
/>
<TextInput
value={stateCreate.create.form.total}
label={<Text fw="bold" fz="sm">Total</Text>}
disabled
/>
<TextInput
value={stateCreate.create.form.percentLaki}
label={<Text fw="bold" fz="sm">Persentase Laki Laki</Text>}
disabled
/>
<TextInput
value={stateCreate.create.form.percentPerempuan}
label={<Text fw="bold" fz="sm">Persentase Perempuan</Text>}
disabled
/>
<Select
label={<Text fw="bold" fz="sm">Pilih Bulan</Text>}
placeholder="Pilih bulanan"
value={stateCreate.create.form.monthlyStatId}
onChange={(value) => {
if (value) stateCreate.create.form.monthlyStatId = value;
}}
data={
indeksKepuasanState.monthlyStatState.findMany.data?.map((monthlyStat) => ({
value: monthlyStat.id,
label: `${monthlyStat.month} (${monthlyStat.respondentsCount} responden)`,
})) || []
}
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreateListGenderStat;

View File

@@ -0,0 +1,106 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import indeksKepuasanState from '../../../_state/landing-page/indeks-kepuasan-masyarakat';
function ListGenderStat() {
const [search, setSearch] = useState('');
return (
<Box>
<HeaderSearch
title='Gender Stat'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListGender search={search} />
</Box>
);
}
function ListGender({ search }: { search: string }) {
const listState = useProxy(indeksKepuasanState.genderStatState)
const router = useRouter();
useEffect(() => {
listState.findMany.load()
}, [])
const filteredData = (listState.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.laki.toString().includes(keyword) ||
item.perempuan.toString().includes(keyword)
);
});
if (!listState.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<JudulList
title='List Gender Stat'
href='/admin/landing-page/indeks-kepuasan-masyarakat/list-gender-stat/create'
/>
<Box style={{ overflowX: 'auto' }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
<TableThead>
<TableTr>
<TableTh>Laki Laki</TableTh>
<TableTh>Perempuan</TableTh>
<TableTh>Persentase Laki Laki</TableTh>
<TableTh>Persentase Perempuan</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>{item.laki}</Text>
</Box>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"}>{item.perempuan}</Text>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"}>{item.percentLaki}%</Text>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"}>{item.percentPerempuan}%</Text>
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/landing-page/indeks-kepuasan-masyarakat/list-gender-stat/${item.id}`)}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
</Stack>
</Paper>
</Box>
)
}
export default ListGenderStat;

View File

@@ -0,0 +1,122 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan-masyarakat';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput } 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 EditListSurvey() {
const editState = useProxy(indeksKepuasanState.surveyState)
const params = useParams()
const router = useRouter()
const [formData, setFormData] = useState({
title: editState.edit.form.title || '',
totalRespondents: editState.edit.form.totalRespondents || 0,
averageScore: editState.edit.form.averageScore || 0,
})
useEffect(() => {
const loadSurvey = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await editState.edit.load(id); // akses langsung, bukan dari proxy
if (data) {
setFormData({
title: data.title || '',
totalRespondents: data.totalRespondents || 0,
averageScore: data.averageScore || 0,
});
}
} catch (error) {
console.error("Error loading list survey:", error);
toast.error("Gagal memuat data list survey");
}
};
loadSurvey();
}, [params?.id]);
const handleSubmit = async () => {
try {
// edit global state with form data
editState.edit.form = {
...editState.edit.form,
title: formData.title,
totalRespondents: formData.totalRespondents,
averageScore: formData.averageScore,
};
await editState.edit.update();
toast.success("list survey berhasil diperbarui!");
router.push("/admin/landing-page/indeks-kepuasan-masyarakat/list-survey");
} catch (error) {
console.error("Error updating list survey:", error);
toast.error("Terjadi kesalahan saat memperbarui list survey");
}
};
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"}>Edit List Survey</Text>
<Paper>
<Stack gap={"xs"}>
<TextInput
value={formData.title}
onChange={(val) => {
setFormData({
...formData,
title: val.target.value
})
}}
label={<Text fw={"bold"} fz={"sm"}>Judul</Text>}
placeholder='Masukkan judul'
/>
<TextInput
value={formData.totalRespondents}
onChange={(val) => {
setFormData({
...formData,
totalRespondents: Number(val.target.value)
})
}}
label={<Text fw={"bold"} fz={"sm"}>Total Responden</Text>}
placeholder='Masukkan total responden'
/>
<TextInput
value={formData.averageScore}
onChange={(val) => {
setFormData({
...formData,
averageScore: Number(val.target.value)
})
}}
label={<Text fw={"bold"} fz={"sm"}>Skor Rata-rata</Text>}
placeholder='Masukkan skor'
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Stack>
</Paper>
</Box>
);
}
export default EditListSurvey;

View File

@@ -0,0 +1,109 @@
'use client'
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan-masyarakat';
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 DetailKegiatanDesa() {
const detailState = useProxy(indeksKepuasanState.surveyState)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter()
useShallowEffect(() => {
detailState.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
detailState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/landing-page/indeks-kepuasan-masyarakat/list-survey")
}
}
if (!detailState.findUnique.data) {
return (
<Stack py={10}>
<Skeleton 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 List Survey</Text>
{detailState.findUnique.data ? (
<Paper key={detailState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Judul</Text>
<Text fz={"lg"}>{detailState.findUnique.data?.title}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Total Responden</Text>
<Text fz={"lg"}>{detailState.findUnique.data?.totalRespondents}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Rata-rata Skor</Text>
<Text fz={"lg"}>{detailState.findUnique.data?.averageScore}</Text>
</Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (detailState.findUnique.data) {
setSelectedId(detailState.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={detailState.delete.loading || !detailState.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (detailState.findUnique.data) {
router.push(`/admin/landing-page/indeks-kepuasan-masyarakat/list-survey/${detailState.findUnique.data.id}/edit`);
}
}}
disabled={!detailState.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 list survey ini?'
/>
</Box>
);
}
export default DetailKegiatanDesa;

View File

@@ -0,0 +1,78 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan-masyarakat';
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 { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
function CreateListSurvey() {
const router = useRouter();
const stateCreate = useProxy(indeksKepuasanState.surveyState)
useEffect(() => {
stateCreate.findMany.load();
}, []);
const resetForm = () => {
stateCreate.create.form = {
title: "",
totalRespondents: 0,
averageScore: 0,
};
};
const handleSubmit = async () => {
await stateCreate.create.create();
resetForm();
router.push("/admin/landing-page/indeks-kepuasan-masyarakat/list-survey")
}
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}>Create List Survey</Title>
<TextInput
value={stateCreate.create.form.title}
onChange={(val) => {
stateCreate.create.form.title = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Judul</Text>}
placeholder='Masukkan judul'
/>
<TextInput
value={stateCreate.create.form.totalRespondents}
onChange={(val) => {
stateCreate.create.form.totalRespondents = Number(val.target.value);
}}
label={<Text fw={"bold"} fz={"sm"}>Total Responden</Text>}
placeholder='Masukkan tahun'
/>
<TextInput
value={stateCreate.create.form.averageScore}
onChange={(val) => {
stateCreate.create.form.averageScore = Number(val.target.value);
}}
label={<Text fw={"bold"} fz={"sm"}>Skor</Text>}
placeholder='Masukkan skor'
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreateListSurvey;

View File

@@ -0,0 +1,103 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import indeksKepuasanState from '../../../_state/landing-page/indeks-kepuasan-masyarakat';
import JudulList from '../../../_com/judulList';
function ListSurveyLandingPage() {
const [search, setSearch] = useState('');
return (
<Box>
<HeaderSearch
title='List Survey'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListSurvey search={search} />
</Box>
);
}
function ListSurvey({ search }: { search: string }) {
const listState = useProxy(indeksKepuasanState.surveyState)
const router = useRouter();
useEffect(() => {
listState.findMany.load()
}, [])
const filteredData = (listState.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.title.toLowerCase().includes(keyword) ||
item.totalRespondents.toString().includes(keyword) ||
item.averageScore.toString().includes(keyword)
);
});
if (!listState.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<JudulList
title='List Survey'
href='/admin/landing-page/indeks-kepuasan-masyarakat/list-survey/create'
/>
<Box style={{ overflowX: 'auto' }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
<TableThead>
<TableTr>
<TableTh>Judul</TableTh>
<TableTh>Total Responden</TableTh>
<TableTh>Skor Rata-rata</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>{item.title}</Text>
</Box>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"}>{item.totalRespondents}</Text>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"}>{item.averageScore}</Text>
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/landing-page/indeks-kepuasan-masyarakat/list-survey/${item.id}`)}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
</Stack>
</Paper>
</Box>
)
}
export default ListSurveyLandingPage;

View File

@@ -1,11 +0,0 @@
import React from 'react';
function Page() {
return (
<div>
Indeks Kepuasan MAsyarakat
</div>
);
}
export default Page;

View File

@@ -1,23 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Group, Stack, Text, Textarea, Title } from '@mantine/core';
function Page() {
return (
<Box>
<Stack gap={"xs"}>
<Title order={2}>Layanan</Title>
<Textarea
label={<Text>Deskripsi</Text>}
placeholder='tambah deskripsi'
/>
<Group>
<Button
bg={colors['blue-button']} fz={'md'}>Submit</Button>
</Group>
</Stack>
</Box>
);
}
export default Page;

View File

@@ -1,11 +0,0 @@
import React from 'react';
function Page() {
return (
<div>
Penghargaan
</div>
);
}
export default Page;

View File

@@ -1,11 +0,0 @@
import React from 'react';
function Page() {
return (
<div>
Potensi
</div>
);
}
export default Page;

View File

@@ -0,0 +1,62 @@
/* 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 LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter()
const pathname = usePathname()
const tabs = [
{
label: "List Prestasi Desa",
value: "listPrestasiDesa",
href: "/admin/landing-page/prestasi-desa/list-prestasi-desa"
},
{
label: "Kategori Prestasi Desa",
value: "kategoriPrestasiDesa",
href: "/admin/landing-page/prestasi-desa/kategori-prestasi-desa"
},
];
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}>Prestasi Desa</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 LayoutTabs;

View File

@@ -0,0 +1,98 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import prestasiState from '@/app/admin/(dashboard)/_state/landing-page/prestasi-desa';
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 EditKategoriPrestasi() {
const router = useRouter();
const params = useParams();
const id = params?.id as string;
const stateKategori = useProxy(prestasiState.kategoriPrestasi);
const [formData, setFormData] = useState({
name: "",
});
useEffect(() => {
const loadKategoriprestasi = async () => {
if (!id) return;
try {
const data = await stateKategori.edit.load(id);
if (data) {
// pastikan id-nya masuk ke state edit
stateKategori.edit.id = id;
setFormData({
name: data.name || '',
});
}
} catch (error) {
console.error("Error loading kategori prestasi desa:", error);
toast.error("Gagal memuat data kategori prestasi desa");
}
};
loadKategoriprestasi();
}, [id]);
const handleSubmit = async () => {
try {
if (!formData.name.trim()) {
toast.error('Nama kategori prestasi desa tidak boleh kosong');
return;
}
stateKategori.edit.form = {
name: formData.name.trim(),
};
// Safety check tambahan: pastikan ID tidak kosong
if (!stateKategori.edit.id) {
stateKategori.edit.id = id; // fallback
}
const success = await stateKategori.edit.update();
if (success) {
router.push("/admin/landing-page/prestasi-desa/kategori-prestasi-desa");
}
} catch (error) {
console.error("Error updating kategori prestasi desa:", error);
// toast akan ditampilkan dari fungsi update
}
};
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 Kategori Prestasi Desa</Title>
<TextInput
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Prestasi Desa</Text>}
placeholder='Masukkan nama kategori prestasi desa'
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditKategoriPrestasi;

View File

@@ -0,0 +1,62 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import prestasiState from '@/app/admin/(dashboard)/_state/landing-page/prestasi-desa';
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 { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
function CreateKategoriPrestasi() {
const router = useRouter();
const stateKategori = useProxy(prestasiState.kategoriPrestasi)
useEffect(() => {
stateKategori.findMany.load();
}, []);
const resetForm = () => {
stateKategori.create.form = {
name: "",
};
}
const handleSubmit = async () => {
await stateKategori.create.create();
resetForm();
router.push("/admin/landing-page/prestasi-desa/kategori-prestasi-desa")
}
return (
<Box>
<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}>Create Kategori Prestasi Desa</Title>
<TextInput
value={stateKategori.create.form.name}
onChange={(val) => {
stateKategori.create.form.name = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Prestasi Desa</Text>}
placeholder='Masukkan nama kategori prestasi desa'
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
</Box>
);
}
export default CreateKategoriPrestasi;

View File

@@ -0,0 +1,112 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import prestasiState from '../../../_state/landing-page/prestasi-desa';
function KategoriPrestasiDesa() {
const [search, setSearch] = useState("")
return (
<Box>
<HeaderSearch
title='Kategori Prestasi Desa'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListKategoriPrestasi search={search} />
</Box>
);
}
function ListKategoriPrestasi({ search }: { search: string }) {
const stateKategori = useProxy(prestasiState.kategoriPrestasi)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter()
const handleHapus = () => {
if (selectedId) {
stateKategori.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
}
}
useShallowEffect(() => {
stateKategori.findMany.load()
}, [])
const filteredData = (stateKategori.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword)
);
});
if (!stateKategori.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Kategori Prestasi Desa'
href='/admin/landing-page/prestasi-desa/kategori-prestasi-desa/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Kategori</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>
<Button color="green" onClick={() => router.push(`/admin/landing-page/prestasi-desa/kategori-prestasi-desa/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<Button color="red" onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconX size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus kategori prestasi desa ini?'
/>
</Box>
);
}
export default KategoriPrestasiDesa

View File

@@ -0,0 +1,12 @@
import React from 'react';
import LayoutTabs from './_lib/layoutTabs';
function Layout({children} : {children: React.ReactNode}) {
return (
<LayoutTabs>
{children}
</LayoutTabs>
);
}
export default Layout;

View File

@@ -0,0 +1,230 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import { useProxy } from 'valtio/utils';
import { Box, Button, Group, Image, Paper, Select, Stack, Text, TextInput } from '@mantine/core';
import { IconArrowBack, IconFile, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import colors from '@/con/colors';
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import prestasiState from '@/app/admin/(dashboard)/_state/landing-page/prestasi-desa';
import ApiFetch from '@/lib/api-fetch';
import { Dropzone } from '@mantine/dropzone';
import { toast } from 'react-toastify';
interface FormPrestasiDesa {
name: string;
deskripsi: string;
kategoriId: string;
imageId: string;
}
function EditPrestasiDesa() {
const editState = useProxy(prestasiState.prestasiDesa)
const [previewFile, setPreviewFile] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const params = useParams()
const router = useRouter()
const [formData, setFormData] = useState<FormPrestasiDesa>({
name: '',
deskripsi: '',
kategoriId: '',
imageId: '',
})
useEffect(() => {
const loadDesaAntiKorupsi = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await editState.edit.load(id);
if (data) {
// ⬇️ FIX PENTING: tambahkan ini
editState.edit.id = id;
editState.edit.form = {
name: data.name,
deskripsi: data.deskripsi,
kategoriId: data.kategoriId,
imageId: data.imageId,
};
setFormData({
name: data.name,
deskripsi: data.deskripsi,
kategoriId: data.kategoriId,
imageId: data.imageId,
});
if (data?.image?.link) {
setPreviewFile(data.image.link)
}
}
} catch (error) {
console.error("Error loading prestasi desa:", error);
toast.error("Gagal memuat data prestasi desa");
}
}
loadDesaAntiKorupsi();
}, [params?.id]);
const handleSubmit = async () => {
try {
// Update global state with form data
editState.edit.form = {
...editState.edit.form,
name: formData.name,
deskripsi: formData.deskripsi,
kategoriId: formData.kategoriId || '',
imageId: formData.imageId // Keep existing imageId if not changed
};
// Jika ada file baru, upload
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");
}
// Update imageId in global state
editState.edit.form.imageId = uploaded.id;
}
await editState.edit.update();
toast.success("prestasi desa berhasil diperbarui!");
router.push("/admin/landing-page/prestasi-desa/list-prestasi-desa");
} catch (error) {
console.error("Error updating prestasi desa:", error);
toast.error("Terjadi kesalahan saat memperbarui prestasi desa");
}
};
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"}>Edit List Prestasi Desa</Text>
{editState.findUnique.data ? (
<Paper key={editState.findUnique.data.id}>
<Stack gap={"xs"}>
<TextInput
value={formData.name}
onChange={(val) => {
setFormData({
...formData,
name: val.target.value
})
}}
label={<Text fw={"bold"} fz={"sm"}>Judul</Text>}
placeholder='Masukkan judul'
/>
<Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
<EditEditor
value={formData.deskripsi}
onChange={(val) => {
setFormData({
...formData,
deskripsi: val
})
}}
/>
</Box>
<Select
value={formData.kategoriId}
onChange={(val) => {
setFormData({
...formData,
kategoriId: val ?? ""
})
}}
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>}
placeholder="Pilih kategori"
data={
prestasiState.kategoriPrestasi.findMany.data?.map((v) => ({
value: v.id,
label: v.name,
})) || []
}
/>
<Box>
<Text fz={"md"} fw={"bold"}>File Image</Text>
<Stack gap={"xs"}>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewFile(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{
'image/*': ['.png', '.jpg', '.jpeg', '.gif', '.webp'],
}}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconFile size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag file ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format image
</Text>
</div>
</Group>
</Dropzone>
<Box>
<Text fw={"bold"} fz={"lg"}>Image</Text>
{previewFile ? (
<Image
alt=''
src={previewFile}
width="100%"
height="500px"
style={{ border: "1px solid #ccc", borderRadius: "8px" }}
/>
) : (
<Text>Tidak ada image tersedia</Text>
)}
</Box>
</Stack>
</Box>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
</Box>
);
}
export default EditPrestasiDesa;

View File

@@ -0,0 +1,122 @@
'use client'
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import prestasiState from '@/app/admin/(dashboard)/_state/landing-page/prestasi-desa';
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 DetailPrestasiDesa() {
const detailState = useProxy(prestasiState.prestasiDesa)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter()
useShallowEffect(() => {
detailState.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
detailState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/landing-page/prestasi-desa/list-prestasi-desa")
}
}
if (!detailState.findUnique.data) {
return (
<Stack py={10}>
<Skeleton 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 List Prestasi Desa</Text>
{detailState.findUnique.data ? (
<Paper key={detailState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Judul</Text>
<Text fz={"lg"}>{detailState.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: detailState.findUnique.data?.deskripsi }} />
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Kategori</Text>
<Text fz={"lg"}>{detailState.findUnique.data?.kategori?.name}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Image</Text>
{detailState.findUnique.data?.image?.link ? (
<iframe
src={detailState.findUnique.data.image.link}
width="100%"
height="500px"
style={{ border: "1px solid #ccc", borderRadius: "8px" }}
/>
) : (
<Text>Tidak ada image tersedia</Text>
)}
</Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (detailState.findUnique.data) {
setSelectedId(detailState.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={detailState.delete.loading || !detailState.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (detailState.findUnique.data) {
router.push(`/admin/landing-page/prestasi-desa/list-prestasi-desa/${detailState.findUnique.data.id}/edit`);
}
}}
disabled={!detailState.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 prestasi desa ini?'
/>
</Box>
);
}
export default DetailPrestasiDesa;

View File

@@ -0,0 +1,167 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import prestasiState from '@/app/admin/(dashboard)/_state/landing-page/prestasi-desa';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconImageInPicture, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function CreatePrestasiDesa() {
const router = useRouter();
const stateCreate = useProxy(prestasiState.prestasiDesa)
const [previewFile, setPreviewFile] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
useEffect(() => {
stateCreate.findMany.load();
prestasiState.kategoriPrestasi.findMany.load();
}, []);
const resetForm = () => {
stateCreate.create.form = {
name: "",
deskripsi: "",
kategoriId: "",
imageId: "",
};
setFile(null);
setPreviewFile(null);
};
const handleSubmit = async () => {
if (!file) {
return toast.warn("Pilih file image terlebih dahulu");
}
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
})
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal mengupload image");
}
stateCreate.create.form.imageId = uploaded.id;
await stateCreate.create.create();
resetForm();
router.push("/admin/landing-page/prestasi-desa/list-prestasi-desa")
}
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}>Create Prestasi Desa</Title>
<Box>
<Text fz={"md"} fw={"bold"}>File Image</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewFile(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{
'application/*': ['.jpg', '.jpeg', '.png'],
}}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconImageInPicture size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag file ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format image
</Text>
</div>
</Group>
</Dropzone>
<Box>
<Text fw={"bold"} fz={"lg"}>Image</Text>
{previewFile ? (
<iframe
src={previewFile}
width="100%"
height="500px"
style={{ border: "1px solid #ccc", borderRadius: "8px" }}
/>
) : (
<Text>Tidak ada image tersedia</Text>
)}
</Box>
</Box>
</Box>
<TextInput
value={stateCreate.create.form.name}
onChange={(val) => {
stateCreate.create.form.name = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Judul</Text>}
placeholder='Masukkan judul'
/>
<Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
<CreateEditor
value={stateCreate.create.form.deskripsi}
onChange={(val) => {
stateCreate.create.form.deskripsi = val;
}}
/>
</Box>
<Select
value={stateCreate.create.form.kategoriId}
onChange={(val) => {
stateCreate.create.form.kategoriId = val ?? "";
}}
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>}
placeholder="Pilih kategori"
data={
prestasiState.kategoriPrestasi.findMany.data?.map((v) => ({
value: v.id,
label: v.name,
})) || []
}
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreatePrestasiDesa;

View File

@@ -0,0 +1,100 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import prestasiState from '../../../_state/landing-page/prestasi-desa';
function ListPrestasiDesa() {
const [search, setSearch] = useState('');
return (
<Box>
<HeaderSearch
title='List Prestasi Desa'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListPrestasi search={search} />
</Box>
);
}
function ListPrestasi({ search }: { search: string }) {
const listState = useProxy(prestasiState.prestasiDesa)
const router = useRouter();
useEffect(() => {
listState.findMany.load()
}, [])
const filteredData = (listState.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.deskripsi.toLowerCase().includes(keyword) ||
item.kategori?.name?.toLowerCase().includes(keyword)
);
});
if (!listState.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<JudulList
title='List Prestasi Desa'
href='/admin/landing-page/prestasi-desa/list-prestasi-desa/create'
/>
<Box style={{ overflowX: 'auto' }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
<TableThead>
<TableTr>
<TableTh>Nama Prestasi Desa</TableTh>
<TableTh>Deskripsi Prestasi Desa</TableTh>
<TableTh>Kategori Prestasi Desa</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>{item.name}</Text>
</Box>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd>
<TableTd>{item.kategori?.name}</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/landing-page/prestasi-desa/list-prestasi-desa/${item.id}`)}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
</Stack>
</Paper>
</Box>
)
}
export default ListPrestasiDesa;

View File

@@ -1,11 +0,0 @@
import React from 'react';
function Page() {
return (
<div>
Prestasi Desa
</div>
);
}
export default Page;

View File

@@ -0,0 +1,67 @@
/* 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 LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter()
const pathname = usePathname()
const tabs = [
{
label: "Program Inovasi",
value: "program-inovasi",
href: "/admin/landing-page/profile/program-inovasi"
},
{
label: "Pejabat Desa",
value: "pejabat-desa",
href: "/admin/landing-page/profile/pejabat-desa"
},
{
label: "Media Sosial",
value: "media-sosial",
href: "/admin/landing-page/profile/media-sosial"
},
];
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}>Profile</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 LayoutTabs;

View File

@@ -0,0 +1,11 @@
import React from 'react';
import LayoutTabs from './_lib/layoutTabs';
function Layout({ children }: { children: React.ReactNode }) {
return (
<LayoutTabs>
{children}
</LayoutTabs>
);
}
export default Layout;

View File

@@ -0,0 +1,174 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditMediaSosial() {
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial)
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: stateMediaSosial.update.form.name || "",
iconUrl: stateMediaSosial.update.form.iconUrl || "",
imageId: stateMediaSosial.update.form.imageId || ""
})
useEffect(() => {
const id = params?.id as string;
if (!id) return;
const loadMediaSosial = async () => {
try {
const data = await stateMediaSosial.update.load(id);
if (data) {
setFormData({
name: data.name || "",
iconUrl: data.iconUrl || "",
imageId: data.imageId || "",
});
// Tampilkan preview gambar
if (data.image?.link) {
setPreviewImage(data.image.link);
}
}
} catch (error) {
console.error("Error loading program inovasi:", error);
toast.error(
error instanceof Error ? error.message : "Gagal mengambil data program inovasi"
);
}
}
loadMediaSosial();
}, [params?.id]);
const handleSubmit = async () => {
try {
stateMediaSosial.update.form = {
...stateMediaSosial.update.form,
name: formData.name,
iconUrl: formData.iconUrl,
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");
}
// Update imageId in global state
stateMediaSosial.update.form.imageId = uploaded.id;
}
await stateMediaSosial.update.update();
toast.success("Media Sosial berhasil diperbarui!");
router.push("/admin/landing-page/profile/media-sosial");
} catch (error) {
console.error("Error updating media sosial:", error);
toast.error("Terjadi kesalahan saat memperbarui media sosial");
}
};
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 Media Sosial</Title>
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box>
<TextInput
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Nama Media Sosial / Nama Kontak</Text>}
placeholder='Masukkan nama media sosial'
/>
<TextInput
value={formData.iconUrl}
onChange={(e) => setFormData({ ...formData, iconUrl: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Icon URL / No Telephone</Text>}
placeholder='Masukkan icon url'
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditMediaSosial;

View File

@@ -0,0 +1,107 @@
'use client'
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
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 DetailMediaSosial() {
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter();
useShallowEffect(() => {
stateMediaSosial.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
stateMediaSosial.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/landing-page/profile/media-sosial")
}
}
if (!stateMediaSosial.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 w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Media Sosial</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Media Sosial / Nama Kontak</Text>
<Text fz={"lg"}>{stateMediaSosial.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Icon URL / No Telephone</Text>
<Text fz={"lg"}>{stateMediaSosial.findUnique.data?.iconUrl}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
<Box w={100} h={100}>
<Image src={stateMediaSosial.findUnique.data?.image?.link} alt="gambar" />
</Box>
</Box>
<Box>
<Flex gap={"xs"}>
<Button
onClick={() => {
if (stateMediaSosial.findUnique.data) {
setSelectedId(stateMediaSosial.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={!stateMediaSosial.findUnique.data}
color="red">
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (stateMediaSosial.findUnique.data) {
router.push(`/admin/landing-page/profile/media-sosial/${stateMediaSosial.findUnique.data.id}/edit`);
}
}}
disabled={!stateMediaSosial.findUnique.data}
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 media sosial ini?"
/>
</Box>
);
}
export default DetailMediaSosial;

View File

@@ -0,0 +1,148 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import profileLandingPageState from '../../../../_state/landing-page/profile';
function CreateMediaSosial() {
const router = useRouter();
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial)
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
useEffect(() => {
stateMediaSosial.findMany.load();
}, []);
const resetForm = () => {
stateMediaSosial.create.form = {
name: "",
imageId: "",
iconUrl: "",
};
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 mengupload file");
}
stateMediaSosial.create.form.imageId = uploaded.id;
await stateMediaSosial.create.create();
resetForm();
router.push("/admin/landing-page/profile/media-sosial")
}
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}>Create Media Sosial</Title>
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box>
<TextInput
value={stateMediaSosial.create.form.name || ''}
onChange={(val) => {
stateMediaSosial.create.form.name = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Media Sosial / Nama Kontak</Text>}
placeholder='Masukkan nama media sosial / nama kontak'
/>
<TextInput
value={stateMediaSosial.create.form.iconUrl || ''}
onChange={(val) => {
stateMediaSosial.create.form.iconUrl = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Link Media Sosial / No Telephone</Text>}
placeholder='Masukkan link media sosial / no telephone'
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreateMediaSosial;

View File

@@ -0,0 +1,93 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Image, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import profileLandingPageState from '../../../_state/landing-page/profile';
function MediaSosial() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Media Sosial'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListMediaSosial search={search} />
</Box>
);
}
function ListMediaSosial({ search }: { search: string }) {
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial)
const router = useRouter();
useShallowEffect(() => {
stateMediaSosial.findMany.load()
}, [])
const filteredData = (stateMediaSosial.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.iconUrl?.toLowerCase().includes(keyword)
);
});
if (!stateMediaSosial.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Media Sosial'
href='/admin/landing-page/profile/media-sosial/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Media Sosial / Nama Kontak</TableTh>
<TableTh>Image</TableTh>
<TableTh>Icon URL / No Telephone</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>
<Box w={50} h={50}>
<Image src={item.image?.link} alt={item.name} />
</Box>
</TableTd>
<TableTd>{item.iconUrl}</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/landing-page/profile/media-sosial/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>
</Box>
);
}
export default MediaSosial;

View File

@@ -1,11 +0,0 @@
import React from 'react';
function Page() {
return (
<div>
Profile
</div>
);
}
export default Page;

View File

@@ -0,0 +1,270 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import colors from '@/con/colors';
import { Alert, Box, Button, Center, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
import ApiFetch from '@/lib/api-fetch';
import { Dropzone } from '@mantine/dropzone';
import { IconAlertCircle, IconArrowBack, IconImageInPicture, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { toast } from 'react-toastify';
function EditPejabatDesa() {
const allState = useProxy(profileLandingPageState.pejabatDesa);
const params = useParams();
const router = useRouter();
// UI States
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
// Load data on mount
useEffect(() => {
const loadData = async () => {
const id = params?.id as string;
if (!id) {
toast.error("ID tidak valid");
router.push("/admin/landing-page/profile/pejabat-desa");
return;
}
try {
const profileData = await profileLandingPageState.pejabatDesa.findUnique.load(id);
profileLandingPageState.pejabatDesa.edit.initialize(profileData);
if (profileData && profileData.image?.link) {
setPreviewImage(profileData.image.link);
}
} catch (error) {
console.error("Error loading profile:", error);
toast.error("Gagal memuat data profile");
}
};
loadData();
return () => {
profileLandingPageState.pejabatDesa.edit.reset(); // cleanup form
};
}, [params?.id, router]);
const handleFieldChange = (field: string, value: string) => {
profileLandingPageState.pejabatDesa.edit.updateField(field as any, value);
};
const handleFileChange = (newFile: File | null) => {
if (!newFile) {
setFile(null);
return;
}
setFile(newFile);
const reader = new FileReader();
reader.onload = (event) => {
setPreviewImage(event.target?.result as string);
};
reader.readAsDataURL(newFile);
};
const handleSubmit = async () => {
if (isSubmitting || !profileLandingPageState.pejabatDesa.edit.form.name.trim()) {
toast.error("Nama wajib diisi");
return;
}
setIsSubmitting(true);
try {
// Upload file jika ada
if (file) {
const uploadResponse = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = uploadResponse.data?.data;
if (!uploaded?.id) {
toast.error("Gagal upload gambar");
return;
}
profileLandingPageState.pejabatDesa.edit.form.imageId = uploaded.id;
}
// Submit form
const success = await profileLandingPageState.pejabatDesa.edit.submit();
if (success) {
toast.success("Berhasil menyimpan perubahan");
router.push("/admin/landing-page/profile/pejabat-desa");
}
} catch (error) {
console.error("Error submitting form:", error);
toast.error("Gagal menyimpan profile");
} finally {
setIsSubmitting(false);
}
};
const handleBack = () => {
router.back();
};
// Loading state
if (allState.edit.loading) {
return (
<Box>
<Center h={400}>
<Text>Memuat data profile...</Text>
</Center>
</Box>
);
}
// Error state
if (allState.edit.error) {
return (
<Box>
<Stack gap="md">
<Button variant="subtle" onClick={handleBack}>
<IconArrowBack color={colors['blue-button']} size={20} />
</Button>
<Alert icon={<IconAlertCircle size={16} />} color="red">
<Text fw="bold">Error</Text>
<Text>{allState.edit.error}</Text>
</Alert>
</Stack>
</Box>
);
}
return (
<Box>
<Stack gap="xs">
<Box>
<Button variant="subtle" onClick={handleBack}>
<IconArrowBack color={colors['blue-button']} size={20} />
</Button>
</Box>
<Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p="md" radius={10}>
<Stack gap="xs">
<Title order={3}>Edit Profile Pejabat Desa</Title>
{/* Nama Field */}
<TextInput
label={<Text fw="bold">Nama Perbekel</Text>}
placeholder="Masukkan nama perbekel"
value={allState.edit.form.name}
onChange={(e) => handleFieldChange('name', e.currentTarget.value)}
error={!allState.edit.form.name && "Nama wajib diisi"}
/>
{/* Posisi Field */}
<TextInput
label={<Text fw="bold">Posisi</Text>}
placeholder="Masukkan posisi"
value={allState.edit.form.position}
onChange={(e) => handleFieldChange('position', e.currentTarget.value)}
error={!allState.edit.form.position && "Posisi wajib diisi"}
/>
{/* File Upload */}
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box>
<Dropzone
onDrop={(files) => handleFileChange(files[0])}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box>
{/* Preview Gambar */}
<Box>
<Text fz="sm" fw="bold" mb="xs">Preview Gambar</Text>
{previewImage ? (
<Image alt="Profile preview" src={previewImage} w={200} h={200} fit="cover" radius="md" />
) : (
<Center w={200} h={200} bg="gray.2">
<Stack align="center" gap="xs">
<IconImageInPicture size={48} color="gray" />
<Text size="sm" c="gray">Tidak ada gambar</Text>
</Stack>
</Center>
)}
</Box>
{/* Submit Button */}
<Group>
<Button
bg={colors['blue-button']}
onClick={handleSubmit}
loading={isSubmitting || allState.edit.loading}
disabled={!allState.edit.form.name}
>
{isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'}
</Button>
<Button
variant="outline"
onClick={handleBack}
disabled={isSubmitting || allState.edit.loading}
>
Batal
</Button>
</Group>
</Stack>
</Paper>
</Box>
</Stack>
</Box>
);
}
export default EditPejabatDesa;

View File

@@ -0,0 +1,104 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Center, Divider, Grid, GridCol, Image, Paper, Skeleton, Stack, Text, Title } 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 profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
function Page() {
const router = useRouter()
const allList = useProxy(profileLandingPageState.pejabatDesa)
useShallowEffect(() => {
allList.findUnique.load("edit") // Assuming "1" is your default ID, adjust as needed
}, [])
if (!allList.findUnique.data) {
return <Stack>
<Skeleton radius={10} h={800} />
</Stack>
}
const dataArray = Array.isArray(allList.findUnique.data)
? allList.findUnique.data
: [allList.findUnique.data];
return (
<Paper bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Grid>
<GridCol span={{ base: 12, md: 11 }}>
<Title order={3}>Preview Pejabat Desa</Title>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button bg={colors['blue-button']} onClick={() => router.push(`/admin/landing-page/profile/pejabat-desa/${allList.findUnique.data?.id}`)}>
<IconEdit size={16} />
</Button>
</GridCol>
</Grid>
{dataArray.map((item) => (
<Box key={item.id} >
<Paper p={"xl"} bg={colors['BG-trans']}>
<Box px={{ base: "md", md: 100 }}>
<Grid>
<GridCol span={{ base: 12, md: 12 }}>
<Center>
<Image src={"/darmasaba-icon.png"} w={{ base: 100, md: 150 }} alt='' />
</Center>
</GridCol>
<GridCol span={{ base: 12, md: 12 }}>
<Text ta={"center"} fz={{ base: "1.2rem", md: "1.8rem" }} fw={'bold'}>PROFIL PIMPINAN BADAN PUBLIK DESA DARMASABA </Text>
</GridCol>
</Grid>
</Box>
<Divider my={"md"} color={colors['blue-button']} />
{/* biodata perbekel */}
<Box px={{ base: 0, md: 50 }} pb={30}>
<Box pb={20} px={{ base: 0, md: 50 }}>
<Paper bg={colors['BG-trans']} w={{ base: "100%", md: "100%" }}>
<Stack gap={0}>
<Center>
<Image
pt={{ base: 0, md: 90 }}
src={item.image?.link || "/perbekel.png"}
w={{ base: 250, md: 350 }}
alt='Foto Profil PPID'
onError={(e) => {
e.currentTarget.src = "/perbekel.png";
}}
/>
</Center>
<Paper
bg={colors['blue-button']}
py={20}
className="glass3"
px={{ base: 10, md: 10 }}
>
<Text ta={"center"} c={colors['white-1']} fw={"bolder"} fz={{ base: "1.2rem", md: "1.6rem" }}>
{item.name}
</Text>
</Paper>
</Stack>
</Paper>
</Box>
<Box pt={10}>
<Box>
<Text fz={{ base: "1.125rem", md: "1.6rem" }} fw={'bold'}>Position</Text>
<Text fz={{ base: "1rem", md: "1.5rem" }} ta={"justify"}>{item.position}</Text>
</Box>
</Box>
</Box>
</Paper>
</Box>
))}
</Stack>
</Paper>
)
}
export default Page;

View File

@@ -0,0 +1,182 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditProgramInovasi() {
const stateProgramInovasi = useProxy(profileLandingPageState.programInovasi)
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: stateProgramInovasi.update.form.name || "",
description: stateProgramInovasi.update.form.description || "",
imageId: stateProgramInovasi.update.form.imageId || "",
link: stateProgramInovasi.update.form.link || "",
})
useEffect(() => {
const id = params?.id as string;
if (!id) return;
const loadProgramInovasi = async () => {
try {
const data = await stateProgramInovasi.update.load(id);
if (data) {
setFormData({
name: data.name || "",
description: data.description || "",
imageId: data.imageId || "",
link: data.link || ""
});
// Tampilkan preview gambar
if (data.image?.link) {
setPreviewImage(data.image.link);
}
}
} catch (error) {
console.error("Error loading program inovasi:", error);
toast.error(
error instanceof Error ? error.message : "Gagal mengambil data program inovasi"
);
}
}
loadProgramInovasi();
}, [params?.id]);
const handleSubmit = async () => {
try {
stateProgramInovasi.update.form = {
...stateProgramInovasi.update.form,
name: formData.name,
description: formData.description,
imageId: formData.imageId,
link: formData.link,
}
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");
}
// Update imageId in global state
stateProgramInovasi.update.form.imageId = uploaded.id;
}
await stateProgramInovasi.update.update();
toast.success("Program Inovasi berhasil diperbarui!");
router.push("/admin/landing-page/profile/program-inovasi");
} catch (error) {
console.error("Error updating program inovasi:", error);
toast.error("Terjadi kesalahan saat memperbarui program inovasi");
}
};
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 Program Inovasi</Title>
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box>
<TextInput
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Nama Produk</Text>}
placeholder='Masukkan nama produk'
/>
<TextInput
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Deskripsi</Text>}
placeholder='Masukkan deskripsi'
/>
<TextInput
value={formData.link}
onChange={(e) => setFormData({ ...formData, link: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Link</Text>}
placeholder='Masukkan link'
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditProgramInovasi;

View File

@@ -0,0 +1,109 @@
'use client'
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
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 DetailProgramInovasi() {
const stateProgramInovasi = useProxy(profileLandingPageState.programInovasi)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter();
useShallowEffect(() => {
stateProgramInovasi.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
stateProgramInovasi.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/landing-page/profile/program-inovasi")
}
}
if (!stateProgramInovasi.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 w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Program Inovasi</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Program Inovasi</Text>
<Text fz={"lg"}>{stateProgramInovasi.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text>
<Text fz={"lg"}>{stateProgramInovasi.findUnique.data?.description}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Link</Text>
<Text fz={"lg"}>{stateProgramInovasi.findUnique.data?.link}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
<Image src={stateProgramInovasi.findUnique.data?.image?.link} alt="gambar" />
</Box>
<Box>
<Flex gap={"xs"}>
<Button
onClick={() => {
if (stateProgramInovasi.findUnique.data) {
setSelectedId(stateProgramInovasi.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={!stateProgramInovasi.findUnique.data}
color="red">
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (stateProgramInovasi.findUnique.data) {
router.push(`/admin/landing-page/profile/program-inovasi/${stateProgramInovasi.findUnique.data.id}/edit`);
}
}}
disabled={!stateProgramInovasi.findUnique.data}
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 program inovasi ini?"
/>
</Box>
);
}
export default DetailProgramInovasi;

View File

@@ -0,0 +1,158 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import profileLandingPageState from '../../../../_state/landing-page/profile';
function CreateProgramInovasi() {
const router = useRouter();
const stateProgramInovasi = useProxy(profileLandingPageState.programInovasi)
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
useEffect(() => {
stateProgramInovasi.findMany.load();
}, []);
const resetForm = () => {
stateProgramInovasi.create.form = {
name: "",
description: "",
imageId: "",
link: "",
};
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 mengupload file");
}
stateProgramInovasi.create.form.imageId = uploaded.id;
await stateProgramInovasi.create.create();
resetForm();
router.push("/admin/landing-page/profile/program-inovasi")
}
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}>Create Program Inovasi</Title>
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box>
<TextInput
value={stateProgramInovasi.create.form.name || ''}
onChange={(val) => {
stateProgramInovasi.create.form.name = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Program Inovasi</Text>}
placeholder='Masukkan nama program inovasi'
/>
<TextInput
value={stateProgramInovasi.create.form.description || ''}
onChange={(val) => {
stateProgramInovasi.create.form.description = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Deskripsi</Text>}
placeholder='Masukkan deskripsi'
/>
<TextInput
value={stateProgramInovasi.create.form.link || ''}
onChange={(val) => {
stateProgramInovasi.create.form.link = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Link</Text>}
placeholder='Masukkan link'
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreateProgramInovasi;

View File

@@ -0,0 +1,90 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import profileLandingPageState from '../../../_state/landing-page/profile';
function ProgramInovasi() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Program Inovasi'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListProgramInovasi search={search} />
</Box>
);
}
function ListProgramInovasi({ search }: { search: string }) {
const stateProgramInovasi = useProxy(profileLandingPageState.programInovasi)
const router = useRouter();
useShallowEffect(() => {
stateProgramInovasi.findMany.load()
}, [])
const filteredData = (stateProgramInovasi.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.description?.toLowerCase().includes(keyword) ||
item.link?.toLowerCase().includes(keyword)
);
});
if (!stateProgramInovasi.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Program Inovasi'
href='/admin/landing-page/profile/program-inovasi/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Program</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Link</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>{item.description}</TableTd>
<TableTd>{item.link}</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/landing-page/profile/program-inovasi/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>
</Box>
);
}
export default ProgramInovasi;

View File

@@ -0,0 +1,182 @@
/* eslint-disable react-hooks/exhaustive-deps */
"use client";
import sdgsDesa from "@/app/admin/(dashboard)/_state/landing-page/sdgs-desa";
import colors from "@/con/colors";
import ApiFetch from "@/lib/api-fetch";
import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title
} from "@mantine/core";
import { Dropzone } from "@mantine/dropzone";
import { IconArrowBack, IconImageInPicture, IconUpload, IconX } from "@tabler/icons-react";
import { useParams, useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { toast } from "react-toastify";
import { useProxy } from "valtio/utils";
function EditKolaborasiInovasi() {
const sdgsState = useProxy(sdgsDesa);
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: sdgsState.edit.form.name || '',
jumlah: sdgsState.edit.form.jumlah || '',
imageId: sdgsState.edit.form.imageId || ''
});
// Load sdgs desa by id saat pertama kali
useEffect(() => {
const loadKolaborasi = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await sdgsState.edit.load(id); // akses langsung, bukan dari proxy
if (data) {
setFormData({
name: data.name || '',
jumlah: data.jumlah || '',
imageId: data.imageId || '',
});
if (data.image) {
if (data?.image?.link) {
setPreviewImage(data.image.link);
}
}
}
} catch (error) {
console.error("Error loading sdgs desa:", error);
toast.error("Gagal memuat data sdgs desa");
}
};
loadKolaborasi();
}, [params?.id]);
const handleSubmit = async () => {
try {
// edit global state with form data
sdgsState.edit.form = {
...sdgsState.edit.form,
name: formData.name,
jumlah: formData.jumlah,
imageId: formData.imageId // Keep existing imageId if not changed
};
// Jika ada file baru, upload
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");
}
// edit imageId in global state
sdgsState.edit.form.imageId = uploaded.id;
}
await sdgsState.edit.update();
toast.success("sdgs desa berhasil diperbarui!");
router.push("/admin/landing-page/SDGs-Desa");
} catch (error) {
console.error("Error updating sdgs desa:", error);
toast.error("Terjadi kesalahan saat memperbarui sdgs desa");
}
};
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 SDGs Desa</Title>
<TextInput
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Nama</Text>}
placeholder="masukkan nama"
/>
<TextInput
value={formData.jumlah}
onChange={(e) => setFormData({ ...formData, jumlah: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Jumlah</Text>}
placeholder="masukkan jumlah"
/>
<Box>
<Text fz={"md"} fw={"bold"}>File Image</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{
'application/*': ['.jpeg', '.jpg', '.png', '.gif', '.webp', '.svg'],
}}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconImageInPicture size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag file ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format image
</Text>
</div>
</Group>
</Dropzone>
<Box>
<Text fw={"bold"} fz={"lg"}>Dokumen</Text>
{previewImage ? (
<iframe
src={previewImage}
width="100%"
height="500px"
style={{ border: "1px solid #ccc", borderRadius: "8px" }}
/>
) : (
<Text>Tidak ada image tersedia</Text>
)}
</Box>
</Box>
</Box>
<Button onClick={handleSubmit}>Simpan</Button>
</Stack>
</Paper>
</Box>
);
}
export default EditKolaborasiInovasi;

View File

@@ -0,0 +1,110 @@
'use client'
import { useProxy } from 'valtio/utils';
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 colors from '@/con/colors';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import sdgsDesa from '../../../_state/landing-page/sdgs-desa';
function DetailSDGSDesa() {
const sdgsState = useProxy(sdgsDesa)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter()
useShallowEffect(() => {
sdgsState.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
sdgsState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/landing-page/SDGs-Desa")
}
}
if (!sdgsState.findUnique.data) {
return (
<Stack py={10}>
<Skeleton 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 SDGS Desa</Text>
{sdgsState.findUnique.data ? (
<Paper key={sdgsState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Nama SDGS Desa</Text>
<Text fz={"lg"}>{sdgsState.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Jumlah</Text>
<Text fz={"lg"}>{sdgsState.findUnique.data?.jumlah}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
<Image w={{ base: 150, md: 150, lg: 150 }} src={sdgsState.findUnique.data?.image?.link} alt="gambar" />
</Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (sdgsState.findUnique.data) {
setSelectedId(sdgsState.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={sdgsState.delete.loading || !sdgsState.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (sdgsState.findUnique.data) {
router.push(`/admin/landing-page/SDGs-Desa/${sdgsState.findUnique.data.id}/edit`);
}
}}
disabled={!sdgsState.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 SDGS Desa ini?'
/>
</Box>
);
}
export default DetailSDGSDesa;

View File

@@ -0,0 +1,148 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconImageInPicture, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import sdgsDesa from '../../../_state/landing-page/sdgs-desa';
function CreateSDGsDesa() {
const router = useRouter();
const stateSDGSDesa = useProxy(sdgsDesa)
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
useEffect(() => {
stateSDGSDesa.findMany.load();
}, []);
const resetForm = () => {
stateSDGSDesa.create.form = {
name: "",
jumlah: "",
imageId: "",
};
setFile(null);
setPreviewImage(null);
};
const handleSubmit = async () => {
if (!file) {
return toast.warn("Pilih file image terlebih dahulu");
}
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
})
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal mengupload file");
}
stateSDGSDesa.create.form.imageId = uploaded.id;
await stateSDGSDesa.create.create();
resetForm();
router.push("/admin/landing-page/SDGs-Desa")
}
return (
<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}>Create SDGs Desa</Title>
<Box>
<Text fz={"md"} fw={"bold"}>File Image</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{
'application/*': ['.jpeg', '.jpg', '.png', '.gif', '.webp', '.svg'],
}}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconImageInPicture size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag file ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format image
</Text>
</div>
</Group>
</Dropzone>
<Box>
<Text fw={"bold"} fz={"lg"}>Dokumen</Text>
{previewImage ? (
<iframe
src={previewImage}
width="100%"
height="500px"
style={{ border: "1px solid #ccc", borderRadius: "8px" }}
/>
) : (
<Text>Tidak ada image tersedia</Text>
)}
</Box>
</Box>
</Box>
<TextInput
value={stateSDGSDesa.create.form.name}
onChange={(val) => {
stateSDGSDesa.create.form.name = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Judul</Text>}
placeholder='Masukkan judul'
/>
<TextInput
type='number'
value={stateSDGSDesa.create.form.jumlah}
onChange={(val) => {
stateSDGSDesa.create.form.jumlah = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Jumlah</Text>}
placeholder='Masukkan jumlah'
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreateSDGsDesa;

View File

@@ -1,11 +1,97 @@
import React from 'react';
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import sdgsDesa from '../../_state/landing-page/sdgs-desa';
import JudulList from '../../_com/judulList';
function Page() {
function SdgsDesa() {
const [search, setSearch] = useState('');
return (
<div>
SDGS Desa
</div>
<Box>
<HeaderSearch
title='SDGs Desa'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListSdgsDesa search={search} />
</Box>
);
}
export default Page;
function ListSdgsDesa({ search }: { search: string }) {
const listState = useProxy(sdgsDesa)
const router = useRouter();
useEffect(() => {
listState.findMany.load()
}, [])
const filteredData = (listState.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.jumlah.toLowerCase().includes(keyword)
)
});
if (!listState.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<JudulList
title='SDGs Desa'
href='/admin/landing-page/SDGs-Desa/create'
/>
<Box style={{ overflowX: 'auto' }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
<TableThead>
<TableTr>
<TableTh>Nama SDGs Desa</TableTh>
<TableTh>Jumlah SDGs Desa</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>{item.name}</Text>
</Box>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"}>{item.jumlah}</Text>
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/landing-page/SDGs-Desa/${item.id}`)}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
</Stack>
</Paper>
</Box>
)
}
export default SdgsDesa;

View File

@@ -0,0 +1,62 @@
/* 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 LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter()
const pathname = usePathname()
const tabs = [
{
label: "Kegiatan Desa",
value: "kegiatanDesa",
href: "/admin/lingkungan/gotong-royong/kegiatan-desa"
},
{
label: "Kategori Kegiatan",
value: "kategoriKegiatan",
href: "/admin/lingkungan/gotong-royong/kategori-kegiatan"
},
];
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}>Gotong Royong</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 LayoutTabs;

View File

@@ -1,68 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import React from 'react';
import { KeamananEditor } from '../../../keamanan/_com/keamananEditor';
function CreateGotongRoyong() {
const router = useRouter()
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 Gotong Royong</Title>
<TextInput
label={<Text fz="sm" fw="bold">Judul Gotong Royong</Text>}
placeholder="masukkan judul gotong royong"
/>
<TextInput
label={<Text fz="sm" fw="bold">Kategori Gotong Royong</Text>}
placeholder="masukkan kategori gotong royong"
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Gotong Royong</Text>
<KeamananEditor
showSubmit={false}
/>
</Box>
{/* <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 fz="sm" fw="bold">Gambar</Text>
<IconImageInPicture size={25} />
</Box>
<Button bg={colors['blue-button']} >
Simpan
</Button>
</Stack>
</Paper>
</Box>
);
}
export default CreateGotongRoyong;

View File

@@ -1,66 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Flex, Text, Image } from '@mantine/core';
import { IconArrowBack, IconX, IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import React from 'react';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailGotongRoyong() {
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 Gotong Royong</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Judul Gotong Royong</Text>
<Text fz={"lg"}>Test Judul</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Kategori Gotong Royong</Text>
<Text fz={"lg"}>Test Kategori</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi Gotong Royong</Text>
<Text fz={"lg"} >Test Deskripsi</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
<Image src={"/"} alt="gambar" />
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red">
<IconX size={20} />
</Button>
<Button onClick={() => router.push('/admin/lingkungan/gotong-royong/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 DetailGotongRoyong;

View File

@@ -1,68 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import React from 'react';
import { KeamananEditor } from '../../../keamanan/_com/keamananEditor';
function EditGotongRoyong() {
const router = useRouter()
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 Gotong Royong</Title>
<TextInput
label={<Text fz="sm" fw="bold">Judul Gotong Royong</Text>}
placeholder="masukkan judul gotong royong"
/>
<TextInput
label={<Text fz="sm" fw="bold">Kategori Gotong Royong</Text>}
placeholder="masukkan kategori gotong royong"
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Gotong Royong</Text>
<KeamananEditor
showSubmit={false}
/>
</Box>
{/* <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 fz="sm" fw="bold">Gambar</Text>
<IconImageInPicture size={25} />
</Box>
<Button bg={colors['blue-button']} >
Simpan
</Button>
</Stack>
</Paper>
</Box>
);
}
export default EditGotongRoyong;

View File

@@ -0,0 +1,98 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import gotongRoyongState from '@/app/admin/(dashboard)/_state/lingkungan/gotong-royong';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditKategoriKegiatan() {
const router = useRouter();
const params = useParams();
const id = params?.id as string;
const stateKategori = useProxy(gotongRoyongState.kategoriKegiatan);
const [formData, setFormData] = useState({
nama: "",
});
useEffect(() => {
const loadKategorikegiatan = async () => {
if (!id) return;
try {
const data = await stateKategori.edit.load(id);
if (data) {
// pastikan id-nya masuk ke state edit
stateKategori.edit.id = id;
setFormData({
nama: data.nama || '',
});
}
} catch (error) {
console.error("Error loading kategori kegiatan:", error);
toast.error("Gagal memuat data kategori kegiatan");
}
};
loadKategorikegiatan();
}, [id]);
const handleSubmit = async () => {
try {
if (!formData.nama.trim()) {
toast.error('Nama kategori kegiatan tidak boleh kosong');
return;
}
stateKategori.edit.form = {
nama: formData.nama.trim(),
};
// Safety check tambahan: pastikan ID tidak kosong
if (!stateKategori.edit.id) {
stateKategori.edit.id = id; // fallback
}
const success = await stateKategori.edit.update();
if (success) {
router.push("/admin/lingkungan/gotong-royong/kategori-kegiatan");
}
} catch (error) {
console.error("Error updating kategori kegiatan:", error);
// toast akan ditampilkan dari fungsi update
}
};
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 Kategori kegiatan</Title>
<TextInput
value={formData.nama}
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori kegiatan</Text>}
placeholder='Masukkan nama kategori kegiatan'
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditKategoriKegiatan;

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