Compare commits

...

21 Commits

Author SHA1 Message Date
f7437708c0 Menu Desa, Sub Menu Yang Tersisa Tinga Tinggal Profile Aja 2025-06-16 17:46:25 +08:00
7bf5ee69d5 Api Admin Menu Desa Sub Menu Layanan Tabs 3 Pelayanan Done 2025-06-16 15:01:15 +08:00
e03b071b00 UI Admin Dashboard Layanan Desa 1 tabs
API Admin Dashboard Layanan Desa 1 Tabs
2025-06-16 03:08:34 +08:00
8ded234991 UI Desa Gallery Done
API Desa Gallery Done
2025-06-13 16:32:33 +08:00
1462e1d256 Tampilan UI Admin PPID IKM & API 2025-06-12 17:40:37 +08:00
a1c2821153 API Struktur Admin PPID 2025-06-11 11:19:30 +08:00
9f66b037f9 Add Fitur Status Operasional & Jadwal Kerja 2025-06-10 14:22:51 +08:00
9e725e2eea Fix UI Lingkungan, UI Ekonomi Progress 2025-06-10 11:53:10 +08:00
e4b7418ed3 Tambahan Fix UI PPID 2025-06-10 11:01:13 +08:00
6d312b7a28 Fix UI Admin PPID
Profile PPID clear
2025-06-10 00:54:29 +08:00
41ae92774d Fix UI PPID:
Visi Misi PPID, Dasar Hukum, Daftar Informasi Publik
2025-06-09 12:09:00 +08:00
46748205fd API Menu Kesehatan, Sub Menu Posyandu 2025-06-05 16:10:43 +08:00
5e74447056 Tambahan UI di menu ligkungan 2025-06-05 12:23:02 +08:00
c9d0ea2a97 Tambahan Tampilan Admin Menu Inovasi 2025-06-05 11:00:51 +08:00
7d58513e33 UI admin bagian pengumuman dan backendnya 2025-06-04 17:24:32 +08:00
f56c5b3532 Tambahan UI Admin : Dibagian Desa Sisa Profile, Potensi, Penghargaan 2025-06-04 15:08:12 +08:00
06622c49e8 Tampilan UI Admin Ekonomi & Inovasi Progress 2025-06-04 14:02:39 +08:00
a1e7fddbed UI Admin Revisi Baru Di Menu Ekonomi 2025-06-03 17:42:34 +08:00
423ad0e2ba Tambahan 2025-06-03 12:10:00 +08:00
084435500f Lanjutan UI 2025-06-03 10:21:06 +08:00
5037009c40 Kesehatan : udah fix semua, cuma dibagian kayak detail sama edit uinya belum 2025-06-02 22:23:13 +08:00
412 changed files with 20398 additions and 5216 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -22,8 +22,9 @@
"@mantine/carousel": "^7.16.2",
"@mantine/charts": "^7.17.1",
"@mantine/core": "^7.17.4",
"@mantine/dates": "^7.17.4",
"@mantine/dates": "^8.1.0",
"@mantine/dropzone": "^7.17.0",
"@mantine/form": "^8.1.0",
"@mantine/hooks": "^7.17.4",
"@mantine/tiptap": "^7.17.4",
"@paljs/types": "^8.1.0",

View File

@@ -0,0 +1,7 @@
[
{
"id": "1",
"name": "Pelayanan Penduduk Non-Permanent",
"deskripsi": "<p>Surat Keterangan Penduduk Non-Permanent adalah dokumen yang dikeluarkan oleh pihak berwenang untuk memberikan keterangan bahwa seseorang atau kelompok orang memiliki status penduduk non-permanent di suatu wilayah. Dokumen ini biasanya digunakan untuk keperluan administratif atau legal, seperti mendapatkan akses ke layanan kesehatan, pendidikan, atau pelayanan publik lainnya.</p>"
}
]

View File

@@ -0,0 +1,8 @@
[
{
"id": "1",
"name": "Pelayanan Perizinan Berusaha Berbasis Risiko Melalui Sistem ONLINE SINGLE SUBMISSION (OSS)",
"deskripsi": "<p>Penyelenggaraan Perizinan Berusaha Berbasis Risiko melalui Sistem Online Single Submission (OSS) merupakan pelaksanaan Undang-Undang Nomor 11 Tahun 2020 Tentang Cipta Kerja. OSS Berbasis Risiko wajib digunakan oleh Pelaku Usaha, Kementerian/Lembaga, Pemerintah Daerah, Administrator Kawasan Ekonomi Khusus (KEK), dan Badan Pengusahaan Kawasan Perdagangan Bebas Pelabuhan Bebas (KPBPB).Berdasarkan Peraturan Pemerintah Nomor 5 Tahun 2021 terdapat 1.702 kegiatan usaha yang terdiri atas 1.349 Klasifikasi Baku Lapangan Usaha Indonesia (KBLI) yang sudah diimplementasikan dalam Sistem OSS Berbasis Risiko.</p>",
"link" : "https://oss.go.id/"
}
]

View File

@@ -5,7 +5,6 @@
"biodata": "<p>I.B Surya Prabhawa Manuaba, S.H., M.H., adalah Perbekel Darmasaba periode 2021-2027, seorang advokat, pendiri Mantra Legal Consultants & Advocates, serta aktif di bidang musik dan akademis. Dia menempuh pendidikan hukum di Universitas Udayana dan Universitas Mahasaraswati Denpasar, serta memiliki pengalaman luas di berbagai organisasi dan kepemimpinan.</p>",
"riwayat": "<ul> <li>2021 - 2027: Perbekel Desa Darmasaba</li> <li>2015 - Sekarang: Founder & Managing Director Mantra Legal Consultants & Advocates</li> <li>2020 - Sekarang: Founder Ugawa Record Music Studio</li> <li>2010 - 2016: Dosen Fakultas Hukum Universitas Mahasaraswati Denpasar</li> </ul>",
"pengalaman": "<ul> <li>1996 1997: Ketua OSIS SMP Negeri 1 Abiansemal</li><li>1999 2000: Ketua OSIS SMA Negeri 1 Mengwi</li> <li>2008 2009: Ketua BEM Universitas Mahasaraswati Denpasar</li> <li>2008 2010: Ketua Sekaa Taruna Sila Dharma, Banjar Tengah, Desa Adat Tegal, Darmasaba</li> <li>2020 Sekarang: Pengurus Young Lawyer Committee Peradi Denpasar</li> <li>2021 Sekarang: Dewan Kehormatan Himpunan Pengusaha Muda Indonesia (HIPMI) Badung</li> <li>2023 2028: Komite Tetap Advokasi Bidang Hukum dan Regulasi Kamar Dagang dan Industri Badung</li> </ul>",
"unggulan": "<h3>Pemberdayaan Ekonomi dan UMKM</h3> <ul> <li>Pelatihan dan pendampingan UMKM lokal</li> <li>Program bantuan modal usaha bagi pelaku usaha kecil</li><li>Digitalisasi UMKM untuk meningkatkan pemasaran produk lokal</li></ul>",
"imageUrl": "/uploads/seeded-images/profile-ppid/perbekel.png"
"unggulan": "<h3>Pemberdayaan Ekonomi dan UMKM</h3> <ul> <li>Pelatihan dan pendampingan UMKM lokal</li> <li>Program bantuan modal usaha bagi pelaku usaha kecil</li><li>Digitalisasi UMKM untuk meningkatkan pemasaran produk lokal</li></ul>"
}
]

View File

@@ -0,0 +1,6 @@
[
{
"id" : "1",
"name" : "Struktur PPID"
}
]

View File

@@ -0,0 +1,582 @@
/*
Warnings:
- You are about to drop the column `image` on the `Berita` table. All the data in the column will be lost.
- You are about to drop the column `katagoryBeritaId` on the `Berita` table. All the data in the column will be lost.
- You are about to drop the column `name` on the `FasilitasPendukung` table. All the data in the column will be lost.
- You are about to drop the column `fasilitasKesehatanId` on the `InformasiUmum` table. All the data in the column will be lost.
- You are about to drop the `KatagoryBerita` table. If the table is not empty, all the data it contains will be lost.
- Added the required column `imageId` to the `Berita` table without a default value. This is not possible if the table is not empty.
- Added the required column `jadwal` to the `DokterdanTenagaMedis` table without a default value. This is not possible if the table is not empty.
- Added the required column `specialist` to the `DokterdanTenagaMedis` table without a default value. This is not possible if the table is not empty.
- Added the required column `content` to the `FasilitasPendukung` table without a default value. This is not possible if the table is not empty.
*/
-- DropForeignKey
ALTER TABLE "Berita" DROP CONSTRAINT "Berita_katagoryBeritaId_fkey";
-- DropForeignKey
ALTER TABLE "InformasiUmum" DROP CONSTRAINT "InformasiUmum_fasilitasKesehatanId_fkey";
-- AlterTable
ALTER TABLE "Berita" DROP COLUMN "image",
DROP COLUMN "katagoryBeritaId",
ADD COLUMN "imageId" TEXT NOT NULL,
ADD COLUMN "kategoriBeritaId" TEXT;
-- AlterTable
ALTER TABLE "DataKematian_Kelahiran" ADD COLUMN "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true;
-- AlterTable
ALTER TABLE "DokterdanTenagaMedis" ADD COLUMN "jadwal" TEXT NOT NULL,
ADD COLUMN "specialist" TEXT NOT NULL;
-- AlterTable
ALTER TABLE "FasilitasPendukung" DROP COLUMN "name",
ADD COLUMN "content" TEXT NOT NULL;
-- AlterTable
ALTER TABLE "InformasiUmum" DROP COLUMN "fasilitasKesehatanId";
-- DropTable
DROP TABLE "KatagoryBerita";
-- CreateTable
CREATE TABLE "FileStorage" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"realName" TEXT NOT NULL,
"path" TEXT NOT NULL,
"mimeType" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3),
"isActive" BOOLEAN NOT NULL DEFAULT true,
"link" TEXT NOT NULL,
CONSTRAINT "FileStorage_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "VisiMisiPPID" (
"id" TEXT NOT NULL,
"visi" TEXT NOT NULL,
"misi" 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 "VisiMisiPPID_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "DasarHukumPPID" (
"id" TEXT NOT NULL,
"judul" TEXT NOT NULL,
"content" 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 "DasarHukumPPID_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ProfilePPID" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"biodata" TEXT NOT NULL,
"riwayat" TEXT NOT NULL,
"pengalaman" TEXT NOT NULL,
"unggulan" TEXT NOT NULL,
"imageUrl" 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 "ProfilePPID_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "DaftarInformasiPublik" (
"id" TEXT NOT NULL,
"nomor" SERIAL NOT NULL,
"jenisInformasi" TEXT NOT NULL,
"deskripsi" TEXT NOT NULL,
"tanggal" 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 "DaftarInformasiPublik_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PermohonanInformasiPublik" (
"id" TEXT NOT NULL,
"nomor" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"nik" TEXT NOT NULL,
"notelp" TEXT NOT NULL,
"alamat" TEXT NOT NULL,
"email" TEXT NOT NULL,
"jenisInformasiDimintaId" TEXT,
"caraMemperolehInformasiId" TEXT,
"caraMemperolehSalinanInformasiId" 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 "PermohonanInformasiPublik_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "JenisInformasiDiminta" (
"id" TEXT NOT NULL,
"name" 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 "JenisInformasiDiminta_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "CaraMemperolehInformasi" (
"id" TEXT NOT NULL,
"name" 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 "CaraMemperolehInformasi_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "CaraMemperolehSalinanInformasi" (
"id" TEXT NOT NULL,
"name" 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 "CaraMemperolehSalinanInformasi_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "FormulirPermohonanKeberatan" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"email" TEXT NOT NULL,
"notelp" TEXT NOT NULL,
"alasan" 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 "FormulirPermohonanKeberatan_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "IndeksKepuasanMasyarakat" (
"id" SERIAL NOT NULL,
"label" TEXT NOT NULL,
"kepuasan" 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 "IndeksKepuasanMasyarakat_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "GrafikBerdasarkanJenisKelamin" (
"id" TEXT NOT NULL,
"perempuan" TEXT NOT NULL,
"laki" 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 "GrafikBerdasarkanJenisKelamin_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "GrafikBerdasarkanResponden" (
"id" TEXT NOT NULL,
"sangatbaik" TEXT NOT NULL,
"baik" TEXT NOT NULL,
"kurangbaik" TEXT NOT NULL,
"tidakbaik" 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 "GrafikBerdasarkanResponden_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "GrafikBerdasarkanUmur" (
"id" TEXT NOT NULL,
"remaja" TEXT NOT NULL,
"dewasa" TEXT NOT NULL,
"orangtua" TEXT NOT NULL,
"lansia" 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 "GrafikBerdasarkanUmur_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ProfileDesa" (
"id" TEXT NOT NULL,
"sejarah" TEXT NOT NULL,
"visi" TEXT NOT NULL,
"misi" TEXT NOT NULL,
"lambang" TEXT NOT NULL,
"maskot" TEXT NOT NULL,
"profilPerbekelId" 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 "ProfileDesa_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ProfilPerbekel" (
"id" TEXT NOT NULL,
"biodata" TEXT NOT NULL,
"pengalaman" TEXT NOT NULL,
"pengalamanOrganisasi" TEXT NOT NULL,
"programUnggulan" 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 "ProfilPerbekel_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "KategoriBerita" (
"id" TEXT NOT NULL,
"name" 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 "KategoriBerita_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PotensiDesa" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"deskripsi" TEXT NOT NULL,
"kategori" TEXT NOT NULL,
"imageId" TEXT NOT NULL,
"content" 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 "PotensiDesa_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "TarifDanLayanan" (
"id" TEXT NOT NULL,
"layanan" TEXT NOT NULL,
"tarif" 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 "TarifDanLayanan_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "JadwalKegiatan" (
"id" TEXT NOT NULL,
"content" 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 "JadwalKegiatan_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "InformasiJadwalKegiatan" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"tanggal" TEXT NOT NULL,
"waktu" TEXT NOT NULL,
"lokasi" 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 "InformasiJadwalKegiatan_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "DeskripsiJadwalKegiatan" (
"id" 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 "DeskripsiJadwalKegiatan_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "LayananJadwalKegiatan" (
"id" TEXT NOT NULL,
"content" 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 "LayananJadwalKegiatan_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "SyaratKetentuanJadwalKegiatan" (
"id" TEXT NOT NULL,
"content" 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 "SyaratKetentuanJadwalKegiatan_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "DokumenJadwalKegiatan" (
"id" TEXT NOT NULL,
"content" 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 "DokumenJadwalKegiatan_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PendaftaranJadwalKegiatan" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"tanggal" TEXT NOT NULL,
"namaOrangtua" TEXT NOT NULL,
"nomor" TEXT NOT NULL,
"alamat" TEXT NOT NULL,
"catatan" 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 "PendaftaranJadwalKegiatan_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "GrafikKepuasan" (
"id" SERIAL NOT NULL,
"label" TEXT NOT NULL,
"jumlah" 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 "GrafikKepuasan_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ArtikelKesehatan" (
"id" SERIAL NOT NULL,
"title" TEXT NOT NULL,
"content" 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 "ArtikelKesehatan_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Introduction" (
"id" SERIAL NOT NULL,
"content" 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 "Introduction_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Symptom" (
"id" SERIAL NOT NULL,
"title" TEXT NOT NULL,
"content" 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 "Symptom_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Prevention" (
"id" SERIAL NOT NULL,
"title" TEXT NOT NULL,
"content" 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 "Prevention_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "FirstAid" (
"id" SERIAL NOT NULL,
"title" TEXT NOT NULL,
"content" 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 "FirstAid_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "MythVsFact" (
"id" SERIAL NOT NULL,
"title" TEXT NOT NULL,
"mitos" TEXT NOT NULL,
"fakta" 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 "MythVsFact_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "DoctorSign" (
"id" SERIAL NOT NULL,
"content" 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 "DoctorSign_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "_FasilitasKesehatanToInformasiUmum" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL,
CONSTRAINT "_FasilitasKesehatanToInformasiUmum_AB_pkey" PRIMARY KEY ("A","B")
);
-- CreateTable
CREATE TABLE "_FasilitasKesehatanToTarifDanLayanan" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL,
CONSTRAINT "_FasilitasKesehatanToTarifDanLayanan_AB_pkey" PRIMARY KEY ("A","B")
);
-- CreateIndex
CREATE UNIQUE INDEX "FileStorage_name_key" ON "FileStorage"("name");
-- CreateIndex
CREATE UNIQUE INDEX "JenisInformasiDiminta_name_key" ON "JenisInformasiDiminta"("name");
-- CreateIndex
CREATE UNIQUE INDEX "CaraMemperolehInformasi_name_key" ON "CaraMemperolehInformasi"("name");
-- CreateIndex
CREATE UNIQUE INDEX "CaraMemperolehSalinanInformasi_name_key" ON "CaraMemperolehSalinanInformasi"("name");
-- CreateIndex
CREATE UNIQUE INDEX "KategoriBerita_name_key" ON "KategoriBerita"("name");
-- CreateIndex
CREATE INDEX "_FasilitasKesehatanToInformasiUmum_B_index" ON "_FasilitasKesehatanToInformasiUmum"("B");
-- CreateIndex
CREATE INDEX "_FasilitasKesehatanToTarifDanLayanan_B_index" ON "_FasilitasKesehatanToTarifDanLayanan"("B");
-- AddForeignKey
ALTER TABLE "PermohonanInformasiPublik" ADD CONSTRAINT "PermohonanInformasiPublik_jenisInformasiDimintaId_fkey" FOREIGN KEY ("jenisInformasiDimintaId") REFERENCES "JenisInformasiDiminta"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PermohonanInformasiPublik" ADD CONSTRAINT "PermohonanInformasiPublik_caraMemperolehInformasiId_fkey" FOREIGN KEY ("caraMemperolehInformasiId") REFERENCES "CaraMemperolehInformasi"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PermohonanInformasiPublik" ADD CONSTRAINT "PermohonanInformasiPublik_caraMemperolehSalinanInformasiId_fkey" FOREIGN KEY ("caraMemperolehSalinanInformasiId") REFERENCES "CaraMemperolehSalinanInformasi"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ProfileDesa" ADD CONSTRAINT "ProfileDesa_profilPerbekelId_fkey" FOREIGN KEY ("profilPerbekelId") REFERENCES "ProfilPerbekel"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Berita" ADD CONSTRAINT "Berita_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Berita" ADD CONSTRAINT "Berita_kategoriBeritaId_fkey" FOREIGN KEY ("kategoriBeritaId") REFERENCES "KategoriBerita"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PotensiDesa" ADD CONSTRAINT "PotensiDesa_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_FasilitasKesehatanToInformasiUmum" ADD CONSTRAINT "_FasilitasKesehatanToInformasiUmum_A_fkey" FOREIGN KEY ("A") REFERENCES "FasilitasKesehatan"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_FasilitasKesehatanToInformasiUmum" ADD CONSTRAINT "_FasilitasKesehatanToInformasiUmum_B_fkey" FOREIGN KEY ("B") REFERENCES "InformasiUmum"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_FasilitasKesehatanToTarifDanLayanan" ADD CONSTRAINT "_FasilitasKesehatanToTarifDanLayanan_A_fkey" FOREIGN KEY ("A") REFERENCES "FasilitasKesehatan"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_FasilitasKesehatanToTarifDanLayanan" ADD CONSTRAINT "_FasilitasKesehatanToTarifDanLayanan_B_fkey" FOREIGN KEY ("B") REFERENCES "TarifDanLayanan"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -50,21 +50,43 @@ model AppMenuChild {
// ========================================= FILE STORAGE ========================================= //
model FileStorage {
id String @id @default(cuid())
name String @unique
realName String
path String
mimeType String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
isActive Boolean @default(true)
link String
Berita Berita[]
PotensiDesa PotensiDesa[]
id String @id @default(cuid())
name String @unique
realName String
path String
mimeType String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
isActive Boolean @default(true)
link String
Berita Berita[]
PotensiDesa PotensiDesa[]
Posyandu Posyandu[]
ProfilePPID ProfilePPID[]
StrukturPPID StrukturPPID[]
GalleryFoto GalleryFoto[]
PelayananSuratKeterangan PelayananSuratKeterangan[]
Penghargaan Penghargaan[]
}
//========================================= MENU PPID ========================================= //
//========================================= STRUKTUR PPID ========================================= //
model StrukturPPID {
id String @id @default(cuid())
name String @db.Text
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= VISI MISI PPID ========================================= //
model VisiMisiPPID {
id String @id @default(cuid())
@@ -89,23 +111,23 @@ model DasarHukumPPID {
// ========================================= PROFILE PPID ========================================= //
model ProfilePPID {
id String @id @default(cuid())
name String @db.Text
biodata String @db.Text
riwayat String @db.Text
pengalaman String @db.Text
unggulan String @db.Text
imageUrl String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
id String @id @default(cuid())
name String @db.Text
biodata String @db.Text
riwayat String @db.Text
pengalaman String @db.Text
unggulan String @db.Text
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= DAFTAR INFORMASI PUBLIK ========================================= //
model DaftarInformasiPublik {
id String @id @default(cuid())
nomor Int @default(autoincrement())
jenisInformasi String
deskripsi String
tanggal String
@@ -181,7 +203,7 @@ model FormulirPermohonanKeberatan {
// ========================================= IKM ========================================= //
model IndeksKepuasanMasyarakat {
id Int @id @default(autoincrement())
id String @id @default(cuid())
label String
kepuasan String
createdAt DateTime @default(now())
@@ -319,51 +341,87 @@ model CategoryPengumuman {
isActive Boolean @default(true)
}
// ========================================= IMAGES ========================================= //
model Images {
id String @id @default(cuid())
url String
label String @default("null")
active Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
GalleryFoto GalleryFoto[]
}
// ========================================= VIDEOS ========================================= //
model Videos {
id String @id @default(cuid())
url String
label String @default("null")
active Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
GalleryVideo GalleryVideo[]
}
// ========================================= GALLERY ========================================= //
model GalleryFoto {
id String @id @default(cuid())
id String @id @default(cuid())
name String
image String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
imagesId String? @unique
imageGalleryFoto Images? @relation(fields: [imagesId], references: [id])
deskripsi String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
imagesId String? @unique
imageGalleryFoto FileStorage? @relation(fields: [imagesId], references: [id])
}
model GalleryVideo {
id String @id @default(cuid())
name String
video String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
videosId String? @unique
videosGalleryVideo Videos? @relation(fields: [videosId], references: [id])
id String @id @default(cuid())
name String
deskripsi String @db.Text
linkVideo String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= LAYANAN DESA ========================================= //
model PelayananSuratKeterangan {
id String @id @default(cuid())
name String
deskripsi String @db.Text
image FileStorage @relation(fields: [imageId], references: [id])
imageId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model PelayananTelunjukSaktiDesa {
id String @id @default(cuid())
name String
deskripsi String @db.Text
link String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model PelayananPerizinanBerusaha {
id String @id @default(cuid())
name String
deskripsi String @db.Text
link String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model PelayananPendudukNonPermanen {
id String @id @default(cuid())
name String
deskripsi String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= PENGHARGAAN ========================================= //
model Penghargaan {
id String @id @default(cuid())
name String
juara String
deskripsi String @db.Text
image FileStorage @relation(fields: [imageId], references: [id])
imageId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= MENU KESEHATAN ========================================= //
@@ -615,3 +673,17 @@ model DoctorSign {
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= POSYANDU ========================================= //
model Posyandu {
id String @id @default(cuid())
name String
nomor String
deskripsi 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)
}

View File

@@ -6,15 +6,12 @@ import caraMemperolehSalinanInformasi from "./data/list-caraMemperolehSalinanInf
import jenisInformasiDiminta from "./data/list-jenisInfromasi.json";
import layanan from "./data/list-layanan.json";
import potensi from "./data/list-potensi.json";
import visiMisiPPID from "./data/ppid/visi-misi-ppid/visimisiPPID.json";
import dasarHukumPPID from "./data/ppid/dasar-hukum-ppid/dasarhukumPPID.json";
import profileDesa from "./data/desa/profile/profile_desa.json";
import profilePerbekel from "./data/desa/profile/profil_perbekel.json";
import profilePPID from "./data/ppid/profile-ppid/profilePPid.json";
import path from "path";
import fs from "fs";
import { mkdir, writeFile } from "fs/promises";
import { v4 as uuid } from "uuid";
import visiMisiPPID from "./data/ppid/visi-misi-ppid/visimisiPPID.json";
import strukturPPID from "./data/ppid/struktur-ppid/strukturPPID.json";
import pelayananPerizinanBerusaha from "./data/desa/layanan/pelayananPerizinanBerusaha.json";
import pelayananPendudukNonPermanen from "./data/desa/layanan/pelayanaPendudukNonPermanen.json";
(async () => {
for (const l of layanan) {
@@ -33,6 +30,62 @@ import { v4 as uuid } from "uuid";
console.log("layanan success ...");
for (const l of pelayananPerizinanBerusaha) {
await prisma.pelayananPerizinanBerusaha.upsert({
where: {
id: l.id,
},
update: {
name: l.name,
deskripsi: l.deskripsi,
link: l.link,
},
create: {
id: l.id,
name: l.name,
deskripsi: l.deskripsi,
link: l.link,
},
});
}
console.log("pelayanan perizinan berusaha success ...");
for (const l of pelayananPendudukNonPermanen) {
await prisma.pelayananPendudukNonPermanen.upsert({
where: {
id: l.id,
},
update: {
name: l.name,
deskripsi: l.deskripsi,
},
create: {
id: l.id,
name: l.name,
deskripsi: l.deskripsi,
},
});
}
console.log("pelayanan perizinan berusaha success ...");
for (const s of strukturPPID) {
await prisma.strukturPPID.upsert({
where: {
id: s.id,
},
update: {
name: s.name,
},
create: {
id: s.id,
name: s.name,
},
});
}
console.log("struktur ppid success ...");
for (const p of potensi) {
await prisma.potensi.upsert({
where: {
@@ -126,56 +179,29 @@ import { v4 as uuid } from "uuid";
}
console.log("cara memperoleh salinan informasi success ...");
const seedProfilePPID = async () => {
const targetDir = path.resolve("public", "uploads", "seeded-images", "profile-ppid")
// Buat folder hanya jika belum ada
if (!fs.existsSync(targetDir)) {
await mkdir(targetDir, { recursive: true })
}
for (const c of profilePPID) {
let finalImageUrl = c.imageUrl
if (c.imageUrl.startsWith("/uploads/seeded-images/")) {
const filename = path.basename(c.imageUrl)
const seedImagePath = path.resolve("prisma", "seed-images", filename)
const targetFilename = `${uuid()}_${filename}`
const targetPath = path.join(targetDir, targetFilename)
const buffer = fs.readFileSync(seedImagePath)
await writeFile(targetPath, buffer)
finalImageUrl = `/uploads/seeded-images/profile-ppid/${targetFilename}`
}
await prisma.profilePPID.upsert({
where: { id: c.id },
update: {
name: c.name,
biodata: c.biodata,
riwayat: c.riwayat,
pengalaman: c.pengalaman,
unggulan: c.unggulan,
imageUrl: finalImageUrl,
},
create: {
id: c.id,
name: c.name,
biodata: c.biodata,
riwayat: c.riwayat,
pengalaman: c.pengalaman,
unggulan: c.unggulan,
imageUrl: finalImageUrl,
},
})
}
console.log("✅ profilePPID seeded from JSON with image copying")
for (const c of profilePPID) {
await prisma.profilePPID.upsert({
where: { id: c.id },
update: {
name: c.name,
biodata: c.biodata,
riwayat: c.riwayat,
pengalaman: c.pengalaman,
unggulan: c.unggulan,
// imageId tidak di-update
},
create: {
id: c.id,
name: c.name,
biodata: c.biodata,
riwayat: c.riwayat,
pengalaman: c.pengalaman,
unggulan: c.unggulan,
// imageId tidak di-create
},
});
}
await seedProfilePPID()
console.log("✅ profilePPID seeded without imageId (editable later via UI)");
for (const v of visiMisiPPID) {
await prisma.visiMisiPPID.upsert({
@@ -212,54 +238,6 @@ import { v4 as uuid } from "uuid";
});
}
console.log("dasar hukum PPID success ...");
for (const v of profileDesa) {
await prisma.profileDesa.upsert({
where: {
id: v.id,
},
update: {
sejarah: v.sejarah,
visi: v.visi,
misi: v.misi,
lambang: v.lambang,
maskot: v.maskot,
profilPerbekelId: v.profilPerbekelId,
},
create: {
id: v.id,
sejarah: v.sejarah,
visi: v.visi,
misi: v.misi,
lambang: v.lambang,
maskot: v.maskot,
profilPerbekelId: v.profilPerbekelId,
},
});
}
console.log("profile desa success ...");
for (const v of profilePerbekel) {
await prisma.profilPerbekel.upsert({
where: {
id: v.id,
},
update: {
biodata: v.biodata,
pengalaman: v.pengalaman,
pengalamanOrganisasi: v.pengalamanOrganisasi,
programUnggulan: v.programUnggulan,
},
create: {
id: v.id,
biodata: v.biodata,
pengalaman: v.pengalaman,
pengalamanOrganisasi: v.pengalamanOrganisasi,
programUnggulan: v.programUnggulan,
},
});
}
console.log("profile perbekel success ...");
})()
.then(() => prisma.$disconnect())
.catch((e) => {

BIN
public/Share.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

BIN
public/bagikanPostingan.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

BIN
public/sematkan.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

BIN
public/struktur_ppid.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

BIN
public/video.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@@ -0,0 +1,41 @@
'use client'
import colors from '@/con/colors';
import { Grid, GridCol, Button, Text, Paper, TextInput } from '@mantine/core';
import { IconCircleDashedPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import React from 'react';
const JudulListTab = ({ title = "", href = "#", placeholder = "pencarian", searchIcon = <IconSearch size={20} /> }) => {
const router = useRouter();
const handleNavigate = () => {
router.push(href);
};
return (
<Grid mb={10}>
<GridCol span={{ base: 12, md: 8 }}>
<Text fz={{base: "md", md: "xl"}} fw={"bold"}>{title}</Text>
</GridCol>
<GridCol span={{ base: 9, md: 3}} ta="right">
<Paper radius={"lg"} bg={colors['white-1']}>
<TextInput
radius="lg"
placeholder={placeholder}
leftSection={searchIcon}
w="100%"
/>
</Paper>
</GridCol>
<GridCol span={{ base: 3, md: 1}} ta="right">
<Button onClick={handleNavigate} bg={colors['blue-button']}>
<IconCircleDashedPlus size={25} />
</Button>
</GridCol>
</Grid>
);
};
export default JudulListTab;

View File

@@ -21,7 +21,7 @@ export function ModalKonfirmasiHapus({
<Modal
opened={opened}
onClose={onClose}
title="Konfirmasi Hapus"
title={<Text fw={"bold"} fz={"xl"}>Konfirmasi Hapus</Text>}
centered
>
<Text mb="md">{text}</Text>

View File

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

View File

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

View File

@@ -0,0 +1,221 @@
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
const templateForm = z.object({
name: z.string().min(1).max(50),
juara: z.string().min(1).max(50),
deskripsi: z.string().min(1).max(5000),
imageId: z.string().min(1).max(50),
});
const defaultForm = {
name: "",
juara: "",
deskripsi: "",
imageId: "",
};
const penghargaanState = proxy({
create: {
form: { ...defaultForm },
loading: false,
async create() {
const cek = templateForm.safeParse(penghargaanState.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
penghargaanState.create.loading = true;
const res = await ApiFetch.api.desa.penghargaan["create"].post(
penghargaanState.create.form
);
if (res.status === 200) {
penghargaanState.findMany.load();
return toast.success("success create");
}
console.log(res);
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
} finally {
penghargaanState.create.loading = false;
}
},
},
findMany: {
data: null as
| Prisma.PenghargaanGetPayload<{
include: {
image: true;
};
}>[]
| null,
async load() {
const res = await ApiFetch.api.desa.penghargaan["find-many"].get();
if (res.status === 200) {
penghargaanState.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.PenghargaanGetPayload<{
include: {
image: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/desa/penghargaan/${id}`);
if (res.ok) {
const data = await res.json();
penghargaanState.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
penghargaanState.findUnique.data = null;
}
} catch (error) {
console.error("Error loading penghargaan:", error);
penghargaanState.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
penghargaanState.delete.loading = true;
const response = await fetch(`/api/desa/penghargaan/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok) {
toast.success(result.message || "Penghargaan berhasil dihapus");
await penghargaanState.findMany.load();
} else {
toast.error(result?.message || "Gagal menghapus penghargaan");
}
} catch (error) {
console.log((error as Error).message);
toast.error("Terjadi kesalahan saat menghapus penghargaan");
} finally {
penghargaanState.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...defaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/desa/penghargaan/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
name: data.name,
juara: data.juara,
deskripsi: data.deskripsi,
imageId: data.imageId,
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading penghargaan:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateForm.safeParse(penghargaanState.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
penghargaanState.edit.loading = true;
const response = await fetch(
`/api/desa/penghargaan/${penghargaanState.edit.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
juara: this.form.juara,
deskripsi: this.form.deskripsi,
imageId: this.form.imageId,
}),
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update penghargaan");
await penghargaanState.findMany.load();
return true;
} else {
throw new Error(result?.message || "Gagal update penghargaan");
}
} catch (error) {
console.error("Error updating penghargaan:", error);
toast.error(
error instanceof Error
? error.message
: "Terjadi kesalahan saat update penghargaan"
);
return false;
} finally {
penghargaanState.edit.loading = false;
}
},
reset() {
penghargaanState.edit.id = "";
penghargaanState.edit.form = { ...defaultForm };
},
},
});
export default penghargaanState;

View File

@@ -1,83 +1,246 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch"
import { Prisma } from "@prisma/client"
import { toast } from "react-toastify"
import { proxy } from "valtio"
import { z } from "zod"
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 templateFormPengumuman = z.object({
judul: z.string().min(3, "Judul minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
content: z.string().min(3, "Content minimal 3 karakter"),
categoryPengumumanId: z.string().nonempty(),
})
judul: z.string().min(3, "Judul minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
content: z.string().min(3, "Content minimal 3 karakter"),
categoryPengumumanId: z.string().nonempty(),
});
const category = proxy ({
findMany: {
data: null as
| null
| Prisma.CategoryPengumumanGetPayload<{ omit: { isActive: true } }>[],
async load() {
const res = await ApiFetch.api.desa.pengumuman.category["find-many"].get();
if (res.status === 200) {
category.findMany.data = res.data?.data as any ?? [];
}
}
}
})
const category = proxy({
findMany: {
data: null as
| null
| Prisma.CategoryPengumumanGetPayload<{ omit: { isActive: true } }>[],
async load() {
const res = await ApiFetch.api.desa.pengumuman.category[
"find-many"
].get();
if (res.status === 200) {
category.findMany.data = (res.data?.data as any) ?? [];
}
},
},
});
type PengumumanForm = Prisma.PengumumanGetPayload<{
select: {
judul: true;
deskripsi: true;
content: true;
categoryPengumumanId: true;
}
}>
select: {
judul: true;
deskripsi: true;
content: true;
categoryPengumumanId: true;
};
}>;
const pengumuman = proxy({
create: {
create: {
form: {} as PengumumanForm,
loading: false,
async create() {
const cek = templateFormPengumuman.safeParse(pengumuman.create.form);
if (!cek.success) {
const cek = templateFormPengumuman.safeParse(pengumuman.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
}
try {
pengumuman.create.loading = true;
const res = await ApiFetch.api.desa.pengumuman["create"].post(pengumuman.create.form)
const res = await ApiFetch.api.desa.pengumuman["create"].post(
pengumuman.create.form
);
if (res.status === 200) {
pengumuman.findMany.load();
return toast.success("success create");
pengumuman.findMany.load();
return toast.success("success create");
}
console.log(res)
console.log(res);
return toast.error("failed create");
} catch (error) {
} catch (error) {
console.log((error as Error).message);
} finally{
} finally {
pengumuman.create.loading = false;
}
}
},
findMany: {
}
},
},
findMany: {
data: null as
| Prisma.PengumumanGetPayload<{omit: {isActive: true}}>[]
| null,
async load () {
const res = await ApiFetch.api.desa.pengumuman["find-many"].get();
console.log(res)
if (res.status === 200) {
pengumuman.findMany.data = res.data?.data ?? [];
| Prisma.PengumumanGetPayload<{
include: {
CategoryPengumuman: true;
}
}
}
})
}>[]
| null,
async load() {
const res = await ApiFetch.api.desa.pengumuman["find-many"].get();
console.log(res);
if (res.status === 200) {
pengumuman.findMany.data = res.data?.data ?? [];
}
},
},
// findUnique: {
// data: null as
// | Prisma.PengumumanGetPayload<{
// include: {
// CategoryPengumuman: true;
// }
// }>
// | null,
// async load(id: string) {
// try {
// const res = await fetch(`/api/desa/pengumuman/${id}`);
// if (res.ok) {
// const data = await res.json();
// pengumuman.findUnique.data = data.data ?? null;
// } else {
// console.error('Failed to fetch pengumuman:', res.statusText);
// pengumuman.findUnique.data = null;
// }
// } catch (error) {
// console.error('Error fetching pengumuman:', error);
// pengumuman.findUnique.data = null;
// }
// },
// },
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
pengumuman.delete.loading = true;
const response = await fetch(`/api/desa/pengumuman/delete/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Pengumuman berhasil dihapus");
await pengumuman.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus pengumuman");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus pengumuman");
} finally {
pengumuman.delete.loading = false;
}
},
},
update: {
id: "",
form: {} as PengumumanForm,
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/desa/pengumuman/${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,
content: data.content,
categoryPengumumanId: data.categoryPengumumanId || "",
};
return data;
} else {
throw new Error(result?.message || "Gagal mengambil data pengumuman");
}
} catch (error) {
console.error((error as Error).message);
toast.error("Terjadi kesalahan saat mengambil data pengumuman");
} finally {
pengumuman.update.loading = false;
}
},
async update() {
const cek = templateFormPengumuman.safeParse(pengumuman.update.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
pengumuman.update.loading = true;
const response = await fetch(`/api/desa/pengumuman/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
judul: this.form.judul,
deskripsi: this.form.deskripsi,
content: this.form.content,
categoryPengumumanId: this.form.categoryPengumumanId,
}),
});
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 pengumuman");
await pengumuman.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal update pengumuman");
}
} catch (error) {
console.error("Error updating pengumuman:", error);
toast.error(
error instanceof Error
? error.message
: "Terjadi kesalahan saat update pengumuman"
);
return false;
} finally {
pengumuman.update.loading = false;
}
},
},
});
const stateDesaPengumuman = proxy({
category,
pengumuman
})
export default stateDesaPengumuman
category,
pengumuman,
});
export default stateDesaPengumuman;

View File

@@ -5,61 +5,239 @@ import { proxy } from "valtio";
import { z } from "zod";
const templateDaftarInformasi = z.object({
jenisInformasi: z.string().min(3, "Jenis Informasi minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
tanggal: z.string().min(3, "Tanggal minimal 3 karakter"),
})
jenisInformasi: z.string().min(3, "Jenis Informasi minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
tanggal: z.string().min(3, "Tanggal minimal 3 karakter"),
});
const defaultForm = {
jenisInformasi: "",
deskripsi: "",
tanggal: "",
};
type DaftarInformasi = Prisma.DaftarInformasiPublikGetPayload<{
select: {
jenisInformasi: true;
deskripsi: true;
tanggal: true;
};
select: {
jenisInformasi: true;
deskripsi: true;
tanggal: true;
};
}>;
const daftarInformasi = proxy({
create: {
form: {} as DaftarInformasi,
loading: false,
async create() {
const cek = templateDaftarInformasi.safeParse(daftarInformasi.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
daftarInformasi.create.loading = true;
const res = await ApiFetch.api.ppid.daftarinformasipublik["create"].post(daftarInformasi.create.form);
if (res.status === 200) {
daftarInformasi.findMany.load();
return toast.success("success create");
}
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
} finally {
daftarInformasi.create.loading = false;
}
},
},
findMany: {
data: null as
| Prisma.DaftarInformasiPublikGetPayload<{ omit: { isActive: true } }>[]
| null,
async load() {
const res = await ApiFetch.api.ppid.daftarinformasipublik["find-many"].get();
if (res.status === 200) {
daftarInformasi.findMany.data = res.data?.data ?? [];
}
const daftarInformasiPublik = proxy({
create: {
form: {} as DaftarInformasi,
loading: false,
async create() {
const cek = templateDaftarInformasi.safeParse(
daftarInformasiPublik.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
daftarInformasiPublik.create.loading = true;
const res = await ApiFetch.api.ppid.daftarinformasipublik[
"create"
].post(daftarInformasiPublik.create.form);
if (res.status === 200) {
daftarInformasiPublik.findMany.load();
return toast.success("success create");
}
}
});
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
} finally {
daftarInformasiPublik.create.loading = false;
}
},
},
findMany: {
data: null as
| Prisma.DaftarInformasiPublikGetPayload<{ omit: { isActive: true } }>[]
| null,
async load() {
const res = await ApiFetch.api.ppid.daftarinformasipublik[
"find-many"
].get();
if (res.status === 200) {
daftarInformasiPublik.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.DaftarInformasiPublikGetPayload<{
omit: { isActive: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/ppid/daftarinformasipublik/${id}`);
if (res.ok) {
const data = await res.json();
daftarInformasiPublik.findUnique.data = data.data ?? null;
} else {
console.error(
"Failed to fetch daftar informasi publik:",
res.statusText
);
daftarInformasiPublik.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching daftar informasi publik:", error);
daftarInformasiPublik.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
const stateDaftarInformasiPublik = proxy({
daftarInformasi
})
try {
daftarInformasiPublik.delete.loading = true;
export default stateDaftarInformasiPublik;
const response = await fetch(
`/api/ppid/daftarinformasipublik/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(
result.message || "Daftar Informasi Publik berhasil dihapus"
);
await daftarInformasiPublik.findMany.load(); // refresh list
} else {
toast.error(
result?.message || "Gagal menghapus daftar informasi publik"
);
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus daftar informasi publik");
} finally {
daftarInformasiPublik.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...defaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/ppid/daftarinformasipublik/${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 = {
jenisInformasi: data.jenisInformasi,
deskripsi: data.deskripsi,
tanggal: data.tanggal,
};
return data;
} else {
throw new Error(result?.message || "Gagal mengambil data");
}
} catch (error) {
console.error("Error loading daftar informasi publik:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateDaftarInformasi.safeParse(
daftarInformasiPublik.edit.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
daftarInformasiPublik.edit.loading = true;
const response = await fetch(
`/api/ppid/daftarinformasipublik/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
jenisInformasi: this.form.jenisInformasi,
deskripsi: this.form.deskripsi,
tanggal: this.form.tanggal,
}),
}
);
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 daftar informasi publik");
await daftarInformasiPublik.findMany.load(); // refresh list
return true;
} else {
throw new Error(
result.message || "Gagal update daftar informasi publik"
);
}
} catch (error) {
console.error("Error updating daftar informasi publik:", error);
toast.error(
error instanceof Error
? error.message
: "Terjadi kesalahan saat update daftar informasi publik"
);
return false;
} finally {
daftarInformasiPublik.edit.loading = false;
}
},
reset() {
daftarInformasiPublik.edit.id = "";
daftarInformasiPublik.edit.form = { ...defaultForm };
},
},
});
export default daftarInformasiPublik;

View File

@@ -11,12 +11,13 @@ const templateGrafikJenisKelamin = z.object({
type GrafikJenisKelamin = Prisma.GrafikBerdasarkanJenisKelaminGetPayload<{
select: {
id: true;
laki: true;
perempuan: true;
};
}>;
const defaultForm: GrafikJenisKelamin = {
const defaultForm: Omit<GrafikJenisKelamin, 'id'> & { id?: string } = {
laki: "",
perempuan: "",
};
@@ -41,9 +42,16 @@ const grafikBerdasarkanJenisKelamin = proxy({
"create"
].post(grafikBerdasarkanJenisKelamin.create.form);
if (res.status === 200) {
grafikBerdasarkanJenisKelamin.create.form = defaultForm;
grafikBerdasarkanJenisKelamin.findMany.load();
return toast.success("success create");
const id = res.data?.data?.id;
if (id) {
toast.success("Success create");
grafikBerdasarkanJenisKelamin.create.form = {
laki: "",
perempuan: "",
};
grafikBerdasarkanJenisKelamin.findMany.load();
return id;
}
}
return toast.error("failed create");
} catch (error) {
@@ -69,9 +77,103 @@ const grafikBerdasarkanJenisKelamin = proxy({
}
},
},
findUnique: {
data: null as Prisma.GrafikBerdasarkanJenisKelaminGetPayload<{
omit: { isActive: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(
`/api/ppid/grafikberdasarkanjeniskelamin/${id}`
);
if (res.ok) {
const data = await res.json();
grafikBerdasarkanJenisKelamin.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
grafikBerdasarkanJenisKelamin.findUnique.data = null;
}
} catch (error) {
console.error("Error loading grafik berdasarkan jenis kelamin:", error);
grafikBerdasarkanJenisKelamin.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 = templateGrafikJenisKelamin.safeParse(this.form);
if (!cek.success) {
const err = `[${cek.error.issues.map((v) => `${v.path.join(".")}`).join("\n")}] required`;
toast.error(err);
return null;
}
this.loading = true;
try {
const response = await fetch(
`/api/ppid/grafikberdasarkanjeniskelamin/${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 grafikBerdasarkanJenisKelamin.findMany.load();
return result.data;
} catch (error) {
console.error("Error update data:", error);
toast.error("Gagal update data grafik berdasarkan jenis kelamin");
} finally {
this.loading = false;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
grafikBerdasarkanJenisKelamin.delete.loading = true;
const response = await fetch(`/api/ppid/grafikberdasarkanjeniskelamin/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Grafik berdasarkan jenis kelamin berhasil dihapus");
await grafikBerdasarkanJenisKelamin.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus grafik berdasarkan jenis kelamin");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus grafik berdasarkan jenis kelamin");
} finally {
grafikBerdasarkanJenisKelamin.delete.loading = false;
}
},
}
});
const stateGrafikBerdasarkanJenisKelamin = proxy({
grafikBerdasarkanJenisKelamin,
});
export default stateGrafikBerdasarkanJenisKelamin;
export default grafikBerdasarkanJenisKelamin;

View File

@@ -13,6 +13,7 @@ const templateGrafikResponden = z.object({
type GrafikResponden = Prisma.GrafikBerdasarkanRespondenGetPayload<{
select: {
id: true;
sangatbaik: true;
baik: true;
kurangbaik: true;
@@ -20,7 +21,7 @@ type GrafikResponden = Prisma.GrafikBerdasarkanRespondenGetPayload<{
};
}>;
const defaultForm: GrafikResponden = {
const defaultForm: Omit<GrafikResponden, 'id'> & { id?: string } = {
sangatbaik: "",
baik: "",
kurangbaik: "",
@@ -47,9 +48,18 @@ const grafikBerdasarkanResponden = proxy({
"create"
].post(grafikBerdasarkanResponden.create.form);
if (res.status === 200) {
grafikBerdasarkanResponden.create.form = defaultForm;
grafikBerdasarkanResponden.findMany.load();
return toast.success("success create");
const id = res.data?.data?.id;
if (id) {
toast.success("Success create");
grafikBerdasarkanResponden.create.form = {
sangatbaik: "",
baik: "",
kurangbaik: "",
tidakbaik: "",
};
grafikBerdasarkanResponden.findMany.load();
return id;
}
}
return toast.error("failed create");
} catch (error) {
@@ -75,10 +85,112 @@ const grafikBerdasarkanResponden = proxy({
}
},
},
findUnique: {
data: null as Prisma.GrafikBerdasarkanRespondenGetPayload<{
omit: { isActive: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(
`/api/ppid/grafikberdasarkanresponden/${id}`
);
if (res.ok) {
const data = await res.json();
grafikBerdasarkanResponden.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
grafikBerdasarkanResponden.findUnique.data = null;
}
} catch (error) {
console.error("Error loading grafik berdasarkan responden:", error);
grafikBerdasarkanResponden.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 = templateGrafikResponden.safeParse(this.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return null;
}
this.loading = true;
try {
const response = await fetch(`/api/ppid/grafikberdasarkanresponden/${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 grafikBerdasarkanResponden.findMany.load();
return result.data;
} catch (error) {
console.error("Error update data:", error);
toast.error("Gagal update data grafik berdasarkan responden");
} finally {
this.loading = false;
}
}
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
grafikBerdasarkanResponden.delete.loading = true;
const response = await fetch(`/api/ppid/grafikberdasarkanresponden/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Grafik berdasarkan responden berhasil dihapus");
await grafikBerdasarkanResponden.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus grafik berdasarkan responden");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus grafik berdasarkan responden");
} finally {
grafikBerdasarkanResponden.delete.loading = false;
}
}
}
});
const stateGrafikResponden = proxy({
grafikBerdasarkanResponden,
});
export default stateGrafikResponden;
export default grafikBerdasarkanResponden;

View File

@@ -5,80 +5,182 @@ import { proxy } from "valtio";
import { z } from "zod";
const templateGrafikUmur = z.object({
remaja: z.string().min(2, "Data remaja harus diisi"),
dewasa: z.string().min(2, "Data dewasa harus diisi"),
orangtua: z.string().min(2, "Data orangtua harus diisi"),
lansia: z.string().min(2, "Data lansia harus diisi"),
remaja: z.string().min(2, "Data remaja harus diisi"),
dewasa: z.string().min(2, "Data dewasa harus diisi"),
orangtua: z.string().min(2, "Data orangtua harus diisi"),
lansia: z.string().min(2, "Data lansia harus diisi"),
});
type GrafikUmur = Prisma.GrafikBerdasarkanUmurGetPayload<{
select: {
remaja: true;
dewasa: true;
orangtua: true;
lansia: true;
};
select: {
id: true;
remaja: true;
dewasa: true;
orangtua: true;
lansia: true;
};
}>;
const defaultForm: GrafikUmur = {
remaja: "",
dewasa: "",
orangtua: "",
lansia: "",
const defaultForm: Omit<GrafikUmur, "id"> & { id?: string } = {
remaja: "",
dewasa: "",
orangtua: "",
lansia: "",
};
const grafikBerdasarkanUmur = proxy({
create: {
form: defaultForm,
loading: false,
async create() {
const cek = templateGrafikUmur.safeParse(
grafikBerdasarkanUmur.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
grafikBerdasarkanUmur.create.loading = true;
const res = await ApiFetch.api.ppid.grafikberdasarkanumur[
"create"
].post(grafikBerdasarkanUmur.create.form);
if (res.status === 200) {
grafikBerdasarkanUmur.create.form = defaultForm;
grafikBerdasarkanUmur.findMany.load();
return toast.success("success create");
}
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
} finally {
grafikBerdasarkanUmur.create.loading = false;
}
},
create: {
form: defaultForm,
loading: false,
async create() {
const cek = templateGrafikUmur.safeParse(
grafikBerdasarkanUmur.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
grafikBerdasarkanUmur.create.loading = true;
const res = await ApiFetch.api.ppid.grafikberdasarkanumur[
"create"
].post(grafikBerdasarkanUmur.create.form);
if (res.status === 200) {
const id = res.data?.data?.id;
if (id) {
toast.success("Success create");
grafikBerdasarkanUmur.create.form = {
remaja: "",
dewasa: "",
orangtua: "",
lansia: "",
};
grafikBerdasarkanUmur.findMany.load();
return id;
}
}
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
} finally {
grafikBerdasarkanUmur.create.loading = false;
}
},
findMany: {
data: null as
| Prisma.GrafikBerdasarkanUmurGetPayload<{
omit: { isActive: true };
},
findMany: {
data: null as
| Prisma.GrafikBerdasarkanUmurGetPayload<{
omit: { isActive: true };
}>[]
| null,
loading: false,
async load() {
const res = await ApiFetch.api.ppid.grafikberdasarkanumur[
"find-many"
].get();
if (res.status === 200) {
grafikBerdasarkanUmur.findMany.data = res.data?.data ?? [];
}
},
| null,
loading: false,
async load() {
const res = await ApiFetch.api.ppid.grafikberdasarkanumur[
"find-many"
].get();
if (res.status === 200) {
grafikBerdasarkanUmur.findMany.data = res.data?.data ?? [];
}
},
})
},
findUnique: {
data: null as Prisma.GrafikBerdasarkanUmurGetPayload<{
omit: { isActive: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/ppid/grafikberdasarkanumur/${id}`);
if (res.ok) {
const data = await res.json();
grafikBerdasarkanUmur.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
grafikBerdasarkanUmur.findUnique.data = null;
}
} catch (error) {
console.error("Error loading grafik berdasarkan umur:", error);
grafikBerdasarkanUmur.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 = templateGrafikUmur.safeParse(this.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return null;
}
this.loading = true;
try {
const response = await fetch(`/api/ppid/grafikberdasarkanumur/${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 grafikBerdasarkanUmur.findMany.load();
return result.data;
} catch (error) {
console.error("Error update data:", error);
toast.error("Gagal update data grafik berdasarkan umur");
} finally {
this.loading = false;
}
}
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
const stateGrafikBerdasarkanUmur = proxy({
grafikBerdasarkanUmur,
})
try {
grafikBerdasarkanUmur.delete.loading = true;
export default stateGrafikBerdasarkanUmur;
const response = await fetch(`/api/ppid/grafikberdasarkanumur/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Grafik berdasarkan umur berhasil dihapus");
await grafikBerdasarkanUmur.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus grafik berdasarkan umur");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus grafik berdasarkan umur");
} finally {
grafikBerdasarkanUmur.delete.loading = false;
}
}
}
});
export default grafikBerdasarkanUmur;

View File

@@ -11,12 +11,13 @@ const templateGrafikHasilKepuasanMasyarakat = z.object({
type GrafikHasilKepuasanMasyarakat = Prisma.IndeksKepuasanMasyarakatGetPayload<{
select: {
id: true;
label: true;
kepuasan: true;
};
}>;
const defaultForm: GrafikHasilKepuasanMasyarakat = {
const defaultForm: Omit<GrafikHasilKepuasanMasyarakat, 'id'> & { id?: string } = {
label: "",
kepuasan: "",
};
@@ -41,13 +42,18 @@ const grafikHasilKepuasanMasyarakat = proxy({
grafikHasilKepuasanMasyarakat.create.form
);
if (res.status === 200) {
grafikHasilKepuasanMasyarakat.create.form = {
label: "",
kepuasan: ""
};
grafikHasilKepuasanMasyarakat.findMany.load();
return toast.success("success create");
const id = res.data?.data?.id;
if (id) {
toast.success("Success create");
grafikHasilKepuasanMasyarakat.create.form = {
label: "",
kepuasan: "",
};
grafikHasilKepuasanMasyarakat.findMany.load();
return id;
}
}
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
@@ -58,19 +64,127 @@ const grafikHasilKepuasanMasyarakat = proxy({
},
findMany: {
data: null as
| Prisma.IndeksKepuasanMasyarakatGetPayload<{ omit: { isActive: true } }>[]
| null,
| Prisma.IndeksKepuasanMasyarakatGetPayload<{
omit: { isActive: true };
}>[]
| null,
async load() {
const res = await ApiFetch.api.ppid.grafikhasilkepuasamanmasyarakat["find-many"].get();
if (res.status === 200) {
grafikHasilKepuasanMasyarakat.findMany.data = res.data?.data ?? [];
const res = await ApiFetch.api.ppid.grafikhasilkepuasamanmasyarakat[
"find-many"
].get();
if (res.status === 200) {
grafikHasilKepuasanMasyarakat.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.IndeksKepuasanMasyarakatGetPayload<{
omit: { isActive: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(
`/api/ppid/grafikhasilkepuasamanmasyarakat/${id}`
);
if (res.ok) {
const data = await res.json();
grafikHasilKepuasanMasyarakat.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
grafikHasilKepuasanMasyarakat.findUnique.data = null;
}
} catch (error) {
console.error("Error loading grafik hasil kepuasan masyarakat:", error);
grafikHasilKepuasanMasyarakat.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;
}
// ✅ Validasi pakai Zod
const cek = templateGrafikHasilKepuasanMasyarakat.safeParse(this.form);
if (!cek.success) {
const err = `[${cek.error.issues.map((v) => `${v.path.join(".")}`).join("\n")}] required`;
toast.error(err);
return null;
}
this.loading = true;
try {
const response = await fetch(`/api/ppid/grafikhasilkepuasamanmasyarakat/${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!");
// ✅ Optional: refresh list kalau kamu langsung ke halaman list
await grafikHasilKepuasanMasyarakat.findMany.load();
return result.data;
} catch (error) {
console.error("Error update data:", error);
toast.error("Gagal update data grafik hasil kepuasan masyarakat");
} finally {
this.loading = false;
}
}
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
grafikHasilKepuasanMasyarakat.delete.loading = true;
const response = await fetch(`/api/ppid/grafikhasilkepuasamanmasyarakat/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Grafik hasil kepuasan masyarakat berhasil dihapus");
await grafikHasilKepuasanMasyarakat.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus grafik hasil kepuasan masyarakat");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus grafik hasil kepuasan masyarakat");
} finally {
grafikHasilKepuasanMasyarakat.delete.loading = false;
}
},
}
});
const stateGrafikHasilKepuasanMasyarakat = proxy({
grafikHasilKepuasanMasyarakat,
});
export default stateGrafikHasilKepuasanMasyarakat;
export default grafikHasilKepuasanMasyarakat;

View File

@@ -1,4 +1,3 @@
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
@@ -13,11 +12,18 @@ const templateForm = z.object({
riwayat: z.string().min(3, "Riwayat minimal 3 karakter"),
pengalaman: z.string().min(3, "Pengalaman minimal 3 karakter"),
unggulan: z.string().min(3, "Unggulan minimal 3 karakter"),
imageId: z.string().min(1, "Gambar wajib dipilih"),
});
/**
* Tipe data ProfilePPID yang digunakan dalam form dan API, berdasarkan Prisma schema.
*/
const defaultForm = {
name: "",
biodata: "",
riwayat: "",
pengalaman: "",
unggulan: "",
imageId: "",
};
type ProfilePPIDForm = Prisma.ProfilePPIDGetPayload<{
select: {
id: true;
@@ -26,147 +32,170 @@ type ProfilePPIDForm = Prisma.ProfilePPIDGetPayload<{
riwayat: true;
pengalaman: true;
unggulan: true;
imageUrl: true;
imageId: true;
image?: {
select: {
link: true;
};
};
};
}>;
/**
* State utama ProfilePPID yang mencakup fitur:
* - Ambil data berdasarkan ID
* - Update data
* - Upload gambar
* Improved State Management - Consolidated and more robust
*/
const stateProfilePPID = proxy({
/**
* Bagian untuk ambil data berdasarkan ID
*/
findById: {
// Consolidated data management
profile: {
data: null as ProfilePPIDForm | null,
loading: false,
error: null as string | null,
/**
* Inisialisasi data kosong ke dalam state.
*/
initialize() {
stateProfilePPID.findById.data = {
id: '',
name: '',
biodata: '',
riwayat: '',
pengalaman: '',
unggulan: '',
imageUrl: ''
} as ProfilePPIDForm;
},
/**
* Mengambil data profil berdasarkan ID.
* @param {string} id - ID dari profile yang ingin diambil.
*/
// Single method to load profile data
async load(id: string) {
try {
stateProfilePPID.findById.loading = true;
const res = await ApiFetch.api.ppid.profileppid["find-by-id"].get({
query: { id },
});
if (!id) {
toast.warn("ID tidak valid");
return null;
}
if (res.status === 200) {
stateProfilePPID.findById.data = res.data?.data ?? null;
this.loading = true;
this.error = null;
try {
const response = await fetch(`/api/ppid/profileppid/${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 {
toast.error("Gagal mengambil data profile");
throw new Error(result.message || "Gagal mengambil data profile");
}
} catch (error) {
console.error((error as Error).message);
const errorMessage = (error as Error).message;
this.error = errorMessage;
console.error("Load profile error:", errorMessage);
toast.error("Terjadi kesalahan saat mengambil data profile");
return null;
} finally {
stateProfilePPID.findById.loading = false;
this.loading = false;
}
},
// Reset profile data
reset() {
this.data = null;
this.error = null;
this.loading = false;
}
},
/**
* Bagian untuk update data profile
*/
update: {
// Edit form management
editForm: {
id: "",
form: { ...defaultForm },
loading: false,
error: null as string | null,
isReadOnly: false, // Flag untuk data yang tidak bisa diedit
/**
* Melakukan validasi dan menyimpan perubahan data profile ke server.
* @param {ProfilePPIDForm} data - Data profil yang akan disimpan.
*/
async save(data: ProfilePPIDForm) {
const cek = templateForm.safeParse(data);
// Initialize form with profile data
initialize(profileData: ProfilePPIDForm) {
this.id = profileData.id;
this.isReadOnly = false; // Semua data bisa diedit
this.form = {
name: profileData.name || "",
biodata: profileData.biodata || "",
riwayat: profileData.riwayat || "",
pengalaman: profileData.pengalaman || "",
unggulan: profileData.unggulan || "",
imageId: profileData.imageId || "",
};
},
if (!cek.success) {
const errors = cek.error.issues
// Update form field
updateField(field: keyof typeof defaultForm, value: string) {
this.form[field] = value;
},
// Submit form
async submit() {
// Validate form
const validation = templateForm.safeParse(this.form);
if (!validation.success) {
const errors = validation.error.issues
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
.join(", ");
toast.error(`Form tidak valid: ${errors}`);
return;
return false;
}
try {
stateProfilePPID.update.loading = true;
const res = await ApiFetch.api.ppid.profileppid["update"].post(data);
this.loading = true;
this.error = null;
if (res.status === 200) {
try {
const response = await fetch(`/api/ppid/profileppid/${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");
await stateProfilePPID.findById.load(data.id);
// Refresh profile data
await stateProfilePPID.profile.load(this.id);
return true;
} else {
toast.error("Gagal update profile");
throw new Error(result.message || "Gagal update profile");
}
} catch (error) {
console.error((error as Error).message);
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 {
stateProfilePPID.update.loading = false;
this.loading = false;
}
},
// Reset form
reset() {
this.id = "";
this.form = { ...defaultForm };
this.error = null;
this.loading = false;
this.isReadOnly = false;
}
},
/**
* Bagian untuk upload gambar profil
*/
uploadImage: {
loading: false,
/**
* Mengunggah gambar profil berdasarkan ID.
* @param {File} file - File gambar yang akan diunggah.
* @param {string} id - ID dari profil yang akan diperbarui gambarnya.
*/
async save(file: File, id: string) {
if (!file || !id) {
toast.error("File atau ID harus disertakan");
return;
}
try {
stateProfilePPID.uploadImage.loading = true;
const form = new FormData();
form.append("file", file);
form.append("id", id);
const res = await ApiFetch.api.ppid.profileppid["edit-img"].post(form);
if (res.status === 200) {
toast.success("Berhasil mengunggah gambar");
await stateProfilePPID.findById.load(id);
} else {
toast.error("Gagal mengunggah gambar");
}
} catch (error) {
console.error((error as Error).message);
toast.error("Terjadi kesalahan saat mengunggah gambar");
} finally {
stateProfilePPID.uploadImage.loading = false;
}
},
// Helper methods
async loadForEdit(id: string) {
const profileData = await this.profile.load(id);
if (profileData) {
this.editForm.initialize(profileData);
}
return profileData;
},
reset() {
this.profile.reset();
this.editForm.reset();
}
});
/**
* Ekspor state utama ProfilePPID untuk digunakan di komponen lain.
*/
export default stateProfilePPID;
export default stateProfilePPID;

View File

@@ -0,0 +1,169 @@
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
const templateForm = z.object({
name: z.string().min(3, "Nama minimal 3 karakter"),
imageId: z.string().min(1, "Gambar wajib dipilih"),
})
const defaultForm = {
name: "",
imageId: "",
};
type StrukturPPIDForm = Prisma.StrukturPPIDGetPayload<{
select: {
id: true;
name: true;
imageId: true;
image?: {
select: {
link: true;
};
};
};
}>;
const stateStrukturPPID = proxy({
struktur: {
data: null as StrukturPPIDForm | null,
loading: false,
error: null as string | null,
async load(id: string) {
if(!id) {
toast.warn("ID tidak valid")
return null
}
this.loading = true;
this.error = null;
try {
const response = await fetch(`/api/ppid/strukturppid/${id}`);
if(!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const result = await response.json();
if(result.success) {
this.data = result.data;
return result.data
} else {
throw new Error(result.message || "Gagal mengambil data struktur")
}
} catch (error) {
const errorMessage = (error as Error).message;
this.error = errorMessage;
console.error("Load struktur error:", errorMessage);
toast.error("Terjadi kesalahan saat mengambil data struktur");
return null;
} finally {
this.loading = false;
}
},
reset() {
this.data = null;
this.error = null;
this.loading = false;
}
},
editStruktur: {
id: "",
form: { ...defaultForm },
loading: false,
error: null as string | null,
isReadOnly: false,
initialize(strukturData: StrukturPPIDForm) {
this.id = strukturData.id;
this.isReadOnly = false;
this.form = {
name: strukturData.name || "",
imageId: strukturData.imageId || "",
};
},
updateField(field: keyof typeof defaultForm, value: string) {
this.form[field] = value;
},
async submit() {
const validation = templateForm.safeParse(this.form);
if (!validation.success) {
const errors = validation.error.issues
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
.join(", ");
toast.error(`Form tidak valid: ${errors}`);
return false;
}
this.loading = true;
this.error = null;
try {
const response = await fetch(`/api/ppid/strukturppid/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update struktur");
await stateStrukturPPID.struktur.load(this.id);
return true;
} else {
throw new Error(result.message || "Gagal update struktur");
}
} catch (error) {
const errorMessage = (error as Error).message;
this.error = errorMessage;
console.error("Update struktur error:", errorMessage);
toast.error("Terjadi kesalahan saat update struktur");
return false;
} finally {
this.loading = false;
}
},
reset() {
this.id = "";
this.form = { ...defaultForm };
this.error = null;
this.loading = false;
this.isReadOnly = false;
}
},
async loadForEdit(id: string) {
const strukturData = await this.struktur.load(id);
if (strukturData) {
this.editStruktur.initialize(strukturData);
}
return strukturData;
},
reset() {
this.struktur.reset();
this.editStruktur.reset();
}
})
export default stateStrukturPPID;

View File

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

View File

@@ -4,9 +4,7 @@ import { Box, Button, Grid, GridCol, Image, Paper, Skeleton, Stack, Table, Table
import { useShallowEffect } from '@mantine/hooks';
import { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus';
import stateDashboardBerita from '../../_state/desa/berita';
@@ -32,76 +30,8 @@ function Page() {
);
}
// function BeritaList() {
// const beritaState = useProxy(stateDashboardBerita)
// useShallowEffect(() => {
// beritaState.berita.findMany.load()
// }, [])
// const router = useRouter()
// if (!beritaState.berita.findMany.data) return <Stack py={10}>
// {Array.from({ length: 10 }).map((v, k) => <Skeleton key={k} h={40} />)}
// </Stack>
// return (
// <Box py={10}>
// <Paper bg={colors['white-1']} p={'md'}>
// <Stack>
// <Text fz={"xl"} fw={"bold"}>List Berita</Text>
// <SimpleGrid cols={{ base: 1, md: 4 }}>
// {beritaState.berita.findMany.data?.map((item) => (
// <Paper key={item.id} bg={colors['BG-trans']} p={'md'}>
// <Box >
// <Flex justify="flex-end" mt={10}>
// <ActionIcon
// onClick={() => beritaState.berita.delete.byId(item.id)}
// disabled={beritaState.berita.delete.loading}
// color={colors['blue-button']} variant='transparent'>
// <IconX size={20} />
// </ActionIcon>
// <ActionIcon onClick={() => {
// router.push("/desa/berita/edit");
// }} color={colors['blue-button']} variant='transparent'>
// <IconEdit size={20} />
// </ActionIcon>
// </Flex>
// <Text fw={"bold"} fz={"sm"}>
// Kategori
// </Text>
// <Text>{item.kategoriBerita?.name}</Text>
// <Text fw={"bold"} fz={"sm"}>
// Judul
// </Text>
// <Text>{item.judul}</Text>
// <Text lineClamp={1} fw={"bold"} fz={"sm"}>
// Deskripsi
// </Text>
// <Text size='sm' lineClamp={2}>{item.deskripsi}</Text>
// <Text fw={"bold"} fz={"sm"}>
// Gambar
// </Text>
// <Image w={{ base: 100, md: 150 }} src={item.image?.link} alt="gambar" />
// </Box>
// </Paper>
// ))}
// </SimpleGrid>
// </Stack>
// </Paper>
// </Box>
// )
// }
function BeritaList() {
const beritaState = useProxy(stateDashboardBerita)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
useShallowEffect(() => {
beritaState.berita.findMany.load()
@@ -109,14 +39,6 @@ function BeritaList() {
const router = useRouter()
const handleHapus = () => {
if (selectedId) {
beritaState.berita.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
}
}
if (!beritaState.berita.findMany.data) {
return (
<Stack py={10}>
@@ -175,13 +97,6 @@ function BeritaList() {
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus berita ini?'
/>
</Box>
)
}

View File

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

View File

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

View File

@@ -0,0 +1,109 @@
'use client'
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function CreateFoto() {
const fotoState = useProxy(stateGallery.foto)
const router = useRouter();
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const resetForm = () => {
fotoState.create.form = {
name: "",
deskripsi: "",
imagesId: "",
};
setPreviewImage(null)
setFile(null)
};
const handleSubmit = async () => {
if (!file) {
return toast.warn("Pilih file gambar terlebih dahulu");
}
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
fotoState.create.form.imagesId = uploaded.id;
await fotoState.create.create();
resetForm();
router.push("/admin/desa/gallery/foto")
};
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Foto</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Judul Foto</Text>}
placeholder='Masukkan judul foto'
value={fotoState.create.form.name}
onChange={(val) => {
fotoState.create.form.name = val.target.value;
}}
/>
<FileInput
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar</Text>}
value={file}
onChange={async (e) => {
if (!e) return;
setFile(e);
const base64 = await e.arrayBuffer().then((buf) =>
"data:image/png;base64," + Buffer.from(buf).toString("base64")
);
setPreviewImage(base64);
}}
/>
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg={"gray"}>
<IconImageInPicture />
</Center>
)}
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Foto</Text>
<CreateEditor
value={fotoState.create.form.deskripsi}
onChange={(val) => {
fotoState.create.form.deskripsi = val;
}}
/>
</Box>
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreateFoto;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,52 +0,0 @@
import colors from '@/con/colors';
import { Box, Button, Center, Group, Paper, SimpleGrid, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconUpload } from '@tabler/icons-react';
import { DesaEditor } from '../../../_com/desaEditor';
import ListFoto from './listPage';
function Foto() {
return (
<Box py={10}>
<SimpleGrid cols={{ base: 1, md: 2 }}>
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={3}>Foto</Title>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Tanggal Foto</Text>}
placeholder="2022-01-01"
/>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Judul Foto</Text>}
placeholder="Judul Foto"
/>
<Text fz={"sm"} fw={"bold"}>Upload Foto</Text>
<Box bg={colors['BG-trans']} p={"md"}>
<Center>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Center>
</Box>
<Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi Foto</Text>
<DesaEditor
showSubmit={false}
/>
</Box>
<Group>
<Button
mt={10}
bg={colors['blue-button']}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
<ListFoto/>
</SimpleGrid>
</Box>
);
}
export default Foto;

View File

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

View File

@@ -1,52 +0,0 @@
import colors from '@/con/colors';
import { Box, Button, Center, Group, Paper, SimpleGrid, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconUpload } from '@tabler/icons-react';
import { DesaEditor } from '../../../_com/desaEditor';
import ListVideo from './listPage';
function Video() {
return (
<Box py={10}>
<SimpleGrid cols={{ base: 1, md: 2 }}>
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={3}>Video</Title>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Tanggal Video</Text>}
placeholder="2022-01-01"
/>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Judul Video</Text>}
placeholder="Judul Video"
/>
<Text fz={"sm"} fw={"bold"}>Upload Video</Text>
<Box bg={colors['BG-trans']} p={"md"}>
<Center>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Center>
</Box>
<Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi Video</Text>
<DesaEditor
showSubmit={false}
/>
</Box>
<Group>
<Button
mt={10}
bg={colors['blue-button']}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
<ListVideo/>
</SimpleGrid>
</Box>
);
}
export default Video;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,103 @@
'use client'
/* eslint-disable react-hooks/exhaustive-deps */
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditPelayananPendudukNonPermanent() {
const router = useRouter();
const params = useParams()
const statePendudukNonPermanent = useProxy(stateLayananDesa.pelayananPendudukNonPermanen)
const [formData, setFormData] = useState({
name: statePendudukNonPermanent.findById.data?.name || '',
deskripsi: statePendudukNonPermanent.findById.data?.deskripsi || '',
})
useEffect(() => {
const loadPelayananPerizinan = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await statePendudukNonPermanent.update.load(id);
if (data) {
setFormData({
name: data.name || '',
deskripsi: data.deskripsi || '',
});
}
} catch (error) {
console.error("Error loading pelayanan perizinan berusaha:", error);
toast.error("Gagal memuat data pelayanan perizinan berusaha");
}
};
loadPelayananPerizinan();
}, [params?.id]);
const handleSubmit = async () => {
if (statePendudukNonPermanent.findById.data) {
statePendudukNonPermanent.findById.data.name = formData.name;
statePendudukNonPermanent.findById.data.deskripsi = formData.deskripsi;
statePendudukNonPermanent.update.update(statePendudukNonPermanent.findById.data)
}
router.push('/admin/desa/layanan/pelayanan_penduduk_non_permanent')
}
return (
<Box>
<Stack gap={'xs'}>
<Box>
<Button
variant={'subtle'}
onClick={() => router.back()}
>
<IconArrowBack color={colors['blue-button']} size={20} />
</Button>
</Box>
<Box>
<Paper bg={colors['white-1']} p={'md'} radius={10} w={{ base: '100%', md: '50%' }}>
<Stack gap={'xs'}>
<Title order={3}>Edit Pelayanan Penduduk Non Permanent</Title>
<Text fw={"bold"}>Judul</Text>
<TextInput
value={formData.name}
onChange={(val) => {
setFormData({
...formData,
name: val.target.value,
})
}}
/>
<Text fw={"bold"}>Deskripsi</Text>
<EditEditor
value={formData.deskripsi}
onChange={(val) => {
setFormData({
...formData,
deskripsi: val,
})
}}
/>
<Group>
<Button
bg={colors['blue-button']}
onClick={handleSubmit}
loading={statePendudukNonPermanent.update.loading}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
</Stack>
</Box>
);
}
export default EditPelayananPendudukNonPermanent;

View File

@@ -0,0 +1,51 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Grid, GridCol, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import stateLayananDesa from '../../../_state/desa/layananDesa';
function SuratKeterangan() {
const router = useRouter()
const pelayananPendudukNonPermanen = useProxy(stateLayananDesa.pelayananPendudukNonPermanen)
useShallowEffect(() => {
pelayananPendudukNonPermanen.findById.load('1')
}, [])
if (!pelayananPendudukNonPermanen.findById.data) {
return (
<Stack>
<Skeleton radius={10} h={800} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Paper bg={colors['BG-trans']} p={'md'}>
<Box py={15}>
<Stack gap={"xs"}>
<Grid>
<GridCol span={{ base: 12, md: 11 }}>
<Text fz={"h4"} fw={"bold"}>Preview Pelayanan Perizinan Berusaha</Text>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button bg={colors['blue-button']} onClick={() => router.push('/admin/desa/layanan/pelayanan_penduduk_non_permanent/edit')}>
<IconEdit size={16} />
</Button>
</GridCol>
</Grid>
</Stack>
</Box>
<Text fz={{ base: "h4", md: 'h2' }} fw={"bold"}>{pelayananPendudukNonPermanen.findById.data.name}</Text>
<Text py={10} ta={"justify"} fz={{ base: "sm", md: 'h3' }} dangerouslySetInnerHTML={{ __html: pelayananPendudukNonPermanen.findById.data.deskripsi }} />
</Paper>
</Paper>
</Box>
);
}
export default SuratKeterangan;

View File

@@ -0,0 +1,116 @@
'use client'
/* eslint-disable react-hooks/exhaustive-deps */
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditPelayananPerizinanBerusaha() {
const router = useRouter();
const params = useParams()
const statePerizinanBerusaha = useProxy(stateLayananDesa.pelayananPerizinanBerusaha)
const [formData, setFormData] = useState({
name: statePerizinanBerusaha.findById.data?.name || '',
deskripsi: statePerizinanBerusaha.findById.data?.deskripsi || '',
link: statePerizinanBerusaha.findById.data?.link || '',
})
useEffect(() => {
const loadPelayananPerizinan = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await statePerizinanBerusaha.update.load(id);
if (data) {
setFormData({
name: data.name || '',
deskripsi: data.deskripsi || '',
link: data.link || '',
});
}
} catch (error) {
console.error("Error loading pelayanan perizinan berusaha:", error);
toast.error("Gagal memuat data pelayanan perizinan berusaha");
}
};
loadPelayananPerizinan();
}, [params?.id]);
const handleSubmit = async () => {
if (statePerizinanBerusaha.findById.data) {
statePerizinanBerusaha.findById.data.name = formData.name;
statePerizinanBerusaha.findById.data.deskripsi = formData.deskripsi;
statePerizinanBerusaha.findById.data.link = formData.link;
statePerizinanBerusaha.update.update(statePerizinanBerusaha.findById.data)
}
router.push('/admin/desa/layanan/pelayanan_perizinan_berusaha')
}
return (
<Box>
<Stack gap={'xs'}>
<Box>
<Button
variant={'subtle'}
onClick={() => router.back()}
>
<IconArrowBack color={colors['blue-button']} size={20} />
</Button>
</Box>
<Box>
<Paper bg={colors['white-1']} p={'md'} radius={10} w={{ base: '100%', md: '50%' }}>
<Stack gap={'xs'}>
<Title order={3}>Edit Pelayanan Perizinan Berusaha</Title>
<Text fw={"bold"}>Judul</Text>
<TextInput
value={formData.name}
onChange={(val) => {
setFormData({
...formData,
name: val.target.value,
})
}}
/>
<Text fw={"bold"}>Link</Text>
<TextInput
value={formData.link}
onChange={(val) => {
setFormData({
...formData,
link: val.target.value,
})
}}
/>
<Text fw={"bold"}>Deskripsi</Text>
<EditEditor
value={formData.deskripsi}
onChange={(val) => {
setFormData({
...formData,
deskripsi: val,
})
}}
/>
<Group>
<Button
bg={colors['blue-button']}
onClick={handleSubmit}
loading={statePerizinanBerusaha.update.loading}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
</Stack>
</Box>
);
}
export default EditPelayananPerizinanBerusaha;

View File

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

View File

@@ -0,0 +1,132 @@
'use client'
/* eslint-disable react-hooks/exhaustive-deps */
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditSuratKeterangan() {
const router = useRouter()
const params = useParams()
const stateSurat = useProxy(stateLayananDesa.suratKeterangan)
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({
name: stateSurat.edit.form.name,
deskripsi: stateSurat.edit.form.deskripsi,
imageId: stateSurat.edit.form.imageId,
})
useEffect(() => {
const loadSurat = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await stateSurat.edit.load(id);
if (data) {
setFormData({
name: data.name,
deskripsi: data.deskripsi,
imageId: data.imageId,
});
if (data?.image?.link) {
setPreviewImage(data.image.link);
}
}
} catch (error) {
console.error("Error loading surat:", error);
toast.error("Gagal memuat data surat");
}
};
loadSurat();
}, [params?.id]);
const handleSubmit = async () => {
try {
stateSurat.edit.form = {
...stateSurat.edit.form,
name: formData.name,
deskripsi: formData.deskripsi,
imageId: formData.imageId,
}
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
stateSurat.edit.form.imageId = uploaded.id;
}
await stateSurat.edit.update()
toast.success("Surat berhasil diperbarui!")
router.push("/admin/desa/layanan/pelayanan_surat_keterangan")
} catch (error) {
console.error("Error updating surat:", error);
toast.error("Terjadi kesalahan saat memperbarui surat");
}
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Edit Surat Keterangan</Title>
<TextInput
value={formData.name}
onChange={(val) => {
setFormData({ ...formData, name: val.target.value });
}}
label={<Text fz={"sm"} fw={"bold"}>Nama Surat Keterangan</Text>}
placeholder="masukkan nama surat keterangan"
/>
<Box>
<Text fz={"sm"} fw={"bold"}>Konten</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
setFormData({ ...formData, deskripsi: htmlContent });
}}
/>
</Box>
<FileInput
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar Konten</Text>}
value={file}
onChange={async (e) => {
if (!e) return;
setFile(e);
const base64 = await e.arrayBuffer().then((buf) =>
"data:image/png;base64," + Buffer.from(buf).toString("base64")
);
setPreviewImage(base64);
}}
/>
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg={"gray"}>
<IconImageInPicture />
</Center>
)}
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
</Stack>
</Paper>
</Box>
);
}
export default EditSuratKeterangan;

View File

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

View File

@@ -0,0 +1,104 @@
'use client'
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function CreateSuratKeterangan() {
const stateSurat = useProxy(stateLayananDesa.suratKeterangan)
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const router = useRouter()
const resetForm = () => {
stateSurat.create.form = {
name: "",
deskripsi: "",
imageId: "",
}
setPreviewImage(null)
setFile(null)
}
const handleSubmit = async () => {
if (!file) {
return toast.error("Silahkan pilih file gambar terlebih dahulu")
}
const res = await ApiFetch.api.fileStorage.create.post({
file: file,
name: file.name
})
const uploaded = res.data?.data
if (!uploaded?.id) {
return toast.error("Gagal upload gambar")
}
stateSurat.create.form.imageId = uploaded.id
await stateSurat.create.create()
resetForm()
router.push("/admin/desa/layanan/pelayanan_surat_keterangan")
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Create Surat Keterangan</Title>
<TextInput
value={stateSurat.create.form.name}
onChange={(val) => {
stateSurat.create.form.name = val.target.value;
}}
label={<Text fz={"sm"} fw={"bold"}>Nama Surat Keterangan</Text>}
placeholder="masukkan nama surat keterangan"
/>
<Box>
<Text fz={"sm"} fw={"bold"}>Konten</Text>
<CreateEditor
value={stateSurat.create.form.deskripsi}
onChange={(htmlContent) => {
stateSurat.create.form.deskripsi = htmlContent;
}}
/>
</Box>
<FileInput
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar Konten</Text>}
value={file}
onChange={async (e) => {
if (!e) return;
setFile(e);
const base64 = await e.arrayBuffer().then((buf) =>
"data:image/png;base64," + Buffer.from(buf).toString("base64")
);
setPreviewImage(base64);
}}
/>
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg={"gray"}>
<IconImageInPicture />
</Center>
)}
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
</Stack>
</Paper>
</Box>
);
}
export default CreateSuratKeterangan;

View File

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

View File

@@ -0,0 +1,103 @@
'use client'
/* eslint-disable react-hooks/exhaustive-deps */
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditPelayananTelunjukSakti() {
const stateTelunjukDesa = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa)
const router = useRouter()
const params = useParams()
const [formData, setFormData] = useState({
name: stateTelunjukDesa.edit.form.name,
deskripsi: stateTelunjukDesa.edit.form.deskripsi,
link: stateTelunjukDesa.edit.form.link,
})
useEffect(() => {
const loadPelayananTelunjukSakti = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await stateTelunjukDesa.edit.load(id);
if (data) {
setFormData({
name: data.name,
deskripsi: data.deskripsi,
link: data.link,
});
}
} catch (error) {
console.error("Error loading pelayanan telunjuk sakti:", error);
toast.error("Gagal memuat data pelayanan telunjuk sakti");
}
};
loadPelayananTelunjukSakti();
}, [params?.id]);
const handleSubmit = async () => {
try {
stateTelunjukDesa.edit.form = {
...stateTelunjukDesa.edit.form,
name: formData.name,
deskripsi: formData.deskripsi,
link: formData.link,
}
await stateTelunjukDesa.edit.update()
toast.success("Pelayanan telunjuk sakti berhasil diperbarui!")
router.push("/admin/desa/layanan/pelayanan_telunjuk_sakti_desa")
} catch (error) {
console.error("Error updating pelayanan telunjuk sakti:", error);
toast.error("Terjadi kesalahan saat memperbarui pelayanan telunjuk sakti");
}
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Edit Surat Keterangan</Title>
<TextInput
value={formData.name}
onChange={(val) => {
setFormData({ ...formData, name: val.target.value });
}}
label={<Text fz={"sm"} fw={"bold"}>Nama Surat Keterangan</Text>}
placeholder="masukkan nama surat keterangan"
/>
<TextInput
value={formData.link}
onChange={(val) => {
setFormData({ ...formData, link: val.target.value });
}}
label={<Text fz={"sm"} fw={"bold"}>Link</Text>}
placeholder="masukkan link"
/>
<Box>
<Text fz={"sm"} fw={"bold"}>Konten</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
setFormData({ ...formData, deskripsi: htmlContent });
}}
/>
</Box>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
</Stack>
</Paper>
</Box>
);
}
export default EditPelayananTelunjukSakti;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,147 @@
'use client'
/* eslint-disable react-hooks/exhaustive-deps */
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import penghargaanState from '@/app/admin/(dashboard)/_state/desa/penghargaan';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Paper, Stack, Title, TextInput, FileInput, Center, Text, Image } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditPenghargaan() {
const statePenghargaan = useProxy(penghargaanState)
const router = useRouter()
const params = useParams()
const [previewImage, setPreviewImage] = useState<string | null>(null)
const [file, setFile] = useState<File | null>(null)
const [formData, setFormData] = useState({
name: statePenghargaan.findUnique.data?.name || '',
juara: statePenghargaan.findUnique.data?.juara || '',
deskripsi: statePenghargaan.findUnique.data?.deskripsi || '',
imageId: statePenghargaan.findUnique.data?.imageId || '',
})
useEffect(() => {
const loadPenghargaan = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await statePenghargaan.edit.load(id);
if (data) {
setFormData({
name: data.name || '',
juara: data.juara || '',
deskripsi: data.deskripsi || '',
imageId: data.imageId || '',
});
if (data?.image?.link) {
setPreviewImage(data.image.link);
}
}
} catch (error) {
console.error("Error loading penghargaan:", error);
toast.error("Gagal memuat data penghargaan");
}
};
loadPenghargaan();
}, [params?.id]);
const handleSubmit = async () => {
try {
statePenghargaan.edit.form = {
...statePenghargaan.edit.form,
name: formData.name,
juara: formData.juara,
deskripsi: formData.deskripsi,
imageId: formData.imageId,
}
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
statePenghargaan.edit.form.imageId = uploaded.id;
}
await statePenghargaan.edit.update();
toast.success("Penghargaan berhasil diperbarui!");
router.push("/admin/desa/penghargaan");
} catch (error) {
console.error("Error updating penghargaan:", error);
toast.error("Terjadi kesalahan saat memperbarui penghargaan");
}
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors["blue-button"]} size={30} />
</Button>
</Box>
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Edit Penghargaan</Title>
<TextInput
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
placeholder="masukkan judul"
/>
<TextInput
value={formData.juara}
onChange={(e) => setFormData({ ...formData, juara: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Juara</Text>}
placeholder="masukkan juara"
/>
<FileInput
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar Baru (Opsional)</Text>}
value={file}
onChange={async (e) => {
if (!e) return;
setFile(e);
const base64 = await e.arrayBuffer().then((buf) =>
"data:image/png;base64," + Buffer.from(buf).toString("base64")
);
setPreviewImage(base64);
}}
/>
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg={"gray"}>
<IconImageInPicture />
</Center>
)}
<Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }));
statePenghargaan.edit.form.deskripsi = htmlContent;
}}
/>
</Box>
<Button onClick={handleSubmit}>Edit Penghargaan</Button>
</Stack>
</Paper>
</Box>
);
}
export default EditPenghargaan;

View File

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

View File

@@ -0,0 +1,114 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import penghargaanState from '../../../_state/desa/penghargaan';
import ApiFetch from '@/lib/api-fetch';
import CreateEditor from '../../../_com/createEditor';
function CreatePenghargaan() {
const statePenghargaan = useProxy(penghargaanState)
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const router = useRouter()
const resetForm = () => {
statePenghargaan.create.form = {
name: "",
juara: "",
deskripsi: "",
imageId: "",
}
setPreviewImage(null)
setFile(null)
}
const handleSubmit = async () => {
if (!file) {
return toast.error("Silahkan pilih file gambar terlebih dahulu")
}
const res = await ApiFetch.api.fileStorage.create.post({
file: file,
name: file.name
})
const uploaded = res.data?.data
if (!uploaded?.id) {
return toast.error("Gagal upload gambar")
}
statePenghargaan.create.form.imageId = uploaded.id
await statePenghargaan.create.create()
resetForm()
router.push("/admin/desa/penghargaan")
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Create Penghargaan</Title>
<TextInput
value={statePenghargaan.create.form.name}
onChange={(val) => {
statePenghargaan.create.form.name = val.target.value;
}}
label={<Text fz={"sm"} fw={"bold"}>Nama Penghargaan</Text>}
placeholder="masukkan nama penghargaan"
/>
<TextInput
value={statePenghargaan.create.form.juara}
onChange={(val) => {
statePenghargaan.create.form.juara = val.target.value;
}}
label={<Text fz={"sm"} fw={"bold"}>Juara</Text>}
placeholder="masukkan juara"
/>
<Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
<CreateEditor
value={statePenghargaan.create.form.deskripsi}
onChange={(htmlContent) => {
statePenghargaan.create.form.deskripsi = htmlContent;
}}
/>
</Box>
<FileInput
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar Konten</Text>}
value={file}
onChange={async (e) => {
if (!e) return;
setFile(e);
const base64 = await e.arrayBuffer().then((buf) =>
"data:image/png;base64," + Buffer.from(buf).toString("base64")
);
setPreviewImage(base64);
}}
/>
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg={"gray"}>
<IconImageInPicture />
</Center>
)}
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
</Stack>
</Paper>
</Box>
);
}
export default CreatePenghargaan;

View File

@@ -1,34 +1,69 @@
'use client'
import penghargaanState from '@/app/admin/(dashboard)/_state/desa/penghargaan';
import colors from '@/con/colors';
import { Box, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import React from 'react';
import Penghargaan from './ui/penghargaan/page';
import GambarPerhargaan from './ui/gambar_perhargaan/page';
import { Box, Button, Image, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import JudulListTab from '../../_com/jusulListTab';
function Page() {
function Penghargaan() {
const state = useProxy(penghargaanState)
const router = useRouter()
useShallowEffect(() => {
state.findMany.load()
}, [])
if (!state.findMany.data) {
return(
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Stack>
<Title order={3}>Penghargaan</Title>
<Tabs color={colors['blue-button']} variant='pills' defaultValue={"Penghargaan"}>
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
<TabsTab value="Penghargaan">
Penghargaan
</TabsTab>
<TabsTab value="Gambar Penghargaan">
Gambar Penghargaan
</TabsTab>
</TabsList>
<TabsPanel value="Penghargaan">
<Penghargaan/>
</TabsPanel>
<TabsPanel value="Gambar Penghargaan">
<GambarPerhargaan/>
</TabsPanel>
</Tabs>
</Stack>
<Paper bg={colors['white-1']} p={'md'}>
<JudulListTab
title='List Penghargaan'
href='/admin/desa/penghargaan/create'
placeholder='pencarian'
searchIcon={<IconSearch size={16} />}
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Image</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{state.findMany.data?.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd>
<TableTd>
<Image w={100} src={item.image?.link} alt="gambar" />
</TableTd>
<TableTd>
<Text>
<Button onClick={() => router.push(`/admin/desa/penghargaan/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</Text>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>
</Box>
);
}
export default Page;
export default Penghargaan;

View File

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

View File

@@ -1,50 +0,0 @@
import colors from '@/con/colors';
import { Box, Paper, SimpleGrid, Stack, Title, Text, Group, Button, TextInput, Center } from '@mantine/core';
import React from 'react';
import { DesaEditor } from '../../../_com/desaEditor';
import ListGambarPenghargaan from './listPage';
import { IconUpload } from '@tabler/icons-react';
function GambarPerhargaan() {
return (
<Box py={10}>
<Stack gap={"xs"}>
<SimpleGrid cols={{ base: 1, md: 2 }}>
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={3}>Tambah Gambar Penghargaan</Title>
<TextInput
label="Judul Gambar Penghargaan"
placeholder='masukkan judul gambar penghargaan'
/>
<Text fw={"bold"}>Deskripsi Gambar Penghargaan</Text>
<DesaEditor showSubmit={false} />
<Box>
<Text fw={"bold"}>Upload Gambar Penghargaan</Text>
<Box bg={colors['BG-trans']} p={"md"}>
<Center>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Center>
</Box>
</Box>
<Group>
<Button
mt={10}
bg={colors['blue-button']}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
<ListGambarPenghargaan />
</SimpleGrid>
</Stack>
</Box>
);
}
export default GambarPerhargaan;

View File

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

View File

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

View File

@@ -0,0 +1,53 @@
'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 { KeamananEditor } from '@/app/admin/(dashboard)/keamanan/_com/keamananEditor';
function EditPengumuman() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25}/>
</Button>
</Box>
<Paper w={{base: '100%', md: '50%'}} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Pengumuman</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Judul</Text>}
placeholder='Masukkan judul'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Deskripsi Singkat</Text>}
placeholder='Masukkan deskripsi singkat'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Tanggal</Text>}
placeholder='Masukkan tanggal'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Waktu</Text>}
placeholder='Masukkan waktu'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi</Text>
<KeamananEditor
showSubmit={false}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditPengumuman;

View File

@@ -0,0 +1,110 @@
'use client'
import { Box, Button, Paper } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import colors from '@/con/colors';
function DetailPengumuman() {
// const pengumumanState = useProxy(stateDesaPengumuman)
// const [modalHapus, setModalHapus] = useState(false)
// const [selectedId, setSelectedId] = useState<string | null>(null)
// const params = useParams()
const router = useRouter()
// useShallowEffect(() => {
// pengumumanState.pengumuman.findUnique.load(params?.id as string)
// }, [])
// const handleHapus = () => {
// if (selectedId) {
// pengumumanState.pengumuman.delete.byId(selectedId)
// setModalHapus(false)
// setSelectedId(null)
// router.push("/admin/desa/pengumuman")
// }
// }
// if (!pengumumanState.pengumuman.findUnique.data) {
// return (
// <Stack py={10}>
// <Skeleton h={400} />
// </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 Pengumuman</Text>
{pengumumanState.pengumuman.findUnique.data ? (
<Paper key={pengumumanState.pengumuman.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Kategori</Text>
<Text fz={"lg"}>{pengumumanState.pengumuman.findUnique.data?.CategoryPengumuman?.name}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Judul</Text>
<Text fz={"lg"}>{pengumumanState.pengumuman.findUnique.data?.judul}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
<Text fz={"lg"}>{pengumumanState.pengumuman.findUnique.data?.deskripsi}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Konten</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: pengumumanState.pengumuman.findUnique.data?.content }} />
</Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (pengumumanState.pengumuman.findUnique.data) {
setSelectedId(pengumumanState.pengumuman.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={pengumumanState.pengumuman.delete.loading || !pengumumanState.pengumuman.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (pengumumanState.pengumuman.findUnique.data) {
router.push(`/admin/desa/pengumuman/${pengumumanState.pengumuman.findUnique.data.id}/edit`);
}
}}
disabled={!pengumumanState.pengumuman.findUnique.data}
color={"green"}
>
<IconEdit size={20} />
</Button>
</Flex>
</Stack>
</Paper>
) : null}
</Stack> */}
</Paper>
{/* Modal Konfirmasi Hapus
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus berita ini?'
/> */}
</Box>
);
}
export default DetailPengumuman;

View File

@@ -0,0 +1,110 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { Prisma } from '@prisma/client';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor';
import stateDesaPengumuman from '../../../_state/desa/pengumuman';
function CreatePengumuman() {
const pengumumanState = useProxy(stateDesaPengumuman)
const router = useRouter();
const handleSubmit = async () => {
await pengumumanState.pengumuman.create.create()
resetForm()
router.push("/admin/desa/pengumuman")
}
const resetForm = () => {
pengumumanState.pengumuman.create.form = {
judul: "",
deskripsi: "",
content: "",
categoryPengumumanId: "",
};
};
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 Pengumuman</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Judul</Text>}
placeholder='Masukkan judul'
onChange={(val) => {
pengumumanState.pengumuman.create.form.judul = val.target.value
}}
/>
<SelectCategory
onChange={(val) => {
pengumumanState.pengumuman.create.form.categoryPengumumanId = val.id;
}}
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Deskripsi Singkat</Text>}
placeholder='Masukkan deskripsi singkat'
onChange={(val) => {
pengumumanState.pengumuman.create.form.deskripsi = val.target.value
}}
/>
<Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
<CreateEditor
value={pengumumanState.pengumuman.create.form.content}
onChange={(htmlContent) => {
pengumumanState.pengumuman.create.form.content = htmlContent;
}}
/>
</Box>
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
function SelectCategory({
onChange,
}: {
onChange: (value: Prisma.CategoryPengumumanGetPayload<{ select: { name: true; id: true; } }>) => void;
}) {
const categoryState = useProxy(stateDesaPengumuman.category);
useShallowEffect(() => {
categoryState.findMany.load();
}, []);
return (
<Select
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>}
placeholder='Pilih kategori'
data={categoryState.findMany.data?.map((item) => ({
label: item.name,
value: item.id,
}))}
onChange={(val) => {
const selected = categoryState.findMany.data?.find((item) => item.id === val);
if (selected) {
onChange(selected);
}
}}
searchable
nothingFoundMessage="Tidak ditemukan"
/>
);
}
export default CreatePengumuman;

View File

@@ -1,102 +1,111 @@
'use client'
import { Box, Group, Paper, Select, SimpleGrid, Skeleton, Stack, Text, TextInput, Title } from '@mantine/core';
import React from 'react';
import { useProxy } from 'valtio/utils';
import stateDesaPengumuman from '../../_state/desa/pengumuman';
import { useShallowEffect } from '@mantine/hooks';
import { Prisma } from '@prisma/client';
import { BeritaEditor } from '../berita/_com/BeritaEditor';
import colors from '@/con/colors';
import { Box, Button, Grid, GridCol, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import stateDesaPengumuman from '../../_state/desa/pengumuman';
import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus';
import { useState } from 'react';
function Page() {
return (
<Box>
<Title order={3}>Pengumuman</Title>
<SimpleGrid cols={{
base: 1, md: 2
}}>
<PengumumanCreate />
<PengumumanList />
</SimpleGrid>
<HeaderSearch
title='Pengumuman'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
/>
<PengumumanList />
</Box>
);
}
function PengumumanCreate() {
const pengumumanState = useProxy(stateDesaPengumuman)
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<SelectCategory onChange={(val) => {
pengumumanState.pengumuman.create.form.categoryPengumumanId = val.id
}} />
<TextInput onChange={(val) => {
pengumumanState.pengumuman.create.form.judul = val.target.value
}} label={<Text fz={"sm"} fw={"bold"}>Judul</Text>} placeholder='masukkan judul' />
<TextInput onChange={(val) => {
pengumumanState.pengumuman.create.form.deskripsi = val.target.value
}} label={<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>} placeholder='masukkan deskripsi' />
<BeritaEditor onSubmit={(val) => {
pengumumanState.pengumuman.create.form.content = val
pengumumanState.pengumuman.create.create()
}} />
</Stack>
</Paper>
</Box>
)
}
function PengumumanList() {
const pengumumanState = useProxy(stateDesaPengumuman)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
useShallowEffect(() => {
pengumumanState.pengumuman.findMany.load()
}, [])
if (!pengumumanState.pengumuman.findMany.data) return <Stack py={10}>
{Array.from({ length: 10 }).map((v, k) => <Skeleton key={k} h={40} />)}
</Stack>
const router = useRouter()
const handleHapus = () => {
if (selectedId) {
pengumumanState.pengumuman.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
}
}
if (!pengumumanState.pengumuman.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Text fz={"xl"} fw={"bold"}>List Pengumuman</Text>
{pengumumanState.pengumuman.findMany.data?.map((item) => (
<Text key={item.id}>{item.judul}</Text>
))}
<Stack>
<Grid>
<GridCol span={{ base: 12, md: 11 }}>
<Text fz={"xl"} fw={"bold"}>List Pengumuman</Text>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button onClick={() => router.push("/admin/desa/pengumuman/create")} bg={colors['blue-button']}>
<IconCircleDashedPlus size={25} />
</Button>
</GridCol>
</Grid>
<Box style={{ overflowX: "auto" }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
<TableThead>
<TableTr>
<TableTh w={250}>Judul</TableTh>
<TableTh w={250}>Kategori</TableTh>
<TableTh w={200}>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody >
{pengumumanState.pengumuman.findMany.data?.map((item) => (
<TableTr key={item.id}>
<TableTd >
<Box w={100}>
<Text truncate="end" fz={"sm"}>{item.judul}</Text>
</Box>
</TableTd>
<TableTd >{item.CategoryPengumuman?.name}</TableTd>
<TableTd>
<Button bg={"green"} onClick={() => router.push(`/admin/desa/pengumuman/detail`)}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus berita ini?'
/>
</Box>
)
}
function SelectCategory({ onChange }: {
onChange: (value: Prisma.CategoryPengumumanGetPayload<{
select: {
name: true,
id: true
}
}>) => void
}) {
const pengumumanState = useProxy(stateDesaPengumuman)
useShallowEffect(() => {
pengumumanState.category.findMany.load()
}, [])
if (!pengumumanState.category.findMany.data) return <Skeleton h={40} />
return <Group>
{/* {JSON.stringify(pengumumanState.category.findMany.data)} */}
<Select placeholder='pilih kategori' label={<Text fz={"sm"} fw={"bold"}>Pilih Kategori</Text>} data={pengumumanState.category.findMany.data.map((item) => ({
value: item.id,
label: item.name
}))} onChange={(v) => {
const data = pengumumanState.category.findMany.data?.find((item) => item.id === v)
if (!data) return
onChange(data)
}}
/>
</Group>
}
export default Page;

View File

@@ -0,0 +1,42 @@
'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';
function CreateDemografiPekerjaan() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25}/>
</Button>
</Box>
<Paper w={{base: '100%', md: '50%'}} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Demografi Pekerjaan</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Pekerjaan</Text>}
placeholder='Masukkan pekerjaan'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Jumlah Pekerja Laki - Laki</Text>}
placeholder='Masukkan jumlah pekerja laki - laki'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Jumlah Pekerja Perempuan</Text>}
placeholder='Masukkan jumlah pekerja perempuan'
/>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreateDemografiPekerjaan;

View File

@@ -0,0 +1,62 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Stack, Text } from '@mantine/core';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailDemografiPekerjaan() {
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 Demografi Pekerjaan</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"}>Pekerjaan</Text>
<Text>Karyawan</Text>
</Box>
<Box>
<Text fw={"bold"}>Jumlah Pekerja Laki - Laki</Text>
<Text>200</Text>
</Box>
<Box>
<Text fw={"bold"}>Jumlah Pekerja Perempuan</Text>
<Text>100</Text>
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red">
<IconX size={20} />
</Button>
<Button onClick={() => router.push('/admin/ekonomi/demografi-pekerjaan/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 DetailDemografiPekerjaan;

View File

@@ -0,0 +1,42 @@
'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';
function EditDemografiPekerjaan() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25}/>
</Button>
</Box>
<Paper w={{base: '100%', md: '50%'}} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Demografi Pekerjaan</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Pekerjaan</Text>}
placeholder='Masukkan pekerjaan'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Jumlah Pekerja Laki - Laki</Text>}
placeholder='Masukkan jumlah pekerja laki - laki'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Jumlah Pekerja Perempuan</Text>}
placeholder='Masukkan jumlah pekerja perempuan'
/>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditDemografiPekerjaan;

View File

@@ -1,47 +1,58 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import React from 'react';
import { Box, Button, Paper, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import { useRouter } from 'next/navigation';
function Page() {
function DemografiPekerjaan() {
return (
<Box>
<Stack gap={"xs"}>
<Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={"md"}>
<Stack gap={"xs"}>
<Title order={3}>Demografi Pekerjaan</Title>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Jumlah Pekerja Laki - Laki</Text>}
placeholder="Masukkan jumlah pekerja laki - laki"
/>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Jumlah Pekerja Perempuan</Text>}
placeholder="Masukkan jumlah pekerja perempuan"
/>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Nama Pekerjaan</Text>}
placeholder="Masukkan nama pekerjaan"
/>
<Group>
<Button
bg={colors['blue-button']}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
<Box>
<Paper bg={colors['white-1']} p={"md"}>
<Stack gap={"xs"}>
<Title order={3}>Grafik Demografi Pekerjaan</Title>
</Stack>
</Paper>
</Box>
</Stack>
<HeaderSearch
title='Demografi Pekerjaan'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
/>
<ListDemografiPekerjaan/>
</Box>
);
}
export default Page;
function ListDemografiPekerjaan() {
const router = useRouter();
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Demografi Pekerjaan'
href='/admin/ekonomi/demografi-pekerjaan/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Pekerjaan</TableTh>
<TableTh>Jumlah Pekerja Laki - Laki</TableTh>
<TableTh>Jumlah Pekerja Perempuan</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>Karyawan</TableTd>
<TableTd>200</TableTd>
<TableTd>100</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/ekonomi/demografi-pekerjaan/detail')}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
</TableTbody>
</Table>
</Paper>
</Box>
);
}
export default DemografiPekerjaan;

View File

@@ -0,0 +1,38 @@
'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';
function CreateJumlahPendudukMiskin() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25}/>
</Button>
</Box>
<Paper w={{base: '100%', md: '50%'}} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Jumlah Penduduk Miskin</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Tahun</Text>}
placeholder='Masukkan tahun'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Jumlah Penduduk Miskin</Text>}
placeholder='Masukkan jumlah penduduk miskin'
/>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreateJumlahPendudukMiskin;

View File

@@ -0,0 +1,58 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Stack, Text } from '@mantine/core';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailJumlahPendudukMiskin() {
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 Jumlah Penduduk Miskin</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"}>Tahun</Text>
<Text>2024</Text>
</Box>
<Box>
<Text fw={"bold"}>Jumlah Penduduk Miskin</Text>
<Text>100</Text>
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red">
<IconX size={20} />
</Button>
<Button onClick={() => router.push('/admin/ekonomi/jumlah-penduduk-miskin-2024-2025/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 DetailJumlahPendudukMiskin;

View File

@@ -0,0 +1,38 @@
'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';
function EditJumlahPendudukMiskin() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25}/>
</Button>
</Box>
<Paper w={{base: '100%', md: '50%'}} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Jumlah Penduduk Miskin</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Tahun</Text>}
placeholder='Masukkan tahun'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Jumlah Penduduk Miskin</Text>}
placeholder='Masukkan jumlah penduduk miskin'
/>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditJumlahPendudukMiskin;

View File

@@ -1,43 +1,56 @@
import { Box, Button, Group, Paper, Stack, TextInput, Title } from '@mantine/core';
'use client'
import colors from '@/con/colors';
import React from 'react';
import { Box, Button, Paper, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import { useRouter } from 'next/navigation';
function Page() {
function JumlahPendudukMiskin() {
return (
<Box>
<Stack gap={"xs"}>
<Box>
<Paper p={"md"} bg={colors['white-1']} w={{ base: '100%', md: '50%' }}>
<Stack gap={"xs"}>
<Title order={3}>Jumlah Penduduk Miskin 2024-2025</Title>
<TextInput
label="Tahun"
placeholder="masukkan tahun"
/>
<TextInput
label="Jumlah Penduduk Miskin"
placeholder="masukkan jumlah penduduk miskin"
/>
<Group>
<Button
bg={colors['blue-button']}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
<Box>
<Paper p={"md"} bg={colors['white-1']} >
<Stack gap={"xs"}>
<Title order={3}>Grafik Jumlah Penduduk Miskin 2024-2025</Title>
</Stack>
</Paper>
</Box>
</Stack>
<HeaderSearch
title='Jumlah Penduduk Miskin'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
/>
<ListJumlahPendudukMiskin/>
</Box>
);
}
export default Page;
function ListJumlahPendudukMiskin() {
const router = useRouter();
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Jumlah Penduduk Miskin'
href='/admin/ekonomi/jumlah-penduduk-miskin-2024-2025/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Tahun</TableTh>
<TableTh>Jumlah Penduduk Miskin</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>2024</TableTd>
<TableTd>100</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/ekonomi/jumlah-penduduk-miskin-2024-2025/detail')}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
</TableTbody>
</Table>
</Paper>
</Box>
);
}
export default JumlahPendudukMiskin;

View File

@@ -1,27 +1,27 @@
import colors from "@/con/colors";
import { Box, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from "@mantine/core";
import PengangguranBerdasarkanUsia from "./ui/pengangguranBerdasarkanUsia/page";
import PengangguranBerdasarkanPendidikan from "./ui/pengangguranBerdasarkanPendidikan/page";
import PengangguranBerdasarkanUsia from "./pengangguran_berdasarkan_usia/page";
import PengangguranBerdasarkanPendidikan from "./pengangguran_berdasarkan_pendidikan/page";
export default function Page() {
return (
<Box>
<Stack gap={"xs"}>
<Title order={3}>Jumlah Penduduk Usia Kerja yang Menganggur</Title>
<Tabs color={colors['blue-button']} variant='pills' defaultValue={"Pengangguran Bredasarkan Usia"}>
<Tabs color={colors['blue-button']} variant='pills' defaultValue={"Pengangguran Berdasarkan Usia"}>
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
<TabsTab value={"Pengangguran Bredasarkan Usia"}>
Pengangguran Bredasarkan Usia
<TabsTab value={"Pengangguran Berdasarkan Usia"}>
Pengangguran Berdasarkan Usia
</TabsTab>
<TabsTab value={"Pengangguran Bredasarkan Pendidikan"}>
Pengangguran Bredasarkan Pendidikan
<TabsTab value={"Pengangguran Berdasarkan Pendidikan"}>
Pengangguran Berdasarkan Pendidikan
</TabsTab>
</TabsList>
<TabsPanel value={"Pengangguran Bredasarkan Usia"}>
<TabsPanel value={"Pengangguran Berdasarkan Usia"}>
<PengangguranBerdasarkanUsia />
</TabsPanel>
<TabsPanel value={"Pengangguran Bredasarkan Pendidikan"}>
<TabsPanel value={"Pengangguran Berdasarkan Pendidikan"}>
<PengangguranBerdasarkanPendidikan/>
</TabsPanel>
</Tabs>

View File

@@ -0,0 +1,54 @@
'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';
function CreatePengangguranBerdasarkanPendidikan() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25}/>
</Button>
</Box>
<Paper w={{base: '100%', md: '50%'}} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Pengangguran Berdasarkan Pendidikan</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Pendidikan SD</Text>}
placeholder='Masukkan pendidikan sd'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Pendidikan SMP</Text>}
placeholder='Masukkan pendidikan smp'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Pendidikan SMA</Text>}
placeholder='Masukkan pendidikan sma'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Pendidikan S1</Text>}
placeholder='Masukkan pendidikan s1'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Pendidikan S2</Text>}
placeholder='Masukkan pendidikan s2'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Pendidikan S3</Text>}
placeholder='Masukkan pendidikan s3'
/>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreatePengangguranBerdasarkanPendidikan;

View File

@@ -0,0 +1,74 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Stack, Text } from '@mantine/core';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailPengangguranBerdasarkanPendidikan() {
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 Pengangguran Berdasarkan Pendidikan</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"}>Pendidikan SD</Text>
<Text>50</Text>
</Box>
<Box>
<Text fw={"bold"}>Pendidikan SMP</Text>
<Text>60</Text>
</Box>
<Box>
<Text fw={"bold"}>Pendidikan SMA</Text>
<Text>80</Text>
</Box>
<Box>
<Text fw={"bold"}>Pendidikan S1</Text>
<Text>40</Text>
</Box>
<Box>
<Text fw={"bold"}>Pendidikan S2</Text>
<Text>20</Text>
</Box>
<Box>
<Text fw={"bold"}>Pendidikan S3</Text>
<Text>10</Text>
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red">
<IconX size={20} />
</Button>
<Button onClick={() => router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/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 DetailPengangguranBerdasarkanPendidikan;

View File

@@ -0,0 +1,54 @@
'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';
function EditPengangguranBerdasarkanPendidikan() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25}/>
</Button>
</Box>
<Paper w={{base: '100%', md: '50%'}} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Pengangguran Berdasarkan Pendidikan</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Pendidikan SD</Text>}
placeholder='Masukkan pendidikan sd'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Pendidikan SMP</Text>}
placeholder='Masukkan pendidikan smp'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Pendidikan SMA</Text>}
placeholder='Masukkan pendidikan sma'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Pendidikan S1</Text>}
placeholder='Masukkan pendidikan s1'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Pendidikan S2</Text>}
placeholder='Masukkan pendidikan s2'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Pendidikan S3</Text>}
placeholder='Masukkan pendidikan s3'
/>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditPengangguranBerdasarkanPendidikan;

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