diff --git a/bun.lockb b/bun.lockb
index f6b34ff1..e30c0976 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/package.json b/package.json
index c32725d5..e4b3bc0b 100644
--- a/package.json
+++ b/package.json
@@ -54,6 +54,7 @@
"chart.js": "^4.4.8",
"classnames": "^2.5.1",
"colors": "^1.4.0",
+ "date-fns": "^4.1.0",
"dayjs": "^1.11.13",
"dotenv": "^17.2.3",
"elysia": "^1.3.5",
diff --git a/prisma/data/user/roles.json b/prisma/data/user/roles.json
index b79f3928..4d2a1046 100644
--- a/prisma/data/user/roles.json
+++ b/prisma/data/user/roles.json
@@ -1,23 +1,32 @@
[
- {
- "id": "role-1",
- "name": "ADMIN DESA",
- "description": "Administrator Desa",
- "permissions": ["manage_users", "manage_content", "view_reports"],
- "isActive": true
- },
- {
- "id": "role-2",
- "name": "ADMIN KESEHATAN",
- "description": "Administrator Bidang Kesehatan",
- "permissions": ["manage_health_data", "view_reports"],
- "isActive": true
- },
- {
- "id": "role-3",
- "name": "ADMIN SEKOLAH",
- "description": "Administrator Sekolah",
- "permissions": ["manage_school_data", "view_reports"],
- "isActive": true
- }
- ]
\ No newline at end of file
+ {
+ "id": "0",
+ "name": "DEVELOPER",
+ "description": "Developer",
+ "isActive": true
+ },
+ {
+ "id": "1",
+ "name": "SUPER ADMIN",
+ "description": "Administrator",
+ "isActive": true
+ },
+ {
+ "id": "2",
+ "name": "ADMIN DESA",
+ "description": "Administrator Desa",
+ "isActive": true
+ },
+ {
+ "id": "3",
+ "name": "ADMIN KESEHATAN",
+ "description": "Administrator Bidang Kesehatan",
+ "isActive": true
+ },
+ {
+ "id": "4",
+ "name": "ADMIN PENDIDIKAN",
+ "description": "Administrator Bidang Pendidikan",
+ "isActive": true
+ }
+]
diff --git a/prisma/data/user/users.json b/prisma/data/user/users.json
index eea2a98a..733aeba4 100644
--- a/prisma/data/user/users.json
+++ b/prisma/data/user/users.json
@@ -1,23 +1,10 @@
[
{
- "id": "user-1",
- "nama": "Admin Desa",
- "nomor": "089647037426",
- "roleId": "role-1",
- "isActive": true
- },
- {
- "id": "user-2",
- "nama": "Admin Kesehatan",
- "nomor": "082339004198",
- "roleId": "role-2",
- "isActive": true
- },
- {
- "id": "user-3",
- "nama": "Admin Sekolah",
- "nomor": "085237157222",
- "roleId": "role-3",
- "isActive": true
+ "id": "cmie1o0zh0002vn132vtzg7hh",
+ "username": "SuperAdmin-Nico",
+ "nomor": "6289647037426",
+ "roleId": 0,
+ "isActive": true,
+ "sessionInvalid": false
}
]
diff --git a/prisma/migrations/20251119062255_add_unique_username/migration.sql b/prisma/migrations/20251119062255_add_unique_username/migration.sql
new file mode 100644
index 00000000..d8d9e087
--- /dev/null
+++ b/prisma/migrations/20251119062255_add_unique_username/migration.sql
@@ -0,0 +1,1127 @@
+/*
+ Warnings:
+
+ - You are about to drop the column `kelahiranKasar` on the `DataKematian_Kelahiran` table. All the data in the column will be lost.
+ - You are about to drop the column `kematianBayi` on the `DataKematian_Kelahiran` table. All the data in the column will be lost.
+ - You are about to drop the column `kematianKasar` on the `DataKematian_Kelahiran` table. All the data in the column will be lost.
+ - You are about to drop the column `tahun` on the `DataKematian_Kelahiran` table. All the data in the column will be lost.
+ - You are about to drop the column `jumlah` on the `GrafikKepuasan` table. All the data in the column will be lost.
+ - You are about to drop the column `label` on the `GrafikKepuasan` table. All the data in the column will be lost.
+ - You are about to drop the column `imageId` on the `KolaborasiInovasi` table. All the data in the column will be lost.
+ - You are about to drop the column `imageId` on the `KontakDaruratKeamanan` table. All the data in the column will be lost.
+ - You are about to drop the column `imageId` on the `KontakItem` table. All the data in the column will be lost.
+ - You are about to drop the column `kategoriId` on the `KontakItem` table. All the data in the column will be lost.
+ - You are about to drop the column `programKeamananId` on the `PencegahanKriminalitas` table. All the data in the column will be lost.
+ - You are about to drop the column `tipsKeamananId` on the `PencegahanKriminalitas` table. All the data in the column will be lost.
+ - You are about to drop the column `videoKeamananId` on the `PencegahanKriminalitas` table. All the data in the column will be lost.
+ - You are about to drop the column `kategori` on the `PotensiDesa` table. All the data in the column will be lost.
+ - You are about to drop the column `ikonUrl` on the `ProgramKemiskinan` table. All the data in the column will be lost.
+ - You are about to drop the `ProgramKeamanan` table. If the table is not empty, all the data it contains will be lost.
+ - You are about to drop the `TipsKeamanan` table. If the table is not empty, all the data it contains will be lost.
+ - You are about to drop the `VideoKeamanan` table. If the table is not empty, all the data it contains will be lost.
+ - You are about to drop the `hubungan_organisasi` table. If the table is not empty, all the data it contains will be lost.
+ - You are about to drop the `pegawai` table. If the table is not empty, all the data it contains will be lost.
+ - You are about to drop the `posisi_organisasi` table. If the table is not empty, all the data it contains will be lost.
+ - You are about to drop the `struktur_organisasi` table. If the table is not empty, all the data it contains will be lost.
+ - Added the required column `kelahiranId` to the `DataKematian_Kelahiran` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `kematianId` to the `DataKematian_Kelahiran` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `category` to the `FileStorage` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `alamat` to the `GrafikKepuasan` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `jenisKelamin` to the `GrafikKepuasan` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `nama` to the `GrafikKepuasan` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `penyakit` to the `GrafikKepuasan` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `tanggal` to the `GrafikKepuasan` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `whatsapp` to the `KontakDarurat` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `icon` to the `KontakDaruratKeamanan` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `kategoriId` to the `KontakDaruratKeamanan` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `icon` to the `KontakItem` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `notelp` to the `LowonganPekerjaan` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `name` to the `MediaSosial` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `kontak` to the `PasarDesa` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `deskripsi` to the `PencegahanKriminalitas` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `deskripsiSingkat` to the `PencegahanKriminalitas` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `judul` to the `PencegahanKriminalitas` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `linkVideo` to the `PencegahanKriminalitas` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `jadwalPelayanan` to the `Posyandu` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `icon` to the `ProgramKemiskinan` table without a default value. This is not possible if the table is not empty.
+
+*/
+-- CreateEnum
+CREATE TYPE "JenisKelamin" AS ENUM ('LAKI_LAKI', 'PEREMPUAN');
+
+-- CreateEnum
+CREATE TYPE "Agama" AS ENUM ('ISLAM', 'KRISTEN_PROTESTAN', 'KRISTEN_KATOLIK', 'HINDU', 'BUDDHA', 'KONGHUCU', 'LAINNYA');
+
+-- CreateEnum
+CREATE TYPE "StatusPernikahan" AS ENUM ('BELUM_MENIKAH', 'MENIKAH', 'JANDA_DUDA');
+
+-- CreateEnum
+CREATE TYPE "UkuranBaju" AS ENUM ('S', 'M', 'L', 'XL', 'XXL', 'LAINNYA');
+
+-- CreateEnum
+CREATE TYPE "StatusPeminjaman" AS ENUM ('Dipinjam', 'Dikembalikan', 'Terlambat', 'Dibatalkan');
+
+-- DropForeignKey
+ALTER TABLE "JadwalKegiatan" DROP CONSTRAINT "JadwalKegiatan_pendaftaranJadwalKegiatanId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "KolaborasiInovasi" DROP CONSTRAINT "KolaborasiInovasi_imageId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "KontakDaruratKeamanan" DROP CONSTRAINT "KontakDaruratKeamanan_imageId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "KontakItem" DROP CONSTRAINT "KontakItem_imageId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "KontakItem" DROP CONSTRAINT "KontakItem_kategoriId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "MediaSosial" DROP CONSTRAINT "MediaSosial_imageId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "PelayananSuratKeterangan" DROP CONSTRAINT "PelayananSuratKeterangan_imageId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "PencegahanKriminalitas" DROP CONSTRAINT "PencegahanKriminalitas_programKeamananId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "PencegahanKriminalitas" DROP CONSTRAINT "PencegahanKriminalitas_tipsKeamananId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "PencegahanKriminalitas" DROP CONSTRAINT "PencegahanKriminalitas_videoKeamananId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "Penghargaan" DROP CONSTRAINT "Penghargaan_imageId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "PotensiDesa" DROP CONSTRAINT "PotensiDesa_imageId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "hubungan_organisasi" DROP CONSTRAINT "hubungan_organisasi_atasanId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "hubungan_organisasi" DROP CONSTRAINT "hubungan_organisasi_bawahanId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "pegawai" DROP CONSTRAINT "pegawai_imageId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "pegawai" DROP CONSTRAINT "pegawai_posisiId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "struktur_organisasi" DROP CONSTRAINT "struktur_organisasi_hubunganOrganisasiId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "struktur_organisasi" DROP CONSTRAINT "struktur_organisasi_pegawaiId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "struktur_organisasi" DROP CONSTRAINT "struktur_organisasi_posisiOrganisasiId_fkey";
+
+-- AlterTable
+ALTER TABLE "ArtikelKesehatan" ADD COLUMN "imageId" TEXT;
+
+-- AlterTable
+ALTER TABLE "DataKematian_Kelahiran" DROP COLUMN "kelahiranKasar",
+DROP COLUMN "kematianBayi",
+DROP COLUMN "kematianKasar",
+DROP COLUMN "tahun",
+ADD COLUMN "kelahiranId" TEXT NOT NULL,
+ADD COLUMN "kematianId" TEXT NOT NULL;
+
+-- AlterTable
+ALTER TABLE "FileStorage" ADD COLUMN "category" TEXT NOT NULL;
+
+-- AlterTable
+ALTER TABLE "GrafikKepuasan" DROP COLUMN "jumlah",
+DROP COLUMN "label",
+ADD COLUMN "alamat" TEXT NOT NULL,
+ADD COLUMN "jenisKelamin" TEXT NOT NULL,
+ADD COLUMN "nama" TEXT NOT NULL,
+ADD COLUMN "penyakit" TEXT NOT NULL,
+ADD COLUMN "tanggal" TIMESTAMP(3) NOT NULL;
+
+-- AlterTable
+ALTER TABLE "JadwalKegiatan" ALTER COLUMN "pendaftaranJadwalKegiatanId" DROP NOT NULL;
+
+-- AlterTable
+ALTER TABLE "KolaborasiInovasi" DROP COLUMN "imageId";
+
+-- AlterTable
+ALTER TABLE "KontakDarurat" ADD COLUMN "whatsapp" TEXT NOT NULL;
+
+-- AlterTable
+ALTER TABLE "KontakDaruratKeamanan" DROP COLUMN "imageId",
+ADD COLUMN "deletedAt" TIMESTAMP(3),
+ADD COLUMN "icon" TEXT NOT NULL,
+ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true,
+ADD COLUMN "kategoriId" TEXT NOT NULL;
+
+-- AlterTable
+ALTER TABLE "KontakItem" DROP COLUMN "imageId",
+DROP COLUMN "kategoriId",
+ADD COLUMN "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ADD COLUMN "icon" TEXT NOT NULL,
+ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true;
+
+-- AlterTable
+ALTER TABLE "LaporanPublik" ADD COLUMN "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true,
+ALTER COLUMN "status" SET DEFAULT 'Proses';
+
+-- AlterTable
+ALTER TABLE "LowonganPekerjaan" ADD COLUMN "notelp" TEXT NOT NULL;
+
+-- AlterTable
+ALTER TABLE "MediaSosial" ADD COLUMN "name" TEXT NOT NULL,
+ALTER COLUMN "imageId" DROP NOT NULL;
+
+-- AlterTable
+ALTER TABLE "PasarDesa" ADD COLUMN "kontak" TEXT NOT NULL;
+
+-- AlterTable
+ALTER TABLE "PelayananSuratKeterangan" ADD COLUMN "image2Id" TEXT,
+ALTER COLUMN "imageId" DROP NOT NULL;
+
+-- AlterTable
+ALTER TABLE "PencegahanKriminalitas" DROP COLUMN "programKeamananId",
+DROP COLUMN "tipsKeamananId",
+DROP COLUMN "videoKeamananId",
+ADD COLUMN "deskripsi" TEXT NOT NULL,
+ADD COLUMN "deskripsiSingkat" TEXT NOT NULL,
+ADD COLUMN "judul" TEXT NOT NULL,
+ADD COLUMN "linkVideo" TEXT NOT NULL;
+
+-- AlterTable
+ALTER TABLE "Penghargaan" ALTER COLUMN "imageId" DROP NOT NULL;
+
+-- AlterTable
+ALTER TABLE "Posyandu" ADD COLUMN "jadwalPelayanan" TEXT NOT NULL;
+
+-- AlterTable
+ALTER TABLE "PotensiDesa" DROP COLUMN "kategori",
+ADD COLUMN "kategoriId" TEXT,
+ALTER COLUMN "imageId" DROP NOT NULL;
+
+-- AlterTable
+ALTER TABLE "ProgramKemiskinan" DROP COLUMN "ikonUrl",
+ADD COLUMN "icon" TEXT NOT NULL;
+
+-- AlterTable
+ALTER TABLE "StrukturPPID" ADD COLUMN "pegawaiPPIDId" TEXT,
+ADD COLUMN "posisiOrganisasiPPIDId" TEXT;
+
+-- DropTable
+DROP TABLE "ProgramKeamanan";
+
+-- DropTable
+DROP TABLE "TipsKeamanan";
+
+-- DropTable
+DROP TABLE "VideoKeamanan";
+
+-- DropTable
+DROP TABLE "hubungan_organisasi";
+
+-- DropTable
+DROP TABLE "pegawai";
+
+-- DropTable
+DROP TABLE "posisi_organisasi";
+
+-- DropTable
+DROP TABLE "struktur_organisasi";
+
+-- CreateTable
+CREATE TABLE "DesaAntiKorupsi" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "kategoriId" TEXT NOT NULL,
+ "fileId" 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 "DesaAntiKorupsi_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "KategoriDesaAntiKorupsi" (
+ "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 "KategoriDesaAntiKorupsi_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "SdgsDesa" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "jumlah" TEXT NOT NULL,
+ "imageId" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "SdgsDesa_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "APBDes" (
+ "id" TEXT NOT NULL,
+ "tahun" INTEGER,
+ "name" TEXT,
+ "deskripsi" TEXT,
+ "jumlah" TEXT,
+ "imageId" TEXT,
+ "fileId" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3),
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "APBDes_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "APBDesItem" (
+ "id" TEXT NOT NULL,
+ "kode" TEXT NOT NULL,
+ "uraian" TEXT NOT NULL,
+ "anggaran" DOUBLE PRECISION NOT NULL,
+ "realisasi" DOUBLE PRECISION NOT NULL,
+ "selisih" DOUBLE PRECISION NOT NULL,
+ "persentase" DOUBLE PRECISION NOT NULL,
+ "tipe" TEXT,
+ "level" INTEGER NOT NULL,
+ "parentId" TEXT,
+ "apbdesId" 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,
+
+ CONSTRAINT "APBDesItem_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "PrestasiDesa" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "kategoriId" TEXT NOT NULL,
+ "imageId" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "PrestasiDesa_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "KategoriPrestasiDesa" (
+ "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 "KategoriPrestasiDesa_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Responden" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "tanggal" DATE NOT NULL,
+ "jenisKelaminId" TEXT NOT NULL,
+ "ratingId" TEXT NOT NULL,
+ "kelompokUmurId" 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 "Responden_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "JenisKelaminResponden" (
+ "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 "JenisKelaminResponden_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "PilihanRatingResponden" (
+ "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 "PilihanRatingResponden_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "UmurResponden" (
+ "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 "UmurResponden_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "PosisiOrganisasiPPID" (
+ "id" TEXT NOT NULL,
+ "nama" VARCHAR(100) NOT NULL,
+ "deskripsi" TEXT,
+ "hierarki" INTEGER NOT NULL,
+ "parentId" TEXT,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "PosisiOrganisasiPPID_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "PegawaiPPID" (
+ "id" TEXT NOT NULL,
+ "namaLengkap" VARCHAR(255) NOT NULL,
+ "gelarAkademik" VARCHAR(100),
+ "imageId" TEXT,
+ "tanggalMasuk" DATE,
+ "email" VARCHAR(255),
+ "telepon" VARCHAR(20),
+ "alamat" TEXT,
+ "posisiId" VARCHAR(50) NOT NULL,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "PegawaiPPID_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "StrukturOrganisasiPPID" (
+ "id" TEXT NOT NULL,
+ "posisiOrganisasiId" VARCHAR(50) NOT NULL,
+ "pegawaiId" TEXT NOT NULL,
+ "hubunganOrganisasiId" 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,
+
+ CONSTRAINT "StrukturOrganisasiPPID_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "PerbekelDariMasaKeMasa" (
+ "id" TEXT NOT NULL,
+ "nama" TEXT NOT NULL,
+ "periode" TEXT NOT NULL,
+ "imageId" TEXT,
+ "daerah" 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 "PerbekelDariMasaKeMasa_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "KategoriPotensi" (
+ "id" TEXT NOT NULL,
+ "nama" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "KategoriPotensi_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "AjukanPermohonan" (
+ "id" TEXT NOT NULL,
+ "nama" TEXT NOT NULL,
+ "nik" TEXT NOT NULL,
+ "alamat" TEXT NOT NULL,
+ "nomorKk" TEXT NOT NULL,
+ "kategoriId" 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 "AjukanPermohonan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Kelahiran" (
+ "id" TEXT NOT NULL,
+ "nama" TEXT NOT NULL,
+ "tanggal" TIMESTAMP(3) NOT NULL,
+ "jenisKelamin" TEXT NOT NULL,
+ "alamat" 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 "Kelahiran_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Kematian" (
+ "id" TEXT NOT NULL,
+ "nama" TEXT NOT NULL,
+ "tanggal" TIMESTAMP(3) NOT NULL,
+ "jenisKelamin" TEXT NOT NULL,
+ "alamat" TEXT NOT NULL,
+ "penyebab" 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 "Kematian_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "KontakDaruratToItem" (
+ "id" TEXT NOT NULL,
+ "kontakDaruratId" TEXT NOT NULL,
+ "kontakItemId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+
+ CONSTRAINT "KontakDaruratToItem_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "StrukturBumDes" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "imageId" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+ "posisiOrganisasiBumDesId" TEXT,
+ "pegawaiBumDesId" TEXT,
+
+ CONSTRAINT "StrukturBumDes_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "PosisiOrganisasiBumDes" (
+ "id" TEXT NOT NULL,
+ "nama" VARCHAR(100) NOT NULL,
+ "deskripsi" TEXT,
+ "hierarki" INTEGER NOT NULL,
+ "parentId" TEXT,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "PosisiOrganisasiBumDes_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "PegawaiBumDes" (
+ "id" TEXT NOT NULL,
+ "namaLengkap" VARCHAR(255) NOT NULL,
+ "gelarAkademik" VARCHAR(100),
+ "imageId" TEXT,
+ "tanggalMasuk" DATE,
+ "email" VARCHAR(255),
+ "telepon" VARCHAR(20),
+ "alamat" TEXT,
+ "posisiId" VARCHAR(50) NOT NULL,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "PegawaiBumDes_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "StrukturOrganisasiBumDes" (
+ "id" TEXT NOT NULL,
+ "posisiOrganisasiId" VARCHAR(50) NOT NULL,
+ "pegawaiId" TEXT NOT NULL,
+ "hubunganOrganisasiId" 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,
+
+ CONSTRAINT "StrukturOrganisasiBumDes_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "MitraKolaborasi" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "imageId" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "MitraKolaborasi_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "JenjangPendidikan" (
+ "id" TEXT NOT NULL,
+ "nama" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "JenjangPendidikan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Lembaga" (
+ "id" TEXT NOT NULL,
+ "nama" TEXT NOT NULL,
+ "jenjangId" 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 "Lembaga_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Siswa" (
+ "id" TEXT NOT NULL,
+ "nama" TEXT NOT NULL,
+ "lembagaId" 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 "Siswa_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Pengajar" (
+ "id" TEXT NOT NULL,
+ "nama" TEXT NOT NULL,
+ "lembagaId" 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 "Pengajar_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "KeunggulanProgram" (
+ "id" TEXT NOT NULL,
+ "judul" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "KeunggulanProgram_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "BeasiswaPendaftar" (
+ "id" TEXT NOT NULL,
+ "namaLengkap" TEXT NOT NULL,
+ "nis" TEXT,
+ "kelas" TEXT,
+ "jenisKelamin" "JenisKelamin" NOT NULL,
+ "alamatDomisili" TEXT,
+ "tempatLahir" TEXT NOT NULL,
+ "tanggalLahir" TIMESTAMP(3) NOT NULL,
+ "namaOrtu" TEXT,
+ "nik" TEXT NOT NULL,
+ "pekerjaanOrtu" TEXT,
+ "penghasilan" TEXT,
+ "noHp" TEXT NOT NULL,
+ "kewarganegaraan" TEXT,
+ "agama" "Agama",
+ "alamatKTP" TEXT,
+ "email" TEXT,
+ "statusPernikahan" "StatusPernikahan",
+ "ukuranBaju" "UkuranBaju",
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "BeasiswaPendaftar_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "TujuanProgram" (
+ "id" TEXT NOT NULL,
+ "judul" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "TujuanProgram_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "ProgramUnggulan" (
+ "id" TEXT NOT NULL,
+ "judul" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "ProgramUnggulan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "TujuanBimbinganBelajarDesa" (
+ "id" TEXT NOT NULL,
+ "judul" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "TujuanBimbinganBelajarDesa_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "LokasiJadwalBimbinganBelajarDesa" (
+ "id" TEXT NOT NULL,
+ "judul" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "LokasiJadwalBimbinganBelajarDesa_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "FasilitasBimbinganBelajarDesa" (
+ "id" TEXT NOT NULL,
+ "judul" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "FasilitasBimbinganBelajarDesa_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "TujuanPendidikanNonFormal" (
+ "id" TEXT NOT NULL,
+ "judul" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "TujuanPendidikanNonFormal_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "TempatKegiatan" (
+ "id" TEXT NOT NULL,
+ "judul" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "TempatKegiatan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "JenisProgramYangDiselenggarakan" (
+ "id" TEXT NOT NULL,
+ "judul" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "JenisProgramYangDiselenggarakan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "DataPerpustakaan" (
+ "id" TEXT NOT NULL,
+ "judul" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "kategoriId" TEXT NOT NULL,
+ "imageId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "DataPerpustakaan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "KategoriBuku" (
+ "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 "KategoriBuku_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "PeminjamanBuku" (
+ "id" TEXT NOT NULL,
+ "nama" TEXT NOT NULL,
+ "noTelp" TEXT NOT NULL,
+ "alamat" TEXT NOT NULL,
+ "bukuId" TEXT NOT NULL,
+ "tanggalPinjam" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "batasKembali" TIMESTAMP(3) NOT NULL,
+ "tanggalKembali" TIMESTAMP(3),
+ "status" "StatusPeminjaman" NOT NULL DEFAULT 'Dipinjam',
+ "catatan" 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 "PeminjamanBuku_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "User" (
+ "id" TEXT NOT NULL,
+ "username" TEXT NOT NULL,
+ "nomor" TEXT NOT NULL,
+ "roleId" TEXT NOT NULL DEFAULT '1',
+ "instansi" TEXT,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+ "lastLogin" TIMESTAMP(3),
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3),
+
+ CONSTRAINT "User_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "roles" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "description" TEXT,
+ "permissions" JSONB NOT NULL,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3),
+
+ CONSTRAINT "roles_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "KodeOtp" (
+ "id" TEXT NOT NULL,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "nomor" TEXT NOT NULL,
+ "otp" INTEGER NOT NULL,
+
+ CONSTRAINT "KodeOtp_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "permissions" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "description" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "permissions_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "UserSession" (
+ "id" TEXT NOT NULL,
+ "token" TEXT NOT NULL,
+ "expires" TIMESTAMP(3),
+ "active" BOOLEAN NOT NULL DEFAULT true,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "userId" TEXT NOT NULL,
+
+ CONSTRAINT "UserSession_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "DataPendidikan" (
+ "id" TEXT NOT NULL,
+ "name" 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 "DataPendidikan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "DesaAntiKorupsi_name_key" ON "DesaAntiKorupsi"("name");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "KategoriDesaAntiKorupsi_name_key" ON "KategoriDesaAntiKorupsi"("name");
+
+-- CreateIndex
+CREATE INDEX "APBDesItem_kode_idx" ON "APBDesItem"("kode");
+
+-- CreateIndex
+CREATE INDEX "APBDesItem_level_idx" ON "APBDesItem"("level");
+
+-- CreateIndex
+CREATE INDEX "APBDesItem_apbdesId_idx" ON "APBDesItem"("apbdesId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "KategoriPrestasiDesa_name_key" ON "KategoriPrestasiDesa"("name");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Responden_name_key" ON "Responden"("name");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "JenisKelaminResponden_name_key" ON "JenisKelaminResponden"("name");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "PilihanRatingResponden_name_key" ON "PilihanRatingResponden"("name");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "UmurResponden_name_key" ON "UmurResponden"("name");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "PegawaiPPID_email_key" ON "PegawaiPPID"("email");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "PegawaiBumDes_email_key" ON "PegawaiBumDes"("email");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "BeasiswaPendaftar_nik_key" ON "BeasiswaPendaftar"("nik");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "BeasiswaPendaftar_email_key" ON "BeasiswaPendaftar"("email");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "User_nomor_key" ON "User"("nomor");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "roles_name_key" ON "roles"("name");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "permissions_name_key" ON "permissions"("name");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "UserSession_userId_key" ON "UserSession"("userId");
+
+-- AddForeignKey
+ALTER TABLE "MediaSosial" ADD CONSTRAINT "MediaSosial_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "DesaAntiKorupsi" ADD CONSTRAINT "DesaAntiKorupsi_kategoriId_fkey" FOREIGN KEY ("kategoriId") REFERENCES "KategoriDesaAntiKorupsi"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "DesaAntiKorupsi" ADD CONSTRAINT "DesaAntiKorupsi_fileId_fkey" FOREIGN KEY ("fileId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "SdgsDesa" ADD CONSTRAINT "SdgsDesa_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "APBDes" ADD CONSTRAINT "APBDes_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "APBDes" ADD CONSTRAINT "APBDes_fileId_fkey" FOREIGN KEY ("fileId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "APBDesItem" ADD CONSTRAINT "APBDesItem_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "APBDesItem"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "APBDesItem" ADD CONSTRAINT "APBDesItem_apbdesId_fkey" FOREIGN KEY ("apbdesId") REFERENCES "APBDes"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "PrestasiDesa" ADD CONSTRAINT "PrestasiDesa_kategoriId_fkey" FOREIGN KEY ("kategoriId") REFERENCES "KategoriPrestasiDesa"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "PrestasiDesa" ADD CONSTRAINT "PrestasiDesa_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Responden" ADD CONSTRAINT "Responden_jenisKelaminId_fkey" FOREIGN KEY ("jenisKelaminId") REFERENCES "JenisKelaminResponden"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Responden" ADD CONSTRAINT "Responden_ratingId_fkey" FOREIGN KEY ("ratingId") REFERENCES "PilihanRatingResponden"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Responden" ADD CONSTRAINT "Responden_kelompokUmurId_fkey" FOREIGN KEY ("kelompokUmurId") REFERENCES "UmurResponden"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "StrukturPPID" ADD CONSTRAINT "StrukturPPID_posisiOrganisasiPPIDId_fkey" FOREIGN KEY ("posisiOrganisasiPPIDId") REFERENCES "PosisiOrganisasiPPID"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "StrukturPPID" ADD CONSTRAINT "StrukturPPID_pegawaiPPIDId_fkey" FOREIGN KEY ("pegawaiPPIDId") REFERENCES "PegawaiPPID"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "PosisiOrganisasiPPID" ADD CONSTRAINT "PosisiOrganisasiPPID_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "PosisiOrganisasiPPID"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "PegawaiPPID" ADD CONSTRAINT "PegawaiPPID_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "PegawaiPPID" ADD CONSTRAINT "PegawaiPPID_posisiId_fkey" FOREIGN KEY ("posisiId") REFERENCES "PosisiOrganisasiPPID"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "StrukturOrganisasiPPID" ADD CONSTRAINT "StrukturOrganisasiPPID_posisiOrganisasiId_fkey" FOREIGN KEY ("posisiOrganisasiId") REFERENCES "PosisiOrganisasiPPID"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "StrukturOrganisasiPPID" ADD CONSTRAINT "StrukturOrganisasiPPID_pegawaiId_fkey" FOREIGN KEY ("pegawaiId") REFERENCES "PegawaiPPID"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "PerbekelDariMasaKeMasa" ADD CONSTRAINT "PerbekelDariMasaKeMasa_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "PotensiDesa" ADD CONSTRAINT "PotensiDesa_kategoriId_fkey" FOREIGN KEY ("kategoriId") REFERENCES "KategoriPotensi"("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 SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "PelayananSuratKeterangan" ADD CONSTRAINT "PelayananSuratKeterangan_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "PelayananSuratKeterangan" ADD CONSTRAINT "PelayananSuratKeterangan_image2Id_fkey" FOREIGN KEY ("image2Id") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "AjukanPermohonan" ADD CONSTRAINT "AjukanPermohonan_kategoriId_fkey" FOREIGN KEY ("kategoriId") REFERENCES "PelayananSuratKeterangan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Penghargaan" ADD CONSTRAINT "Penghargaan_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "JadwalKegiatan" ADD CONSTRAINT "JadwalKegiatan_pendaftaranJadwalKegiatanId_fkey" FOREIGN KEY ("pendaftaranJadwalKegiatanId") REFERENCES "PendaftaranJadwalKegiatan"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "DataKematian_Kelahiran" ADD CONSTRAINT "DataKematian_Kelahiran_kematianId_fkey" FOREIGN KEY ("kematianId") REFERENCES "Kematian"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "DataKematian_Kelahiran" ADD CONSTRAINT "DataKematian_Kelahiran_kelahiranId_fkey" FOREIGN KEY ("kelahiranId") REFERENCES "Kelahiran"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "ArtikelKesehatan" ADD CONSTRAINT "ArtikelKesehatan_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "KontakDaruratKeamanan" ADD CONSTRAINT "KontakDaruratKeamanan_kategoriId_fkey" FOREIGN KEY ("kategoriId") REFERENCES "KontakItem"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "KontakDaruratToItem" ADD CONSTRAINT "KontakDaruratToItem_kontakDaruratId_fkey" FOREIGN KEY ("kontakDaruratId") REFERENCES "KontakDaruratKeamanan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "KontakDaruratToItem" ADD CONSTRAINT "KontakDaruratToItem_kontakItemId_fkey" FOREIGN KEY ("kontakItemId") REFERENCES "KontakItem"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "StrukturBumDes" ADD CONSTRAINT "StrukturBumDes_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "StrukturBumDes" ADD CONSTRAINT "StrukturBumDes_posisiOrganisasiBumDesId_fkey" FOREIGN KEY ("posisiOrganisasiBumDesId") REFERENCES "PosisiOrganisasiBumDes"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "StrukturBumDes" ADD CONSTRAINT "StrukturBumDes_pegawaiBumDesId_fkey" FOREIGN KEY ("pegawaiBumDesId") REFERENCES "PegawaiBumDes"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "PosisiOrganisasiBumDes" ADD CONSTRAINT "PosisiOrganisasiBumDes_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "PosisiOrganisasiBumDes"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "PegawaiBumDes" ADD CONSTRAINT "PegawaiBumDes_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "PegawaiBumDes" ADD CONSTRAINT "PegawaiBumDes_posisiId_fkey" FOREIGN KEY ("posisiId") REFERENCES "PosisiOrganisasiBumDes"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "StrukturOrganisasiBumDes" ADD CONSTRAINT "StrukturOrganisasiBumDes_posisiOrganisasiId_fkey" FOREIGN KEY ("posisiOrganisasiId") REFERENCES "PosisiOrganisasiBumDes"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "StrukturOrganisasiBumDes" ADD CONSTRAINT "StrukturOrganisasiBumDes_pegawaiId_fkey" FOREIGN KEY ("pegawaiId") REFERENCES "PegawaiBumDes"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "MitraKolaborasi" ADD CONSTRAINT "MitraKolaborasi_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Lembaga" ADD CONSTRAINT "Lembaga_jenjangId_fkey" FOREIGN KEY ("jenjangId") REFERENCES "JenjangPendidikan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Siswa" ADD CONSTRAINT "Siswa_lembagaId_fkey" FOREIGN KEY ("lembagaId") REFERENCES "Lembaga"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Pengajar" ADD CONSTRAINT "Pengajar_lembagaId_fkey" FOREIGN KEY ("lembagaId") REFERENCES "Lembaga"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "DataPerpustakaan" ADD CONSTRAINT "DataPerpustakaan_kategoriId_fkey" FOREIGN KEY ("kategoriId") REFERENCES "KategoriBuku"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "DataPerpustakaan" ADD CONSTRAINT "DataPerpustakaan_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "PeminjamanBuku" ADD CONSTRAINT "PeminjamanBuku_bukuId_fkey" FOREIGN KEY ("bukuId") REFERENCES "DataPerpustakaan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "User" ADD CONSTRAINT "User_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "roles"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "UserSession" ADD CONSTRAINT "UserSession_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 7aa39b9a..954a144b 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -183,41 +183,41 @@ model SdgsDesa {
//========================================= APBDes ========================================= //
model APBDes {
- id String @id @default(cuid())
- tahun Int?
- name String? // misalnya: "APBDes Tahun 2025"
- deskripsi String?
- jumlah String? // total keseluruhan (opsional, bisa juga dihitung dari items)
- items APBDesItem[]
- image FileStorage? @relation("APBDesImage", fields: [imageId], references: [id])
- imageId String?
- file FileStorage? @relation("APBDesFile", fields: [fileId], references: [id])
- fileId String?
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
- deletedAt DateTime? // opsional, tidak perlu default now()
- isActive Boolean @default(true)
+ id String @id @default(cuid())
+ tahun Int?
+ name String? // misalnya: "APBDes Tahun 2025"
+ deskripsi String?
+ jumlah String? // total keseluruhan (opsional, bisa juga dihitung dari items)
+ items APBDesItem[]
+ image FileStorage? @relation("APBDesImage", fields: [imageId], references: [id])
+ imageId String?
+ file FileStorage? @relation("APBDesFile", fields: [fileId], references: [id])
+ fileId String?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime? // opsional, tidak perlu default now()
+ isActive Boolean @default(true)
}
model APBDesItem {
- id String @id @default(cuid())
- kode String // contoh: "4", "4.1", "4.1.2"
- uraian String // nama item, contoh: "Pendapatan Asli Desa", "Hasil Usaha"
- anggaran Float // dalam satuan Rupiah (bisa DECIMAL di DB, tapi Float umum di TS/JS)
- realisasi Float
- selisih Float // realisasi - anggaran
- persentase Float
- tipe String? // (realisasi / anggaran) * 100
- level Int // 1 = kelompok utama, 2 = sub-kelompok, 3 = detail
- parentId String? // untuk relasi hierarki
- parent APBDesItem? @relation("APBDesItemParent", fields: [parentId], references: [id])
- children APBDesItem[] @relation("APBDesItemParent")
- apbdesId String
- apbdes APBDes @relation(fields: [apbdesId], references: [id])
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
- deletedAt DateTime?
- isActive Boolean @default(true)
+ id String @id @default(cuid())
+ kode String // contoh: "4", "4.1", "4.1.2"
+ uraian String // nama item, contoh: "Pendapatan Asli Desa", "Hasil Usaha"
+ anggaran Float // dalam satuan Rupiah (bisa DECIMAL di DB, tapi Float umum di TS/JS)
+ realisasi Float
+ selisih Float // realisasi - anggaran
+ persentase Float
+ tipe String? // (realisasi / anggaran) * 100
+ level Int // 1 = kelompok utama, 2 = sub-kelompok, 3 = detail
+ parentId String? // untuk relasi hierarki
+ parent APBDesItem? @relation("APBDesItemParent", fields: [parentId], references: [id])
+ children APBDesItem[] @relation("APBDesItemParent")
+ apbdesId String
+ apbdes APBDes @relation(fields: [apbdesId], references: [id])
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime?
+ isActive Boolean @default(true)
@@index([kode])
@@index([level])
@@ -2163,25 +2163,27 @@ enum StatusPeminjaman {
// ========================================= USER ========================================= //
model User {
- id String @id @default(cuid())
- username String
- nomor String @unique
- role Role @relation(fields: [roleId], references: [id])
- roleId String @default("1")
- instansi String?
- UserSession UserSession? // Nama instansi (Puskesmas, Sekolah, dll)
- isActive Boolean @default(true)
- lastLogin DateTime?
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
- deletedAt DateTime?
+ id String @id @default(cuid())
+ username String
+ nomor String @unique
+ roleId String @default("2")
+ isActive Boolean @default(false)
+ sessionInvalid Boolean @default(false)
+ lastLogin DateTime?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @default(now()) @updatedAt
+
+ sessions UserSession[] // ✅ Relasi one-to-many
+ role Role @relation(fields: [roleId], references: [id])
+ menuAccesses UserMenuAccess[]
+
+ @@map("users")
}
model Role {
id String @id @default(cuid())
name String @unique // ADMIN_DESA, ADMIN_KESEHATAN, ADMIN_SEKOLAH
description String?
- permissions Json // Menyimpan permission dalam format JSON
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -2200,26 +2202,32 @@ model KodeOtp {
otp Int
}
-// Tabel untuk menyimpan permission
-model Permission {
- id String @id @default(cuid())
- name String @unique
- description String?
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
-
- @@map("permissions")
-}
-
model UserSession {
id String @id @default(cuid())
- token String
- expires DateTime?
+ token String @db.Text // ✅ JWT bisa panjang
+ expiresAt DateTime // ✅ Ubah jadi expiresAt (konsisten)
active Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
- User User @relation(fields: [userId], references: [id])
- userId String @unique
+
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+ userId String // ✅ HAPUS @unique - user bisa punya multiple sessions
+
+ @@index([userId]) // ✅ Index untuk query cepat
+ @@index([token]) // ✅ Index untuk verify cepat
+ @@map("user_sessions")
+}
+
+model UserMenuAccess {
+ id String @id @default(cuid())
+ userId String
+ menuId String // ID menu (misal: "Landing Page", "Kesehatan")
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ user User @relation(fields: [userId], references: [id])
+
+ @@unique([userId, menuId]) // Satu user tidak bisa punya akses menu yang sama dua kali
}
// ========================================= DATA PENDIDIKAN ========================================= //
diff --git a/prisma/seed.ts b/prisma/seed.ts
index 52de4324..53d35817 100644
--- a/prisma/seed.ts
+++ b/prisma/seed.ts
@@ -54,50 +54,48 @@ import tujuanProgram2 from "./data/pendidikan/pendidikan-non-formal/tujuan-progr
import programUnggulan from "./data/pendidikan/program-pendidikan-anak/program-unggulan.json";
import tujuanProgram from "./data/pendidikan/program-pendidikan-anak/tujuan-program.json";
import roles from "./data/user/roles.json";
-import users from "./data/user/users.json";
import fileStorage from "./data/file-storage.json";
import jenjangPendidikan from "./data/pendidikan/info-sekolah/jenjang-pendidikan.json";
import seedAssets from "./seed_assets";
+import users from "./data/user/users.json";
import { safeSeedUnique } from "./safeseedUnique";
(async () => {
- // =========== USER & ROLE ===========
+ // =========== USER ===========
+ console.log("🔄 Seeding user...");
+ for (const u of users) {
+ await safeSeedUnique(
+ "user",
+ { id: u.id },
+ {
+ username: u.username,
+ nomor: u.nomor,
+ roleId: u.roleId.toString(),
+ isActive: u.isActive,
+ sessionInvalid: false,
+ }
+ );
+ }
+
+ console.log("✅ Roles seeded");
+ // =========== ROLE ===========
// In your seed.ts
// =========== ROLES ===========
console.log("🔄 Seeding roles...");
for (const r of roles) {
- await safeSeedUnique("role", { id: r.id }, {
- name: r.name,
- description: r.description,
- permissions: r.permissions,
- isActive: r.isActive,
- });
+ await safeSeedUnique(
+ "role",
+ { id: r.id },
+ {
+ name: r.name,
+ description: r.description,
+ isActive: r.isActive,
+ }
+ );
}
console.log("✅ Roles seeded");
- // =========== USERS ===========
- console.log("🔄 Seeding users...");
- for (const u of users) {
- // First verify the role exists
- const roleExists = await prisma.role.findUnique({
- where: { id: u.roleId },
- });
-
- if (!roleExists) {
- console.error(`❌ Role with id ${u.roleId} not found for user ${u.nama}`);
- continue;
- }
-
- await safeSeedUnique("user", { id: u.id }, {
- username: u.nama,
- nomor: u.nomor,
- roleId: u.roleId,
- isActive: u.isActive,
- });
- }
- console.log("✅ Users seeded");
-
// =========== FILE STORAGE ===========
console.log("🔄 Seeding file storage...");
for (const f of fileStorage) {
@@ -811,7 +809,9 @@ import { safeSeedUnique } from "./safeseedUnique";
const flattenedPosisiBumdes = posisiOrganisasi.flat();
// ✅ Urutkan berdasarkan hierarki
- const sortedPosisiBumdes = flattenedPosisiBumdes.sort((a, b) => a.hierarki - b.hierarki);
+ const sortedPosisiBumdes = flattenedPosisiBumdes.sort(
+ (a, b) => a.hierarki - b.hierarki
+ );
for (const p of sortedPosisiBumdes) {
console.log(`Seeding: ${p.nama} (id: ${p.id}, parent: ${p.parentId})`);
@@ -891,7 +891,7 @@ import { safeSeedUnique } from "./safeseedUnique";
// Add IDs to the kategoriKegiatan data
const kategoriKegiatan = kategoriKegiatanData.map((k, index) => ({
...k,
- id: `kategori-${index + 1}`
+ id: `kategori-${index + 1}`,
}));
for (const k of kategoriKegiatan) {
@@ -1183,7 +1183,6 @@ import { safeSeedUnique } from "./safeseedUnique";
// seed assets
await seedAssets();
-
})()
.then(() => prisma.$disconnect())
.catch((e) => {
diff --git a/src/app/admin/(dashboard)/_state/user/user-state.ts b/src/app/admin/(dashboard)/_state/user/user-state.ts
index 93594956..e3864904 100644
--- a/src/app/admin/(dashboard)/_state/user/user-state.ts
+++ b/src/app/admin/(dashboard)/_state/user/user-state.ts
@@ -90,42 +90,96 @@ const userState = proxy({
}
},
},
- updateActive: {
+ deleteUser: {
loading: false,
- async submit(id: string, isActive: boolean) {
- this.loading = true;
+
+ async delete(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
try {
- const res = await fetch(`/api/user/updt`, {
- method: "PUT",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ id, isActive }),
+ userState.deleteUser.loading = true;
+
+ const response = await fetch(`/api/user/delUser/${id}`, {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
});
-
- const data = await res.json();
- if (res.status === 200 && data.success) {
- toast.success(data.message);
- userState.findMany.load(userState.findMany.page, 10, userState.findMany.search);
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "User berhasil dihapus permanen");
+ await userState.findMany.load(); // refresh list user setelah delete
} else {
- toast.error(data.message || "Gagal update status user");
+ toast.error(result?.message || "Gagal menghapus user");
}
- } catch (e) {
- console.error(e);
- toast.error("Gagal update status user");
+ } catch (error) {
+ console.error("Gagal delete user:", error);
+ toast.error("Terjadi kesalahan saat menghapus user");
} finally {
- this.loading = false;
+ userState.deleteUser.loading = false;
}
},
+ },
+ // Di file userState.ts atau dimana state user berada
+
+update: {
+ loading: false,
+
+ async submit(payload: { id: string; isActive?: boolean; roleId?: string }) {
+ this.loading = true;
+ try {
+ const res = await fetch(`/api/user/updt`, {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(payload),
+ });
+
+ const data = await res.json();
+
+ if (res.status === 200 && data.success) {
+ // ✅ Tampilkan pesan yang berbeda jika role berubah
+ if (data.roleChanged) {
+ toast.success(
+ `${data.message}\n\nUser akan logout otomatis dalam beberapa detik.`,
+ {
+ autoClose: 5000,
+ }
+ );
+ } else {
+ toast.success(data.message);
+ }
+
+ // Refresh list
+ await userState.findMany.load(
+ userState.findMany.page,
+ 10,
+ userState.findMany.search
+ );
+
+ return true; // ✅ Return success untuk handling di component
+ } else {
+ toast.error(data.message || "Gagal update user");
+ return false;
+ }
+ } catch (e) {
+ console.error("❌ Error update user:", e);
+ toast.error("Gagal update user");
+ return false;
+ } finally {
+ this.loading = false;
+ }
},
+},
});
const templateRole = z.object({
name: z.string().min(1, "Nama harus diisi"),
- permissions: z.array(z.string()).min(1, "Permission harus diisi"),
});
const defaultRole = {
name: "",
- permissions: [] as string[],
};
const roleState = proxy({
@@ -237,7 +291,7 @@ const roleState = proxy({
toast.warn("ID tidak valid");
return null;
}
-
+
try {
const response = await fetch(`/api/role/${id}`, {
method: "GET",
@@ -245,31 +299,25 @@ const roleState = proxy({
"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 = {
+
+ // langsung set melalui root state, bukan this
+ roleState.update.id = data.id;
+ roleState.update.form = {
name: data.name,
- permissions: data.permissions,
};
- return data; // Return the loaded data
- } else {
- throw new Error(result?.message || "Gagal memuat data");
+
+ return data;
}
} catch (error) {
console.error("Error loading role:", error);
- toast.error(
- error instanceof Error ? error.message : "Gagal memuat data"
- );
- return null;
+ toast.error("Gagal memuat data");
}
- },
+ },
async update() {
const cek = templateRole.safeParse(roleState.update.form);
if (!cek.success) {
@@ -290,7 +338,6 @@ const roleState = proxy({
},
body: JSON.stringify({
name: this.form.name,
- permissions: this.form.permissions,
}),
});
diff --git a/src/app/admin/(dashboard)/auth/login-admin/page.tsx b/src/app/admin/(dashboard)/auth/login-admin/page.tsx
index ab591207..a44f35b0 100644
--- a/src/app/admin/(dashboard)/auth/login-admin/page.tsx
+++ b/src/app/admin/(dashboard)/auth/login-admin/page.tsx
@@ -1,104 +1,98 @@
-'use client'
-import { apiFetchLogin } from '@/app/admin/auth/_lib/api_fetch_auth';
+'use client';
+import { apiFetchLogin } from '@/app/api/auth/_lib/api_fetch_auth';
import colors from '@/con/colors';
-import { Box, Button, Center, Flex, Image, Paper, Stack, Text, Title } from '@mantine/core';
-import Link from 'next/link';
+import { Box, Button, Center, Image, Paper, Stack, Title } from '@mantine/core';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
-import { PhoneInput } from "react-international-phone";
-import "react-international-phone/style.css";
+import { PhoneInput } from 'react-international-phone';
+import 'react-international-phone/style.css';
import { toast } from 'react-toastify';
-
-
function Login() {
- const router = useRouter()
- const [phone, setPhone] = useState("")
- const [isError, setError] = useState(false)
- const [loading, setLoading] = useState(false)
+ const router = useRouter();
+ const [phone, setPhone] = useState('');
+ const [loading, setLoading] = useState(false);
+ // Login.tsx
async function onLogin() {
- const nomor = phone.substring(1);
- if (nomor.length <= 4) return setError(true)
-
+ const cleanPhone = phone.replace(/\D/g, '');
+ if (cleanPhone.length < 10) {
+ toast.error('Nomor telepon tidak valid');
+ return;
+ }
try {
setLoading(true);
- const response = await apiFetchLogin({ nomor: nomor })
- if (response && response.success) {
- localStorage.setItem("hipmi_auth_code_id", response.kodeId);
- toast.success(response.message);
- router.push("/validasi", { scroll: false });
+ const response = await apiFetchLogin({ nomor: cleanPhone });
+
+ if (!response.success) {
+ toast.error(response.message || 'Gagal memproses login');
+ return;
+ }
+
+ // Simpan nomor untuk register
+ localStorage.setItem('auth_nomor', cleanPhone);
+
+ if (response.isRegistered) {
+ // ✅ User lama: simpan kodeId & ke validasi
+ localStorage.setItem('auth_kodeId', response.kodeId);
+ router.push('/validasi');
} else {
- setLoading(false);
- toast.error(response?.message);
+ // ❌ User baru: langsung ke registrasi (tanpa kodeId)
+ router.push('/registrasi');
}
} catch (error) {
- setLoading(false)
- console.log("Error Login", error)
- toast.error("Terjadi kesalahan saat login")
+ console.error('Error Login:', error);
+ toast.error('Terjadi kesalahan saat login');
+ } finally {
+ setLoading(false);
}
}
return (
-
+
-
-
-
+
+
+
-
+
Login
-
+
-
- {/*
- Masuk Untuk Akses Admin
- setUsername(e.target.value)}
- required
- />
- */}
+
{
- setPhone(val);
- }}
+ value={phone}
+ onChange={(val) => setPhone(val)}
/>
- {isError ? (
- toast.error("Masukan nomor telepon anda")
- ) : (
- ""
- )}
-
+
Masuk
+ loading={loading}
+ >
+ Masuk
-
- Belum punya akun?
-
- Registrasi
-
-
@@ -108,4 +102,4 @@ function Login() {
);
}
-export default Login;
+export default Login;
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/auth/registrasi-admin/page.tsx b/src/app/admin/(dashboard)/auth/registrasi-admin/page.tsx
index 62d2554b..537ae814 100644
--- a/src/app/admin/(dashboard)/auth/registrasi-admin/page.tsx
+++ b/src/app/admin/(dashboard)/auth/registrasi-admin/page.tsx
@@ -1,113 +1,127 @@
-/* eslint-disable @typescript-eslint/no-unused-expressions */
-'use client'
-import { apiFetchRegister } from '@/app/admin/auth/_lib/api_fetch_auth';
+// app/registrasi/page.tsx
+'use client';
+
+import { apiFetchRegister } from '@/app/api/auth/_lib/api_fetch_auth';
import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto';
import colors from '@/con/colors';
-import { Box, Button, Center, Checkbox, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
+import {
+ Box, Button, Center, Checkbox, Image, Paper, Stack, Text, TextInput, Title,
+} from '@mantine/core';
import { useRouter } from 'next/navigation';
-import { useState } from 'react';
-import { PhoneInput } from "react-international-phone";
-import "react-international-phone/style.css";
+import { useEffect, useState } from 'react';
+import { PhoneInput } from 'react-international-phone';
+import 'react-international-phone/style.css';
import { toast } from 'react-toastify';
-function Registrasi() {
- const [phone, setPhone] = useState("")
- const router = useRouter()
- const [value, setValue] = useState("")
- const [isValue, setIsValue] = useState(false);
+export default function Registrasi() {
+ const router = useRouter();
+ const [username, setUsername] = useState('');
const [loading, setLoading] = useState(false);
+ const [phone, setPhone] = useState(''); // ✅ tambahkan state untuk phone
- async function onRegistarsi() {
- if (value.length < 5) {
- toast.error("Username minimal 5 karakter!");
+ // Ambil data dari localStorage (dari login)
+ useEffect(() => {
+ const storedNomor = localStorage.getItem('auth_nomor');
+ if (!storedNomor) {
+ toast.error('Akses tidak valid');
+ router.push('/login');
return;
}
-
- if (value.includes(" ")) {
- toast.error("Username tidak boleh ada spasi!");
+ setPhone(storedNomor);
+ }, [router]);
+
+ const handleRegister = async () => {
+ if (!username || username.trim().length < 5) {
+ toast.error('Username minimal 5 karakter!');
return;
}
-
- if (!phone) {
- toast.error("Nomor telepon wajib diisi!");
+ if (username.includes(' ')) {
+ toast.error('Username tidak boleh ada spasi!');
return;
}
-
+
+ const cleanPhone = phone.replace(/\D/g, '');
+ if (cleanPhone.length < 10) {
+ toast.error('Nomor tidak valid!');
+ return;
+ }
+
try {
setLoading(true);
- const respone = await apiFetchRegister({ nomor: phone, username: value });
+ // ✅ Hanya kirim username & nomor → dapat kodeId
+ const response = await apiFetchRegister({ username, nomor: cleanPhone });
- if (respone.success) {
- router.push("/login", { scroll: false });
- toast.success(respone.message);
+ if (response.success) {
+ // Simpan sementara
+ localStorage.setItem('auth_kodeId', response.kodeId);
+ localStorage.setItem('auth_username', username); // simpan username
- } else {
- setLoading(false);
- toast.error(respone.message);
+ toast.success('Kode verifikasi dikirim!');
+ router.push('/validasi'); // ✅ ke halaman validasi
}
} catch (error) {
+ console.error('Error Registrasi:', error);
+ toast.error('Gagal mengirim OTP');
+ } finally {
setLoading(false);
- console.log("Error Registrasi", error);
}
- }
+ };
+
return (
-
+
-
-
-
-
+
+
+
+
Registrasi
-
+
-
-
+ setUsername(e.currentTarget.value)}
error={
- value.length > 0 && value.length < 5
- ? "Minimal 5 karakter !"
- : value.includes(" ")
- ? "Tidak boleh ada spasi"
- : isValue
- ? "Masukan username anda"
- : ""
+ username.length > 0 && username.length < 5
+ ? 'Minimal 5 karakter!'
+ : username.includes(' ')
+ ? 'Tidak boleh ada spasi'
+ : ''
}
- onChange={(val) => {
- val.currentTarget.value.length > 0 ? setIsValue(false) : "";
- setValue(val.currentTarget.value);
- }}
required
-
/>
-
- Nomor Telepon
+
+
+ Nomor Telepon
{
- setPhone(val);
- }}
+ value={phone}
+ disabled
/>
-
-
+
+
+
-
- Daftar
+
+
+
+ Kirim Kode Verifikasi
+
@@ -116,6 +130,4 @@ function Registrasi() {
);
-}
-
-export default Registrasi;
+}
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/auth/validasi-admin/page.tsx b/src/app/admin/(dashboard)/auth/validasi-admin/page.tsx
index 862edb33..0ed24cd3 100644
--- a/src/app/admin/(dashboard)/auth/validasi-admin/page.tsx
+++ b/src/app/admin/(dashboard)/auth/validasi-admin/page.tsx
@@ -1,31 +1,270 @@
-'use client'
-import colors from '@/con/colors';
-import { Box, Button, Paper, PinInput, Stack, Text, Title } from '@mantine/core';
-import { useRouter } from 'next/navigation';
+'use client';
+
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Loader,
+ Paper,
+ PinInput,
+ Stack,
+ Text,
+ Title,
+} from '@mantine/core';
+import { useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { authStore } from '@/store/authStore';
+
+export default function Validasi() {
+ const router = useRouter();
+ const [nomor, setNomor] = useState(null);
+ const [otp, setOtp] = useState('');
+ const [loading, setLoading] = useState(false);
+ const [isLoading, setIsLoading] = useState(true);
+ const [kodeId, setKodeId] = useState(null);
+ const [isRegistrationFlow, setIsRegistrationFlow] = useState(false); // Tambahkan flag
+
+ // Cek apakah ini alur registrasi
+ useEffect(() => {
+ const storedUsername = localStorage.getItem('auth_username');
+ setIsRegistrationFlow(!!storedUsername);
+ }, []);
+
+ useEffect(() => {
+ const storedKodeId = localStorage.getItem('auth_kodeId');
+ if (!storedKodeId) {
+ toast.error('Akses tidak valid');
+ router.replace('/login');
+ return;
+ }
+
+ setKodeId(storedKodeId);
+ const loadOtpData = async () => {
+ try {
+ const res = await fetch(`/api/auth/otp-data?kodeId=${encodeURIComponent(storedKodeId)}`);
+ const result = await res.json();
+
+ if (res.ok && result.data?.nomor) {
+ setNomor(result.data.nomor);
+ } else {
+ throw new Error('Data OTP tidak valid');
+ }
+ } catch (error) {
+ console.error('Gagal memuat data OTP:', error);
+ toast.error('Kode verifikasi tidak valid');
+ router.replace('/login');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+ loadOtpData();
+ }, [router]);
+
+ const handleVerify = async () => {
+ if (!kodeId || !nomor || otp.length < 4) return;
+
+ setLoading(true);
+ try {
+ if (isRegistrationFlow) {
+ // 🔑 Alur REGISTRASI
+ await handleRegistrationVerification();
+ } else {
+ // 🔑 Alur LOGIN
+ await handleLoginVerification();
+ }
+ } catch (error) {
+ console.error('Error saat verifikasi:', error);
+ toast.error('Terjadi kesalahan sistem');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+const handleRegistrationVerification = async () => {
+ const username = localStorage.getItem('auth_username');
+ if (!username) {
+ toast.error('Data registrasi tidak ditemukan.');
+ return;
+ }
+
+ const cleanNomor = nomor?.replace(/\D/g, '') ?? '';
+ if (cleanNomor.length < 10 || username.trim().length < 5) {
+ toast.error('Data tidak valid');
+ return;
+ }
+
+ // Verifikasi OTP dulu
+ const verifyRes = await fetch('/api/auth/verify-otp-register', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ nomor: cleanNomor, otp, kodeId }),
+ });
+
+ const verifyData = await verifyRes.json();
+ if (!verifyRes.ok) {
+ toast.error(verifyData.message || 'Verifikasi OTP gagal');
+ return;
+ }
+
+ // ✅ Kirim ke finalize-registration → akan redirect ke /waiting-room
+ const finalizeRes = await fetch('/api/auth/finalize-registration', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ nomor, username, kodeId }),
+ credentials: 'include'
+ });
+
+ if (finalizeRes.redirected) {
+ // ✅ Redirect otomatis oleh server
+ window.location.href = finalizeRes.url;
+ } else {
+ const data = await finalizeRes.json();
+ toast.error(data.message || 'Registrasi gagal');
+ }
+};
+
+ // ✅ Verifikasi OTP untuk LOGIN
+ const handleLoginVerification = async () => {
+ const loginRes = await fetch('/api/auth/verify-otp-login', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ nomor, otp, kodeId }),
+ });
+
+ const loginData = await loginRes.json();
+
+ if (!loginRes.ok) {
+ toast.error(loginData.message || 'Verifikasi gagal');
+ return;
+ }
+
+ const { id, name, roleId, isActive } = loginData.user;
+
+ authStore.setUser({
+ id,
+ name: name || 'User',
+ roleId: Number(roleId),
+ });
+
+ cleanupStorage();
+
+ if (!isActive) {
+ window.location.href = '/waiting-room';
+ return;
+ }
+
+ const redirectPath = getRedirectPath(Number(roleId));
+ router.replace(redirectPath);
+ };
+
+ const getRedirectPath = (roleId: number): string => {
+ switch (roleId) {
+ case 0: // DEVELOPER
+ case 1: // SUPERADMIN
+ case 2: // ADMIN_DESA
+ return '/admin/landing-page/profil/program-inovasi';
+ case 3: // ADMIN_KESEHATAN
+ return '/admin/kesehatan/posyandu';
+ case 4: // ADMIN_PENDIDIKAN
+ return '/admin/pendidikan/info-sekolah/jenjang-pendidikan';
+ default:
+ return '/admin';
+ }
+ };
+
+ const cleanupStorage = () => {
+ localStorage.removeItem('auth_kodeId');
+ localStorage.removeItem('auth_nomor');
+ localStorage.removeItem('auth_username');
+ };
+
+ const handleResend = async () => {
+ if (!nomor) return;
+ try {
+ const res = await fetch('/api/auth/resend', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ nomor }),
+ });
+ const data = await res.json();
+ if (data.success) {
+ localStorage.setItem('auth_kodeId', data.kodeId);
+ toast.success('OTP baru dikirim');
+ } else {
+ toast.error(data.message || 'Gagal mengirim ulang OTP');
+ }
+ } catch {
+ toast.error('Gagal menghubungi server');
+ }
+ };
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ if (!nomor) return null;
-function Validasi() {
- const router = useRouter()
return (
-
+
-
-
-
+
+
+
-
- Kode Verifikasi
+
+ {isRegistrationFlow ? 'Verifikasi Registrasi' : 'Verifikasi Login'}
+
+ Kami telah mengirim kode ke nomor {nomor}
+
-
-
- Masukkan Kode Verifikasi
-
+
+
+
+ Masukkan Kode Verifikasi
+
+
+
+
-
- router.push("/admin/landing-page/profile/program-inovasi")}>
- Page
+
+
+ Verifikasi
+
+
+
+ Tidak menerima kode?{' '}
+
+ Kirim Ulang
-
+
@@ -33,6 +272,4 @@ function Validasi() {
);
-}
-
-export default Validasi;
+}
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/user&role/_com/dynamicNavbar.ts b/src/app/admin/(dashboard)/user&role/_com/dynamicNavbar.ts
new file mode 100644
index 00000000..553aae58
--- /dev/null
+++ b/src/app/admin/(dashboard)/user&role/_com/dynamicNavbar.ts
@@ -0,0 +1,34 @@
+// src/app/admin/(dashboard)/user&role/_com/dynamicNavbar.ts
+import { devBar, navBar, role1, role2, role3 } from '@/app/admin/_com/list_PageAdmin';
+
+// ✅ Helper: normalisasi ID menu agar konsisten
+const normalizeMenuId = (id: string): string => {
+ return id.trim().toLowerCase();
+};
+
+export function getNavbar({
+ roleId,
+ menuIds,
+}: {
+ roleId: number;
+ menuIds?: string[] | null;
+}) {
+ // ✅ Jika menuIds tersedia, gunakan untuk filter — dengan normalisasi
+ if (menuIds && menuIds.length > 0) {
+ // Normalisasi semua menuIds dari DB/state
+ const normalizedMenuSet = new Set(menuIds.map(id => normalizeMenuId(id)));
+
+ return navBar.filter(section => {
+ const normalizedSectionId = normalizeMenuId(section.id);
+ return normalizedMenuSet.has(normalizedSectionId);
+ });
+ }
+
+ // 🔁 Fallback ke role-based navigation
+ if (roleId === 0) return devBar;
+ if (roleId === 1) return navBar;
+ if (roleId === 2) return role1;
+ if (roleId === 3) return role2;
+ if (roleId === 4) return role3;
+ return [];
+}
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/user&role/layout.tsx b/src/app/admin/(dashboard)/user&role/layout.tsx
index c8e9802a..929c905b 100644
--- a/src/app/admin/(dashboard)/user&role/layout.tsx
+++ b/src/app/admin/(dashboard)/user&role/layout.tsx
@@ -2,7 +2,7 @@
'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
-import { IconForms, IconUser } from '@tabler/icons-react';
+import { IconBrush, IconForms, IconUser } from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
@@ -23,6 +23,12 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
href: "/admin/user&role/role",
icon: ,
},
+ {
+ label: "Menu Access",
+ value: "menu-access",
+ href: "/admin/user&role/menu-access",
+ icon: ,
+ }
];
const currentTab = tabs.find(tab => tab.href === pathname);
diff --git a/src/app/admin/(dashboard)/user&role/menu-access/page.tsx b/src/app/admin/(dashboard)/user&role/menu-access/page.tsx
new file mode 100644
index 00000000..b31de9fd
--- /dev/null
+++ b/src/app/admin/(dashboard)/user&role/menu-access/page.tsx
@@ -0,0 +1,129 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+// src/app/admin/user&role/menu-access/page.tsx
+
+'use client'
+
+import { navBar } from '@/app/admin/_com/list_PageAdmin'
+import { Button, Checkbox, Group, Paper, Select, Stack, Text, Title } from '@mantine/core'
+import { useEffect, useState } from 'react'
+import { useProxy } from 'valtio/utils'
+import user from '../../_state/user/user-state'
+import { useShallowEffect } from '@mantine/hooks'
+
+
+// ✅ Helper: ekstrak semua menu ID dari struktur navBar
+const extractMenuIds = (navSections: typeof navBar) => {
+ return navSections.map(section => ({
+ value: section.id, // "Landing Page", "Kesehatan", dll
+ label: section.name // "Landing Page", "Kesehatan", dll
+ }));
+};
+
+function MenuAccessPage() {
+ const stateUser = useProxy(user.userState)
+ const [selectedUserId, setSelectedUserId] = useState(null)
+ const [userMenus, setUserMenus] = useState([])
+
+ useShallowEffect(() => {
+ stateUser.findMany.load()
+ }, [])
+
+ // ✅ Gunakan helper untuk ekstrak menu
+ const availableMenus = extractMenuIds(navBar);
+
+ // Ambil data menu akses user
+ const loadUserMenuAccess = async () => {
+ if (!selectedUserId) return
+
+ try {
+ // ✅ Perbaiki URL: gunakan query string bukan dynamic route
+ const res = await fetch(`/api/admin/user-menu-access?userId=${selectedUserId}`)
+ const data = await res.json()
+
+ if (data.success) {
+ setUserMenus(data.menuIds || [])
+ }
+ } catch (error) {
+ console.error('Gagal memuat menu akses:', error)
+ }
+ }
+
+ useEffect(() => {
+ if (selectedUserId) {
+ loadUserMenuAccess()
+ }
+ }, [selectedUserId])
+
+ const handleToggleMenu = (menuId: string) => {
+ setUserMenus(prev =>
+ prev.includes(menuId)
+ ? prev.filter(id => id !== menuId)
+ : [...prev, menuId]
+ )
+ }
+
+ const handleSave = async () => {
+ if (!selectedUserId) return
+
+ try {
+ const res = await fetch('/api/admin/user-menu-access', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ userId: selectedUserId, menuIds: userMenus }),
+ })
+
+ const data = await res.json()
+ if (data.success) {
+ alert('Menu akses berhasil disimpan')
+ }
+ } catch (error) {
+ console.error('Gagal menyimpan menu akses:', error)
+ alert('Terjadi kesalahan')
+ }
+ }
+
+ return (
+
+ Tampilan Menu
+
+
+
+ Pilih User:
+ ({
+ value: u.id,
+ label: `${u.username} (${u.nomor})`,
+ }))}
+ value={selectedUserId}
+ onChange={setSelectedUserId}
+ w={300}
+ />
+
+
+ {selectedUserId && (
+ <>
+ Menu yang Bisa Diakses:
+
+ {availableMenus.map(menu => (
+ handleToggleMenu(menu.value)}
+ />
+ ))}
+
+
+
+ Simpan Perubahan
+
+ >
+ )}
+
+
+
+ )
+}
+
+export default MenuAccessPage
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/user&role/role/[id]/page.tsx b/src/app/admin/(dashboard)/user&role/role/[id]/page.tsx
index f4e59441..9aacd720 100644
--- a/src/app/admin/(dashboard)/user&role/role/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/user&role/role/[id]/page.tsx
@@ -1,7 +1,8 @@
+/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
-import { Box, Button, Loader, Group, MultiSelect, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
+import { Box, Button, Group, Loader, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useCallback, useEffect, useState } from 'react';
@@ -9,6 +10,7 @@ import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import user from '../../../_state/user/user-state';
+
function EditRole() {
const stateRole = useProxy(user.roleState);
const router = useRouter();
@@ -17,46 +19,37 @@ function EditRole() {
// Controlled local state
const [formData, setFormData] = useState({
name: '',
- permissions: [] as string[],
});
const [originalData, setOriginalData] = useState({
name: '',
- permissions: [] as string[],
});
const [isSubmitting, setIsSubmitting] = useState(false);
// Load role data
const loadRole = useCallback(async (id: string) => {
- try {
- const data = await stateRole.update.load(id);
- if (data) {
- setFormData({
- name: data.name || '',
- permissions: data.permissions || [],
- });
- setOriginalData({
- name: data.name || '',
- permissions: data.permissions || [],
- });
- }
- } catch (error) {
- console.error('Error loading role:', error);
- toast.error(error instanceof Error ? error.message : 'Gagal mengambil data role');
+ const data = await stateRole.update.load(id);
+
+ if (data) {
+ setFormData({
+ name: data.name ?? '',
+ });
+
+ setOriginalData({
+ name: data.name ?? '',
+ });
}
- }, [stateRole.update]);
+ }, []);
useEffect(() => {
- stateRole.findMany.load(); // Load permissions/options
- const id = params?.id as string;
- if (id) loadRole(id);
- }, [params?.id, loadRole, stateRole.findMany]);
+ stateRole.findMany.load(); // load permission
+ if (params?.id) loadRole(params.id as string);
+ }, [params?.id]);
const handleResetForm = () => {
setFormData({
name: originalData.name,
- permissions: originalData.permissions,
});
toast.info("Form dikembalikan ke data awal");
};
@@ -66,10 +59,6 @@ function EditRole() {
toast.error('Nama role tidak boleh kosong');
return;
}
- if (!formData.permissions.length) {
- toast.error('Pilih minimal satu permission');
- return;
- }
try {
setIsSubmitting(true);
@@ -77,7 +66,6 @@ function EditRole() {
stateRole.update.form = {
...stateRole.update.form,
name: formData.name,
- permissions: formData.permissions,
};
await stateRole.update.update();
toast.success('Role berhasil diperbarui!');
@@ -116,24 +104,7 @@ function EditRole() {
label={Nama Role }
placeholder="Masukkan nama role"
/>
- setFormData({ ...formData, permissions: val })}
- label={Permission }
- placeholder="Pilih permission"
- data={
- stateRole.findMany.data?.map((v) => ({
- value: v.id,
- label: v.name,
- })) || []
- }
- clearable
- searchable
- required
- error={!formData.permissions.length ? 'Pilih minimal satu permission' : undefined}
- />
-
-
+
{/* Tombol Batal */}
{
stateRole.create.form = {
- name: '',
- permissions: [],
+ name: ''
};
};
@@ -80,28 +78,6 @@ export default function CreateRole() {
required
/>
-
- item.permissions)
- .flat()
- )
- )
- .filter((p): p is string => typeof p === 'string')
- .map((p) => ({ label: p, value: p }))
- }
- value={stateRole.create.form.permissions}
- onChange={(value) => (stateRole.create.form.permissions = value)}
- required
- />
-
-
-
(null)
+ const stateUser = useProxy(user.userState);
+ const stateRole = useProxy(user.roleState);
+ const [modalHapus, setModalHapus] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
const {
data,
@@ -41,20 +42,103 @@ function ListUser({ search }: { search: string }) {
const handleDelete = () => {
if (selectedId) {
- stateUser.delete.submit(selectedId)
- setModalHapus(false)
- setSelectedId(null)
-
- stateUser.findMany.load()
+ stateUser.deleteUser.delete(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ stateUser.findMany.load();
}
- }
-
+ };
useShallowEffect(() => {
- load(page, 10, search)
- }, [page, search])
+ stateRole.findMany.load();
+ load(page, 10, search);
+ }, [page, search]);
- const filteredData = data || []
+ // ✅ Helper function untuk nama role
+ const getRoleName = (roleId: string) => {
+ // Cari dari data role yang sudah diload
+ const role = stateRole.findMany.data.find((r) => r.id === roleId);
+ return role?.name || "Unknown Role";
+ };
+
+ // ✅ Handler untuk perubahan role dengan konfirmasi
+ const handleRoleChange = async (
+ userId: string,
+ username: string,
+ oldRoleId: string,
+ newRoleId: string
+ ) => {
+ // Skip jika sama
+ if (oldRoleId === newRoleId) {
+ return true;
+ }
+
+ // ✅ Konfirmasi perubahan role
+ const confirmed = window.confirm(
+ `⚠️ PERINGATAN\n\n` +
+ `Mengubah role untuk "${username}" akan:\n` +
+ `• Logout user otomatis dari semua device\n` +
+ `• Mengubah akses menu sesuai role baru\n\n` +
+ `Role: ${getRoleName(oldRoleId)} → ${getRoleName(newRoleId)}\n\n` +
+ `Lanjutkan?`
+ );
+
+ if (!confirmed) {
+ // Reload data untuk reset dropdown ke nilai lama
+ stateUser.findMany.load(page, 10, search);
+ return false;
+ }
+
+ // ✅ Submit update
+ const success = await stateUser.update.submit({
+ id: userId,
+ roleId: newRoleId,
+ });
+
+ if (success) {
+ // ✅ Logout user jika sedang mengedit diri sendiri
+ const currentUserId = authStore.user?.id;
+ if (currentUserId === userId) {
+ authStore.setUser(null);
+ document.cookie = `${process.env.BASE_SESSION_KEY}=; Max-Age=0; path=/;`;
+ alert("Perubahan memerlukan login ulang");
+ window.location.href = "/login";
+ return;
+ }
+
+ // Reload data
+ stateUser.findMany.load(page, 10, search);
+ }
+
+ return success;
+ };
+
+ // ✅ Handler untuk toggle isActive
+ const handleToggleActive = async (userId: string, currentStatus: boolean) => {
+ const success = await stateUser.update.submit({
+ id: userId,
+ isActive: !currentStatus,
+ });
+
+ if (success) {
+ // ✅ Logout user jika sedang mengedit diri sendiri
+ const currentUserId = authStore.user?.id;
+ if (currentUserId === userId) {
+ authStore.setUser(null);
+ document.cookie = `${process.env.BASE_SESSION_KEY}=; Max-Age=0; path=/;`;
+ alert("Perubahan memerlukan login ulang");
+ window.location.href = "/login";
+ return;
+ }
+
+ // Reload data
+ stateUser.findMany.load(page, 10, search);
+ }
+ };
+
+ const filteredData = (data || []).filter(
+ (item) => item.roleId !== "0" // asumsikan id role SUPERADMIN = "0"
+ );
if (loading || !data) {
return (
@@ -78,25 +162,51 @@ function ListUser({ search }: { search: string }) {
Nomor
Role
Aktif / Nonaktif
+ Hapus
{filteredData.length > 0 ? (
filteredData.map((item) => (
-
- {item.username}
+
+
+ {item.username}
+
-
+
{item.nomor}
-
-
- {item.role.name}
-
+
+ r.id !== "0") // ❌ Sembunyikan SUPERADMIN
+ .map(r => ({
+ label: r.name,
+ value: r.id,
+ }))}
+ value={item.roleId}
+ onChange={(val) => {
+ if (!val) return;
+
+ // ✅ Panggil handleRoleChange dengan konfirmasi
+ handleRoleChange(
+ item.id,
+ item.username,
+ item.roleId,
+ val
+ );
+ }}
+ searchable
+ clearable={false}
+ nothingFoundMessage="Role tidak ditemukan"
+ disabled={stateUser.update.loading}
+ />
+
{
- await stateUser.updateActive.submit(item.id, !item.isActive)
- stateUser.findMany.load(page, 10, search)
- }}
+ onClick={() => handleToggleActive(item.id, item.isActive)}
+ disabled={stateUser.update.loading}
>
{item.isActive ? : }
+
+
+ {
+ setSelectedId(item.id);
+ setModalHapus(true);
+ }}
+ >
+
+
+
))
) : (
-
+
- Tidak ada data user yang cocok
+ Tidak ada data user yang cocok
@@ -129,6 +251,7 @@ function ListUser({ search }: { search: string }) {
+
+
{/* Modal Konfirmasi Hapus */}
{
- const response = await fetch("/api/auth/login", {
- method: "POST",
- body: JSON.stringify({ nomor: nomor }),
- headers: {
- "Content-Type": "application/json",
- },
- });
-
- return await response.json().catch(() => null);
-};
-
-const apiFetchRegister = async ({
- nomor,
- username,
-}: {
- nomor: string;
- username: string;
-}) => {
- const data = {
- username: username,
- nomor: nomor,
- };
- const respone = await fetch("/api/auth/register", {
- method: "POST",
- body: JSON.stringify({ data }),
- headers: {
- "Content-Type": "application/json",
- },
- });
-
- const result = await respone.json();
-
- return result;
- // return await respone.json().catch(() => null);
-};
diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx
index e2396825..9dc3b944 100644
--- a/src/app/admin/layout.tsx
+++ b/src/app/admin/layout.tsx
@@ -1,6 +1,7 @@
'use client'
import colors from "@/con/colors";
+import { authStore } from "@/store/authStore";
import {
ActionIcon,
AppShell,
@@ -8,9 +9,11 @@ import {
AppShellMain,
AppShellNavbar,
Burger,
+ Center,
Flex,
Group,
Image,
+ Loader,
NavLink,
ScrollArea,
Text,
@@ -26,14 +29,120 @@ import {
import _ from "lodash";
import Link from "next/link";
import { useRouter, useSelectedLayoutSegments } from "next/navigation";
-import { navBar } from "./_com/list_PageAdmin";
+import { useEffect, useState } from "react";
+// import { useSnapshot } from "valtio";
+import { getNavbar } from "./(dashboard)/user&role/_com/dynamicNavbar";
export default function Layout({ children }: { children: React.ReactNode }) {
const [opened, { toggle }] = useDisclosure();
+ const [loading, setLoading] = useState(true);
+ const [isLoggingOut, setIsLoggingOut] = useState(false);
const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
const router = useRouter();
const segments = useSelectedLayoutSegments().map((s) => _.lowerCase(s));
+ // const { user } = useSnapshot(authStore);
+
+ // console.log("Current user in store:", user);
+
+ // ✅ FIX: Selalu fetch user data setiap kali komponen mount
+ useEffect(() => {
+ const fetchUser = async () => {
+ try {
+ const res = await fetch('/api/auth/me');
+ const data = await res.json();
+
+ if (data.user) {
+ // Check if user is active
+ if (!data.user.isActive) {
+ authStore.setUser(null);
+ router.replace('/waiting-room');
+ return;
+ }
+
+ // ✅ PENTING: Selalu fetch menuIds terbaru setiap login
+ const menuRes = await fetch(`/api/admin/user-menu-access?userId=${data.user.id}`);
+ const menuData = await menuRes.json();
+
+ const menuIds = menuData.success && Array.isArray(menuData.menuIds)
+ ? [...menuData.menuIds]
+ : null;
+
+ // ✅ Set user dengan menuIds yang fresh dari database
+ authStore.setUser({
+ id: data.user.id,
+ name: data.user.name,
+ roleId: Number(data.user.roleId),
+ menuIds, // menuIds terbaru
+ isActive: data.user.isActive
+ });
+ } else {
+ authStore.setUser(null);
+ router.replace('/login');
+ }
+ } catch (error) {
+ console.error('Gagal memuat data pengguna:', error);
+ authStore.setUser(null);
+ router.replace('/login');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchUser();
+ }, [router]); // ✅ Hapus dependency pada authStore.user
+
+ if (loading) {
+ return (
+
+
+
+
+
+
+
+ );
+ }
+
+ // ✅ Ambil menu berdasarkan roleId dan menuIds
+ const currentNav = authStore.user
+ ? getNavbar({ roleId: authStore.user.roleId, menuIds: authStore.user.menuIds })
+ : [];
+
+ const handleLogout = async () => {
+ try {
+ setIsLoggingOut(true);
+
+ // ✅ Panggil API logout untuk clear session di server
+ const response = await fetch('/api/auth/logout', { method: 'POST' });
+ const result = await response.json();
+
+ if (result.success) {
+ // Clear user data dari store
+ authStore.setUser(null);
+
+ // Clear localStorage
+ localStorage.removeItem('auth_nomor');
+ localStorage.removeItem('auth_kodeId');
+
+ // Force reload untuk reset semua state
+ window.location.href = '/login';
+ } else {
+ console.error('Logout failed:', result.message);
+ // Tetap redirect meskipun gagal
+ authStore.setUser(null);
+ window.location.href = '/login';
+ }
+ } catch (error) {
+ console.error('Error during logout:', error);
+ // Tetap clear store dan redirect jika error
+ authStore.setUser(null);
+ window.location.href = '/login';
+ } finally {
+ setIsLoggingOut(false);
+ }
+ };
+
return (
-
{
- router.push("/darmasaba");
- }}
+ onClick={handleLogout}
color={colors["blue-button"]}
radius="xl"
size="lg"
variant="gradient"
gradient={{ from: colors["blue-button"], to: "#228be6" }}
+ loading={isLoggingOut}
+ disabled={isLoggingOut}
>
@@ -156,7 +265,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
p={{ base: 'xs', sm: 'sm' }}
>
- {navBar.map((v, k) => {
+ {currentNav.map((v, k) => {
const isParentActive = segments.includes(_.lowerCase(v.name));
return (
@@ -254,4 +363,4 @@ export default function Layout({ children }: { children: React.ReactNode }) {
);
-}
+}
\ No newline at end of file
diff --git a/src/app/api/[[...slugs]]/_lib/auth/login/route.ts b/src/app/api/[[...slugs]]/_lib/auth/login/route.ts
index dc002998..e7aca89a 100644
--- a/src/app/api/[[...slugs]]/_lib/auth/login/route.ts
+++ b/src/app/api/[[...slugs]]/_lib/auth/login/route.ts
@@ -1,5 +1,4 @@
import prisma from "@/lib/prisma";
-
import { NextResponse } from "next/server";
import { randomOTP } from "../_lib/randomOTP";
@@ -12,52 +11,70 @@ export async function POST(req: Request) {
}
try {
- const codeOtp = randomOTP();
const body = await req.json();
const { nomor } = body;
- const res = await fetch(
- `https://wa.wibudev.com/code?nom=${nomor}&text=Website Desa Darmasaba - Kode ini bersifat RAHASIA dan JANGAN DI BAGIKAN KEPADA SIAPAPUN, termasuk anggota ataupun Admin lainnya.
- \n
- >> Kode OTP anda: ${codeOtp}.
- `
- );
- const sendWa = await res.json();
-
- if (sendWa.status !== "success")
+ if (!nomor || typeof nomor !== "string") {
return NextResponse.json(
- { success: false, message: "Nomor Whatsapp Tidak Aktif" },
+ { success: false, message: "Nomor tidak valid" },
{ status: 400 }
);
+ }
- const createOtpId = await prisma.kodeOtp.create({
- data: {
- nomor: nomor,
- otp: codeOtp,
- },
+ // Cek apakah user sudah terdaftar
+ const existingUser = await prisma.user.findUnique({
+ where: { nomor },
+ select: { id: true }, // cukup cek ada/tidak
});
- if (!createOtpId)
+ const isRegistered = !!existingUser;
+
+ // Generate OTP
+ const codeOtp = randomOTP(); // Pastikan ini menghasilkan number (sesuai tipe di KodeOtp.otp: Int)
+
+ // Kirim OTP via WA
+ const waRes = await fetch(
+ `https://wa.wibudev.com/code?nom=${encodeURIComponent(nomor)}&text=Website Desa Darmasaba - Kode ini bersifat RAHASIA dan JANGAN DI BAGIKAN KEPADA SIAPAPUN, termasuk anggota ataupun Admin lainnya.%0A%0A>> Kode OTP anda: ${codeOtp}.`
+ );
+
+ const sendWa = await waRes.json();
+
+ if (sendWa.status !== "success") {
return NextResponse.json(
- { success: false, message: "Gagal mengirim kode OTP" },
+ { success: false, message: "Nomor WhatsApp tidak aktif" },
{ status: 400 }
);
+ }
+
+ // Simpan OTP ke database
+ const otpRecord = await prisma.kodeOtp.create({
+ data: {
+ nomor: nomor,
+ otp: codeOtp, // Pastikan tipe ini number (Int di Prisma = number di TS)
+ },
+ });
return NextResponse.json(
{
success: true,
message: "Kode verifikasi terkirim",
- kodeId: createOtpId.id,
+ kodeId: otpRecord.id,
+ isRegistered, // 🔑 Ini kunci untuk frontend tahu harus ke register atau verifikasi
},
{ status: 200 }
);
} catch (error) {
- console.log("Error Login", error);
+ console.error("Error Login:", error);
return NextResponse.json(
- { success: false, message: "Terjadi masalah saat login" , reason: error as Error },
+ {
+ success: false,
+ message: "Terjadi masalah saat login",
+ // Hindari mengirim error mentah ke client di production
+ // reason: process.env.NODE_ENV === 'development' ? (error as Error).message : undefined,
+ },
{ status: 500 }
);
} finally {
await prisma.$disconnect();
}
-}
+}
\ No newline at end of file
diff --git a/src/app/api/[[...slugs]]/_lib/auth/me/route.ts b/src/app/api/[[...slugs]]/_lib/auth/me/route.ts
new file mode 100644
index 00000000..4ea5d2fd
--- /dev/null
+++ b/src/app/api/[[...slugs]]/_lib/auth/me/route.ts
@@ -0,0 +1,30 @@
+import prisma from "@/lib/prisma";
+import { NextRequest } from "next/server";
+// Jika pakai custom session (bukan next-auth), ganti dengan logic session-mu
+
+export async function GET(req: NextRequest) {
+ // 🔸 GANTI DENGAN LOGIC SESSION-MU
+ // Contoh: jika kamu simpan user.id di cookie atau JWT
+ const userId = req.cookies.get("hipmi_user_id")?.value; // sesuaikan
+
+ if (!userId) {
+ return Response.json({ error: "Unauthorized" }, { status: 401 });
+ }
+
+ const user = await prisma.user.findUnique({
+ where: { id: userId },
+ select: {
+ id: true,
+ username: true,
+ nomor: true,
+ isActive: true,
+ role: { select: { name: true } },
+ },
+ });
+
+ if (!user) {
+ return Response.json({ error: "User not found" }, { status: 404 });
+ }
+
+ return Response.json({ user });
+}
\ No newline at end of file
diff --git a/src/app/api/[[...slugs]]/_lib/auth/register/route.ts b/src/app/api/[[...slugs]]/_lib/auth/register/route.ts
new file mode 100644
index 00000000..cf2de7f0
--- /dev/null
+++ b/src/app/api/[[...slugs]]/_lib/auth/register/route.ts
@@ -0,0 +1,104 @@
+// import prisma from "@/lib/prisma";
+// import { NextResponse } from "next/server";
+
+// export async function POST(req: Request) {
+// if (req.method !== "POST") {
+// return NextResponse.json(
+// { success: false, message: "Method Not Allowed" },
+// { status: 405 }
+// );
+// }
+
+// try {
+// const { username, nomor, otp, kodeId } = await req.json();
+
+// // Validasi input
+// if (!username || !nomor || !otp || !kodeId) {
+// return NextResponse.json(
+// { success: false, message: "Data tidak lengkap" },
+// { status: 400 }
+// );
+// }
+
+// // 1. Verifikasi OTP
+// const otpRecord = await prisma.kodeOtp.findUnique({
+// where: { id: kodeId },
+// });
+
+// if (!otpRecord) {
+// return NextResponse.json(
+// { success: false, message: "Kode verifikasi tidak valid" },
+// { status: 400 }
+// );
+// }
+
+// if (!otpRecord.isActive) {
+// return NextResponse.json(
+// { success: false, message: "Kode verifikasi sudah digunakan atau kadaluarsa" },
+// { status: 400 }
+// );
+// }
+
+// if (otpRecord.otp !== otp) {
+// return NextResponse.json(
+// { success: false, message: "Kode OTP salah" },
+// { status: 400 }
+// );
+// }
+
+// if (otpRecord.nomor !== nomor) {
+// return NextResponse.json(
+// { success: false, message: "Nomor tidak sesuai dengan kode verifikasi" },
+// { status: 400 }
+// );
+// }
+
+// // 3. Cek apakah nomor sudah terdaftar
+// const existingUserByNomor = await prisma.user.findUnique({
+// where: { nomor },
+// });
+
+// if (existingUserByNomor) {
+// return NextResponse.json(
+// { success: false, message: "Nomor sudah terdaftar" },
+// { status: 409 }
+// );
+// }
+
+// // 4. Buat user
+// const newUser = await prisma.user.create({
+// data: {
+// username,
+// nomor,
+// // roleId default "1" (sesuai model)
+// },
+// });
+
+// // 5. Nonaktifkan OTP agar tidak bisa dipakai lagi
+// await prisma.kodeOtp.update({
+// where: { id: kodeId },
+// data: { isActive: false },
+// });
+
+// return NextResponse.json(
+// {
+// success: true,
+// message: "Registrasi berhasil",
+// userId: newUser.id,
+// },
+// { status: 201 }
+// );
+// } catch (error) {
+// console.error("Error registrasi:", error);
+// return NextResponse.json(
+// {
+// success: false,
+// message: "Terjadi kesalahan saat registrasi",
+// // reason: process.env.NODE_ENV === 'development' ? (error as Error).message : undefined,
+// },
+// { status: 500 }
+// );
+// } finally {
+// await prisma.$disconnect();
+// }
+// }
\ No newline at end of file
diff --git a/src/app/api/[[...slugs]]/_lib/search/findMany.ts b/src/app/api/[[...slugs]]/_lib/search/findMany.ts
index 2a782fd6..5e6eab4e 100644
--- a/src/app/api/[[...slugs]]/_lib/search/findMany.ts
+++ b/src/app/api/[[...slugs]]/_lib/search/findMany.ts
@@ -19,7 +19,9 @@ export default async function searchFindMany(context: Context) {
//========================================= PROFILE ========================================= //
if (type === "pejabatdesa") {
const data = await prisma.pejabatDesa.findMany({
- where: { name: { contains: query, mode: "insensitive" } },
+ where: {
+ name: { contains: query, mode: "insensitive" },
+ },
skip,
take: limitNum,
});
@@ -31,7 +33,12 @@ export default async function searchFindMany(context: Context) {
if (type === "programinovasi") {
const data = await prisma.programInovasi.findMany({
- where: { name: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { description: { contains: query, mode: "insensitive" } },
+ ],
+ },
skip,
take: limitNum,
});
@@ -51,7 +58,13 @@ export default async function searchFindMany(context: Context) {
if (type === "desaantikorupsi") {
const data = await prisma.desaAntiKorupsi.findMany({
- where: { name: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ { kategori: { name: { contains: query, mode: "insensitive" } } },
+ ],
+ },
skip,
take: limitNum,
});
@@ -71,7 +84,23 @@ export default async function searchFindMany(context: Context) {
//========================================= APBDes ========================================= //
if (type === "apbdes") {
const data = await prisma.aPBDes.findMany({
- where: { name: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ ],
+ },
+ skip,
+ take: limitNum,
+ });
+ return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
+ }
+
+ if (type === "apbdesitem") {
+ const data = await prisma.aPBDesItem.findMany({
+ where: {
+ uraian: { contains: query, mode: "insensitive" },
+ },
skip,
take: limitNum,
});
@@ -81,7 +110,13 @@ export default async function searchFindMany(context: Context) {
//========================================= PRESTASI DESA ========================================= //
if (type === "prestasidesa") {
const data = await prisma.prestasiDesa.findMany({
- where: { name: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ { kategori: { name: { contains: query, mode: "insensitive" } } },
+ ],
+ },
skip,
take: limitNum,
});
@@ -103,7 +138,18 @@ export default async function searchFindMany(context: Context) {
if (type === "strukturppid") {
const data = await prisma.strukturPPID.findMany({
where: {
- PegawaiPPID: { namaLengkap: { contains: query, mode: "insensitive" } },
+ OR: [
+ {
+ PegawaiPPID: {
+ namaLengkap: { contains: query, mode: "insensitive" },
+ },
+ },
+ {
+ PosisiOrganisasiPPID: {
+ nama: { contains: query, mode: "insensitive" },
+ },
+ },
+ ],
},
include: {
PosisiOrganisasiPPID: true,
@@ -119,8 +165,10 @@ export default async function searchFindMany(context: Context) {
if (type === "visimisippid") {
const data = await prisma.visiMisiPPID.findMany({
where: {
- visi: { contains: query, mode: "insensitive" },
- misi: { contains: query, mode: "insensitive" },
+ OR: [
+ { visi: { contains: query, mode: "insensitive" } },
+ { misi: { contains: query, mode: "insensitive" } },
+ ],
},
skip,
take: limitNum,
@@ -133,7 +181,12 @@ export default async function searchFindMany(context: Context) {
// ========================================= DASAR HUKUM PPID ========================================= //
if (type === "dasarhukumppid") {
const data = await prisma.dasarHukumPPID.findMany({
- where: { judul: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { judul: { contains: query, mode: "insensitive" } },
+ { content: { contains: query, mode: "insensitive" } },
+ ],
+ },
skip,
take: limitNum,
});
@@ -143,7 +196,15 @@ export default async function searchFindMany(context: Context) {
// ========================================= PROFILE PPID ========================================= //
if (type === "profileppid") {
const data = await prisma.profilePPID.findMany({
- where: { name: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { biodata: { contains: query, mode: "insensitive" } },
+ { riwayat: { contains: query, mode: "insensitive" } },
+ { pengalaman: { contains: query, mode: "insensitive" } },
+ { unggulan: { contains: query, mode: "insensitive" } },
+ ],
+ },
skip,
take: limitNum,
});
@@ -153,7 +214,12 @@ export default async function searchFindMany(context: Context) {
// ========================================= DAFTAR INFORMASI PUBLIK ========================================= //
if (type === "daftarinformasipublik") {
const data = await prisma.daftarInformasiPublik.findMany({
- where: { jenisInformasi: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { jenisInformasi: { contains: query, mode: "insensitive" } },
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ ],
+ },
skip,
take: limitNum,
});
@@ -163,7 +229,14 @@ export default async function searchFindMany(context: Context) {
//=========================================PERMOHONAN INFORMASI PUBLIK========================= //
if (type === "permohonaninformasipublik") {
const data = await prisma.permohonanInformasiPublik.findMany({
- where: { name: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { nik: { contains: query, mode: "insensitive" } },
+ { alamat: { contains: query, mode: "insensitive" } },
+ { email: { contains: query, mode: "insensitive" } },
+ ],
+ },
skip,
take: limitNum,
});
@@ -173,7 +246,13 @@ export default async function searchFindMany(context: Context) {
//=========================================PERMOHONAN INFORMASI KEBERATAN PUBLIK========================= //
if (type === "permohonaninformasikeberatanpublik") {
const data = await prisma.formulirPermohonanKeberatan.findMany({
- where: { name: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { email: { contains: query, mode: "insensitive" } },
+ { alasan: { contains: query, mode: "insensitive" } },
+ ],
+ },
skip,
take: limitNum,
});
@@ -194,7 +273,12 @@ export default async function searchFindMany(context: Context) {
// ========================================= PROFILE DESA ========================================= //
if (type === "sejarahdesa") {
const data = await prisma.sejarahDesa.findMany({
- where: { judul: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { judul: { contains: query, mode: "insensitive" } },
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ ],
+ },
skip,
take: limitNum,
});
@@ -203,7 +287,12 @@ export default async function searchFindMany(context: Context) {
if (type === "visimisidesa") {
const data = await prisma.visiMisiDesa.findMany({
- where: { visi: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { visi: { contains: query, mode: "insensitive" } },
+ { misi: { contains: query, mode: "insensitive" } },
+ ],
+ },
skip,
take: limitNum,
});
@@ -212,7 +301,12 @@ export default async function searchFindMany(context: Context) {
if (type === "lambangdesa") {
const data = await prisma.lambangDesa.findMany({
- where: { judul: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { judul: { contains: query, mode: "insensitive" } },
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ ],
+ },
skip,
take: limitNum,
});
@@ -221,7 +315,12 @@ export default async function searchFindMany(context: Context) {
if (type === "maskotdesa") {
const data = await prisma.maskotDesa.findMany({
- where: { judul: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { judul: { contains: query, mode: "insensitive" } },
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ ],
+ },
skip,
take: limitNum,
});
@@ -230,7 +329,14 @@ export default async function searchFindMany(context: Context) {
if (type === "profilperbekel") {
const data = await prisma.profilPerbekel.findMany({
- where: { biodata: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { biodata: { contains: query, mode: "insensitive" } },
+ { pengalaman: { contains: query, mode: "insensitive" } },
+ { pengalamanOrganisasi: { contains: query, mode: "insensitive" } },
+ { programUnggulan: { contains: query, mode: "insensitive" } },
+ ],
+ },
skip,
take: limitNum,
});
@@ -239,7 +345,13 @@ export default async function searchFindMany(context: Context) {
if (type === "perbekeldarmasaba") {
const data = await prisma.perbekelDariMasaKeMasa.findMany({
- where: { nama: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { nama: { contains: query, mode: "insensitive" } },
+ { periode: { contains: query, mode: "insensitive" } },
+ { daerah: { contains: query, mode: "insensitive" } },
+ ],
+ },
skip,
take: limitNum,
});
@@ -249,7 +361,16 @@ export default async function searchFindMany(context: Context) {
// ========================================= BERITA ========================================= //
if (type === "berita") {
const data = await prisma.berita.findMany({
- where: { judul: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { judul: { contains: query, mode: "insensitive" } },
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ { content: { contains: query, mode: "insensitive" } },
+ {
+ kategoriBerita: { name: { contains: query, mode: "insensitive" } },
+ },
+ ],
+ },
skip,
take: limitNum,
});
@@ -268,7 +389,14 @@ export default async function searchFindMany(context: Context) {
// ========================================= POTENSI DESA ========================================= //
if (type === "potensi") {
const data = await prisma.potensiDesa.findMany({
- where: { name: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ { content: { contains: query, mode: "insensitive" } },
+ { kategori: { nama: { contains: query, mode: "insensitive" } } },
+ ],
+ },
skip,
take: limitNum,
});
@@ -278,7 +406,18 @@ export default async function searchFindMany(context: Context) {
// ========================================= PENGUMUMAN ========================================= //
if (type === "pengumuman") {
const data = await prisma.pengumuman.findMany({
- where: { judul: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { judul: { contains: query, mode: "insensitive" } },
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ { content: { contains: query, mode: "insensitive" } },
+ {
+ CategoryPengumuman: {
+ name: { contains: query, mode: "insensitive" },
+ },
+ },
+ ],
+ },
skip,
take: limitNum,
});
@@ -288,7 +427,12 @@ export default async function searchFindMany(context: Context) {
// ========================================= GALLERY ========================================= //
if (type === "galleryFoto") {
const data = await prisma.galleryFoto.findMany({
- where: { name: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ ],
+ },
skip,
take: limitNum,
});
@@ -297,7 +441,12 @@ export default async function searchFindMany(context: Context) {
if (type === "galleryVideo") {
const data = await prisma.galleryVideo.findMany({
- where: { name: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ ],
+ },
skip,
take: limitNum,
});
@@ -307,7 +456,12 @@ export default async function searchFindMany(context: Context) {
// ========================================= LAYANAN DESA ========================================= //
if (type === "pelayananSuratKeterangan") {
const data = await prisma.pelayananSuratKeterangan.findMany({
- where: { name: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ ],
+ },
skip,
take: limitNum,
});
@@ -316,7 +470,12 @@ export default async function searchFindMany(context: Context) {
if (type === "pelayananPerizinanBerusaha") {
const data = await prisma.pelayananPerizinanBerusaha.findMany({
- where: { name: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ ],
+ },
skip,
take: limitNum,
});
@@ -325,7 +484,12 @@ export default async function searchFindMany(context: Context) {
if (type === "pelayananTelunjukSaktiDesa") {
const data = await prisma.pelayananTelunjukSaktiDesa.findMany({
- where: { name: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ ],
+ },
skip,
take: limitNum,
});
@@ -334,7 +498,12 @@ export default async function searchFindMany(context: Context) {
if (type === "pelayananPendudukNonPermanen") {
const data = await prisma.pelayananPendudukNonPermanen.findMany({
- where: { name: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ ],
+ },
skip,
take: limitNum,
});
@@ -344,7 +513,13 @@ export default async function searchFindMany(context: Context) {
// ========================================= PENGHARGAAN ========================================= //
if (type === "penghargaan") {
const data = await prisma.penghargaan.findMany({
- where: { name: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { juara: { contains: query, mode: "insensitive" } },
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ ],
+ },
skip,
take: limitNum,
});
@@ -355,7 +530,14 @@ export default async function searchFindMany(context: Context) {
// ========================================= POSYANDU ========================================= //
if (type === "posyandu") {
const data = await prisma.posyandu.findMany({
- where: { name: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { nomor: { contains: query, mode: "insensitive" } },
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ { jadwalPelayanan: { contains: query, mode: "insensitive" } },
+ ],
+ },
skip,
take: limitNum,
});
@@ -367,10 +549,20 @@ export default async function searchFindMany(context: Context) {
const data = await prisma.fasilitasKesehatan.findMany({
where: {
name: { contains: query, mode: "insensitive" },
- informasiumum: { fasilitas: { contains: query, mode: "insensitive" } },
+ informasiumum: {
+ OR: [
+ { fasilitas: { contains: query, mode: "insensitive" } },
+ { alamat: { contains: query, mode: "insensitive" } },
+ { jamOperasional: { contains: query, mode: "insensitive" } },
+ ],
+ },
layananunggulan: { content: { contains: query, mode: "insensitive" } },
dokterdantenagamedis: {
- name: { contains: query, mode: "insensitive" },
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { specialist: { contains: query, mode: "insensitive" } },
+ { jadwal: { contains: query, mode: "insensitive" } },
+ ],
},
fasilitaspendukung: {
content: { contains: query, mode: "insensitive" },
@@ -378,7 +570,12 @@ export default async function searchFindMany(context: Context) {
prosedurpendaftaran: {
content: { contains: query, mode: "insensitive" },
},
- tarifdanlayanan: { layanan: { contains: query, mode: "insensitive" } },
+ tarifdanlayanan: {
+ OR: [
+ { layanan: { contains: query, mode: "insensitive" } },
+ { tarif: { contains: query, mode: "insensitive" } },
+ ],
+ },
},
skip,
take: limitNum,
@@ -392,7 +589,12 @@ export default async function searchFindMany(context: Context) {
where: {
content: { contains: query, mode: "insensitive" },
informasijadwalkegiatan: {
- name: { contains: query, mode: "insensitive" },
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { tanggal: { contains: query, mode: "insensitive" } },
+ { waktu: { contains: query, mode: "insensitive" } },
+ { lokasi: { contains: query, mode: "insensitive" } },
+ ],
},
deskripsijadwalkegiatan: {
deskripsi: { contains: query, mode: "insensitive" },
@@ -407,7 +609,14 @@ export default async function searchFindMany(context: Context) {
content: { contains: query, mode: "insensitive" },
},
pendaftaranjadwalkegiatan: {
- name: { contains: query, mode: "insensitive" },
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { tanggal: { contains: query, mode: "insensitive" } },
+ { namaOrangtua: { contains: query, mode: "insensitive" } },
+ { nomor: { contains: query, mode: "insensitive" } },
+ { alamat: { contains: query, mode: "insensitive" } },
+ { catatan: { contains: query, mode: "insensitive" } },
+ ],
},
},
skip,
@@ -426,16 +635,29 @@ export default async function searchFindMany(context: Context) {
content: { contains: query, mode: "insensitive" },
},
symptom: {
- title: { contains: query, mode: "insensitive" },
+ OR: [
+ { title: { contains: query, mode: "insensitive" } },
+ { content: { contains: query, mode: "insensitive" } },
+ ],
},
prevention: {
- title: { contains: query, mode: "insensitive" },
+ OR: [
+ { title: { contains: query, mode: "insensitive" } },
+ { content: { contains: query, mode: "insensitive" } },
+ ],
},
firstaid: {
- title: { contains: query, mode: "insensitive" },
+ OR: [
+ { title: { contains: query, mode: "insensitive" } },
+ { content: { contains: query, mode: "insensitive" } },
+ ],
},
mythvsfact: {
- title: { contains: query, mode: "insensitive" },
+ OR: [
+ { title: { contains: query, mode: "insensitive" } },
+ { mitos: { contains: query, mode: "insensitive" } },
+ { fakta: { contains: query, mode: "insensitive" } },
+ ],
},
doctorsign: {
content: { contains: query, mode: "insensitive" },
@@ -450,7 +672,33 @@ export default async function searchFindMany(context: Context) {
// ========================================= PUSKESMAS ========================================= //
if (type === "puskesmas") {
const data = await prisma.puskesmas.findMany({
- where: { name: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { alamat: { contains: query, mode: "insensitive" } },
+ {
+ jam: {
+ OR: [
+ { workDays: { contains: query, mode: "insensitive" } },
+ { weekDays: { contains: query, mode: "insensitive" } },
+ { holiday: { contains: query, mode: "insensitive" } },
+ ],
+ },
+ },
+ {
+ kontak: {
+ OR: [
+ {
+ kontakPuskesmas: { contains: query, mode: "insensitive" },
+ email: { contains: query, mode: "insensitive" },
+ facebook: { contains: query, mode: "insensitive" },
+ kontakUGD: { contains: query, mode: "insensitive" },
+ },
+ ],
+ },
+ },
+ ],
+ },
skip,
take: limitNum,
});
@@ -460,7 +708,13 @@ export default async function searchFindMany(context: Context) {
// ========================================= PROGRAM KESEHATAN ========================================= //
if (type === "programKesehatan") {
const data = await prisma.programKesehatan.findMany({
- where: { name: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { deskripsiSingkat: { contains: query, mode: "insensitive" } },
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ ],
+ },
skip,
take: limitNum,
});
@@ -470,7 +724,12 @@ export default async function searchFindMany(context: Context) {
// ========================================= PENANGANAN DARURAT ========================================= //
if (type === "penangananDarurat") {
const data = await prisma.penangananDarurat.findMany({
- where: { name: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ ],
+ },
skip,
take: limitNum,
});
@@ -480,7 +739,12 @@ export default async function searchFindMany(context: Context) {
// ========================================= KONTAK DARURAT ========================================= //
if (type === "kontakDarurat") {
const data = await prisma.kontakDarurat.findMany({
- where: { name: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ ],
+ },
skip,
take: limitNum,
});
@@ -490,7 +754,13 @@ export default async function searchFindMany(context: Context) {
// ========================================= INFO WABAH PENYAKIT ========================================= //
if (type === "infoWabahPenyakit") {
const data = await prisma.infoWabahPenyakit.findMany({
- where: { name: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { deskripsiSingkat: { contains: query, mode: "insensitive" } },
+ { deskripsiLengkap: { contains: query, mode: "insensitive" } },
+ ],
+ },
skip,
take: limitNum,
});
@@ -501,7 +771,12 @@ export default async function searchFindMany(context: Context) {
// ========================================= KEAMANAN LINGKUNGAN ========================================= //
if (type === "keamananLingkungan") {
const data = await prisma.keamananLingkungan.findMany({
- where: { name: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ ],
+ },
skip,
take: limitNum,
});
@@ -511,7 +786,13 @@ export default async function searchFindMany(context: Context) {
// ========================================= POLSEK TERDEKAT ========================================= //
if (type === "polsekTerdekat") {
const data = await prisma.polsekTerdekat.findMany({
- where: { nama: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { nama: { contains: query, mode: "insensitive" } },
+ { alamat: { contains: query, mode: "insensitive" } },
+ { nomorTelepon: { contains: query, mode: "insensitive" } },
+ ],
+ },
skip,
take: limitNum,
});
@@ -521,7 +802,15 @@ export default async function searchFindMany(context: Context) {
// ========================================= KONTAK DARURAT ========================================= //
if (type === "kontakDaruratKeamanan") {
const data = await prisma.kontakDaruratKeamanan.findMany({
- where: { nama: { contains: query, mode: "insensitive" } },
+ where: {
+ nama: { contains: query, mode: "insensitive" },
+ kategori: {
+ OR: [
+ { nama: { contains: query, mode: "insensitive" } },
+ { nomorTelepon: { contains: query, mode: "insensitive" } },
+ ],
+ },
+ },
skip,
take: limitNum,
});
@@ -531,7 +820,13 @@ export default async function searchFindMany(context: Context) {
// ========================================= PENCEGAHAN KRIMINALITAS ========================================= //
if (type === "pencegahanKriminalitas") {
const data = await prisma.pencegahanKriminalitas.findMany({
- where: { judul: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { judul: { contains: query, mode: "insensitive" } },
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ { deskripsiSingkat: { contains: query, mode: "insensitive" } },
+ ],
+ },
skip,
take: limitNum,
});
@@ -541,7 +836,12 @@ export default async function searchFindMany(context: Context) {
// ========================================= LAPORAN PUBLIK ========================================= //
if (type === "laporanPublik") {
const data = await prisma.laporanPublik.findMany({
- where: { judul: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { judul: { contains: query, mode: "insensitive" } },
+ { lokasi: { contains: query, mode: "insensitive" } },
+ ],
+ },
skip,
take: limitNum,
});
@@ -551,7 +851,12 @@ export default async function searchFindMany(context: Context) {
// ========================================= TIPS KEAMANAN ========================================= //
if (type === "tipsKeamanan") {
const data = await prisma.menuTipsKeamanan.findMany({
- where: { judul: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { judul: { contains: query, mode: "insensitive" } },
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ ],
+ },
skip,
take: limitNum,
});
@@ -562,7 +867,16 @@ export default async function searchFindMany(context: Context) {
// ========================================= PASAR DESA ========================================= //
if (type === "pasarDesa") {
const data = await prisma.pasarDesa.findMany({
- where: { nama: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { nama: { contains: query, mode: "insensitive" } },
+ { alamatUsaha: { contains: query, mode: "insensitive" } },
+ { kontak: { contains: query, mode: "insensitive" } },
+ {
+ kategoriProduk: { nama: { contains: query, mode: "insensitive" } },
+ },
+ ],
+ },
skip,
take: limitNum,
});
@@ -572,7 +886,7 @@ export default async function searchFindMany(context: Context) {
// ========================================= LOWONGAN KERJA LOKAL ========================================= //
if (type === "lowonganKerjaLokal") {
const data = await prisma.lowonganPekerjaan.findMany({
- where: {
+ where: {
OR: [
{ posisi: { contains: query, mode: "insensitive" } },
{ namaPerusahaan: { contains: query, mode: "insensitive" } },
@@ -592,11 +906,25 @@ export default async function searchFindMany(context: Context) {
// ========================================= STRUKTUR ORGANISASI ========================================= //
if (type === "strukturOrganisasi") {
const data = await prisma.strukturBumDes.findMany({
- where: {
+ where: {
OR: [
{ name: { contains: query, mode: "insensitive" } },
- { PosisiOrganisasiBumDes: { nama: { contains: query, mode: "insensitive" } } },
- { PegawaiBumDes: { namaLengkap: { contains: query, mode: "insensitive" } } },
+ {
+ PosisiOrganisasiBumDes: {
+ nama: { contains: query, mode: "insensitive" },
+ },
+ },
+ {
+ PegawaiBumDes: {
+ OR: [
+ { namaLengkap: { contains: query, mode: "insensitive" } },
+ { email: { contains: query, mode: "insensitive" } },
+ { telepon: { contains: query, mode: "insensitive" } },
+ { alamat: { contains: query, mode: "insensitive" } },
+ { gelarAkademik: { contains: query, mode: "insensitive" } },
+ ],
+ },
+ },
],
},
skip,
@@ -604,11 +932,11 @@ export default async function searchFindMany(context: Context) {
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
-
+
// ========================================= JUMLAH PENDUDUK USIA KERJA YANG MENGANGGUR ========================================= //
if (type === "jumlahPendudukUsiaKerjaYangMenganggurUsia") {
const data = await prisma.grafikMenganggurBerdasarkanUsia.findMany({
- where: {
+ where: {
OR: [
{ usia18_25: { contains: query, mode: "insensitive" } },
{ usia26_35: { contains: query, mode: "insensitive" } },
@@ -625,7 +953,7 @@ export default async function searchFindMany(context: Context) {
// ========================================= JUMLAH PENDUDUK USIA KERJA YANG MENGANGGUR ========================================= //
if (type === "jumlahPendudukUsiaKerjaYangMenganggurPendidikan") {
const data = await prisma.grafikMenganggurBerdasarkanPendidikan.findMany({
- where: {
+ where: {
OR: [
{ SD: { contains: query, mode: "insensitive" } },
{ SMP: { contains: query, mode: "insensitive" } },
@@ -646,13 +974,13 @@ export default async function searchFindMany(context: Context) {
where: {
OR: [
// Convert year to string for partial matching
- ...(isNaN(Number(query)) ? [] : [
- { year: { equals: Number(query) } }
- ]),
+ ...(isNaN(Number(query))
+ ? []
+ : [{ year: { equals: Number(query) } }]),
// Convert totalPoorPopulation to string for partial matching
- ...(isNaN(Number(query)) ? [] : [
- { totalPoorPopulation: { equals: Number(query) } }
- ])
+ ...(isNaN(Number(query))
+ ? []
+ : [{ totalPoorPopulation: { equals: Number(query) } }]),
].filter(Boolean), // Remove any empty arrays from the spread
},
skip,
@@ -663,7 +991,7 @@ export default async function searchFindMany(context: Context) {
// ========================================= PROGRAM KEMISKINAN ========================================= //
if (type === "programKemiskinan") {
const data = await prisma.programKemiskinan.findMany({
- where: {
+ where: {
OR: [
{ nama: { contains: query, mode: "insensitive" } },
{ deskripsi: { contains: query, mode: "insensitive" } },
@@ -675,14 +1003,13 @@ export default async function searchFindMany(context: Context) {
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
-
// ========================================= SEKTOR UNGGULAN DESA ========================================= //
if (type === "sektorUnggulanDesa") {
const data = await prisma.sektorUnggulanDesa.findMany({
- where: {
+ where: {
OR: [
{ name: { contains: query, mode: "insensitive" } },
- { description: { contains: query, mode: "insensitive" } }
+ { description: { contains: query, mode: "insensitive" } },
],
},
skip,
@@ -702,14 +1029,14 @@ export default async function searchFindMany(context: Context) {
}
// ========================================= MENU INOVASI ========================================= //
-// ========================================= DESA DIGITAL / SMART VILLAGE ========================================= //
+ // ========================================= DESA DIGITAL / SMART VILLAGE ========================================= //
if (type === "desaDigital") {
const data = await prisma.desaDigital.findMany({
- where: {
+ where: {
OR: [
{ name: { contains: query, mode: "insensitive" } },
- { deskripsi: { contains: query, mode: "insensitive" } }
+ { deskripsi: { contains: query, mode: "insensitive" } },
],
},
skip,
@@ -722,11 +1049,11 @@ export default async function searchFindMany(context: Context) {
if (type === "programKreatif") {
const data = await prisma.programKreatif.findMany({
- where: {
+ where: {
OR: [
{ name: { contains: query, mode: "insensitive" } },
{ slug: { contains: query, mode: "insensitive" } },
- { deskripsi: { contains: query, mode: "insensitive" } }
+ { deskripsi: { contains: query, mode: "insensitive" } },
],
},
skip,
@@ -739,12 +1066,12 @@ export default async function searchFindMany(context: Context) {
if (type === "kolaborasiInovasi") {
const data = await prisma.kolaborasiInovasi.findMany({
- where: {
+ where: {
OR: [
{ name: { contains: query, mode: "insensitive" } },
{ slug: { contains: query, mode: "insensitive" } },
{ deskripsi: { contains: query, mode: "insensitive" } },
- { kolaborator: { contains: query, mode: "insensitive" } }
+ { kolaborator: { contains: query, mode: "insensitive" } },
],
},
skip,
@@ -755,10 +1082,8 @@ export default async function searchFindMany(context: Context) {
if (type === "mitraKolaborasi") {
const data = await prisma.mitraKolaborasi.findMany({
- where: {
- OR: [
- { name: { contains: query, mode: "insensitive" } }
- ],
+ where: {
+ OR: [{ name: { contains: query, mode: "insensitive" } }],
},
skip,
take: limitNum,
@@ -770,10 +1095,10 @@ export default async function searchFindMany(context: Context) {
if (type === "infoTekno") {
const data = await prisma.infoTekno.findMany({
- where: {
+ where: {
OR: [
{ name: { contains: query, mode: "insensitive" } },
- { deskripsi: { contains: query, mode: "insensitive" } }
+ { deskripsi: { contains: query, mode: "insensitive" } },
],
},
skip,
@@ -783,14 +1108,12 @@ export default async function searchFindMany(context: Context) {
}
// ========================================= LINGKUNGAN ========================================= //
-// ========================================= PENGELOLAAN SAMPAH ========================================= //
+ // ========================================= PENGELOLAAN SAMPAH ========================================= //
if (type === "pengelolaanSampah") {
const data = await prisma.pengelolaanSampah.findMany({
- where: {
- OR: [
- { name: { contains: query, mode: "insensitive" } }
- ],
+ where: {
+ OR: [{ name: { contains: query, mode: "insensitive" } }],
},
skip,
take: limitNum,
@@ -800,12 +1123,12 @@ export default async function searchFindMany(context: Context) {
if (type === "keteranganBankSampahTerdekat") {
const data = await prisma.keteranganBankSampahTerdekat.findMany({
- where: {
+ where: {
OR: [
{ name: { contains: query, mode: "insensitive" } },
{ alamat: { contains: query, mode: "insensitive" } },
{ namaTempatMaps: { contains: query, mode: "insensitive" } },
- { linkPetunjukArah: { contains: query, mode: "insensitive" } }
+ { linkPetunjukArah: { contains: query, mode: "insensitive" } },
],
},
skip,
@@ -818,11 +1141,11 @@ export default async function searchFindMany(context: Context) {
if (type === "programPenghijauan") {
const data = await prisma.programPenghijauan.findMany({
- where: {
+ where: {
OR: [
{ name: { contains: query, mode: "insensitive" } },
{ judul: { contains: query, mode: "insensitive" } },
- { deskripsi: { contains: query, mode: "insensitive" } }
+ { deskripsi: { contains: query, mode: "insensitive" } },
],
},
skip,
@@ -835,10 +1158,10 @@ export default async function searchFindMany(context: Context) {
if (type === "dataLingkunganDesa") {
const data = await prisma.dataLingkunganDesa.findMany({
- where: {
+ where: {
OR: [
{ name: { contains: query, mode: "insensitive" } },
- { deskripsi: { contains: query, mode: "insensitive" } }
+ { deskripsi: { contains: query, mode: "insensitive" } },
],
},
skip,
@@ -851,11 +1174,17 @@ export default async function searchFindMany(context: Context) {
if (type === "gotongRoyong") {
const data = await prisma.kegiatanDesa.findMany({
- where: {
+ where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
{ deskripsiSingkat: { contains: query, mode: "insensitive" } },
- { deskripsiLengkap: { contains: query, mode: "insensitive" } }
+ { deskripsiLengkap: { contains: query, mode: "insensitive" } },
+ { lokasi: { contains: query, mode: "insensitive" } },
+ {
+ kategoriKegiatan: {
+ nama: { contains: query, mode: "insensitive" },
+ },
+ },
],
},
skip,
@@ -868,10 +1197,10 @@ export default async function searchFindMany(context: Context) {
if (type === "tujuanEdukasiLingkungan") {
const data = await prisma.tujuanEdukasiLingkungan.findMany({
- where: {
+ where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
- { deskripsi: { contains: query, mode: "insensitive" } }
+ { deskripsi: { contains: query, mode: "insensitive" } },
],
},
skip,
@@ -882,10 +1211,10 @@ export default async function searchFindMany(context: Context) {
if (type === "materiEdukasiLingkungan") {
const data = await prisma.materiEdukasiLingkungan.findMany({
- where: {
+ where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
- { deskripsi: { contains: query, mode: "insensitive" } }
+ { deskripsi: { contains: query, mode: "insensitive" } },
],
},
skip,
@@ -896,10 +1225,10 @@ export default async function searchFindMany(context: Context) {
if (type === "contohEdukasiLingkungan") {
const data = await prisma.contohEdukasiLingkungan.findMany({
- where: {
+ where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
- { deskripsi: { contains: query, mode: "insensitive" } }
+ { deskripsi: { contains: query, mode: "insensitive" } },
],
},
skip,
@@ -912,10 +1241,10 @@ export default async function searchFindMany(context: Context) {
if (type === "filosofiTriHita") {
const data = await prisma.filosofiTriHita.findMany({
- where: {
+ where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
- { deskripsi: { contains: query, mode: "insensitive" } }
+ { deskripsi: { contains: query, mode: "insensitive" } },
],
},
skip,
@@ -926,10 +1255,10 @@ export default async function searchFindMany(context: Context) {
if (type === "bentukKonservasiBerdasarkanAdat") {
const data = await prisma.bentukKonservasiBerdasarkanAdat.findMany({
- where: {
+ where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
- { deskripsi: { contains: query, mode: "insensitive" } }
+ { deskripsi: { contains: query, mode: "insensitive" } },
],
},
skip,
@@ -940,10 +1269,10 @@ export default async function searchFindMany(context: Context) {
if (type === "nilaiKonservasiAdat") {
const data = await prisma.nilaiKonservasiAdat.findMany({
- where: {
+ where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
- { deskripsi: { contains: query, mode: "insensitive" } }
+ { deskripsi: { contains: query, mode: "insensitive" } },
],
},
skip,
@@ -953,14 +1282,12 @@ export default async function searchFindMany(context: Context) {
}
// ========================================= MENU PENDIDIKAN ========================================= //
-// ========================================= INFO SEKOLAH & PAUD ========================================= //
+ // ========================================= INFO SEKOLAH & PAUD ========================================= //
if (type === "jenjangPendidikan") {
const data = await prisma.jenjangPendidikan.findMany({
- where: {
- OR: [
- { nama: { contains: query, mode: "insensitive" } }
- ],
+ where: {
+ nama: { contains: query, mode: "insensitive" },
},
skip,
take: limitNum,
@@ -970,10 +1297,8 @@ export default async function searchFindMany(context: Context) {
if (type === "lembaga") {
const data = await prisma.lembaga.findMany({
- where: {
- OR: [
- { nama: { contains: query, mode: "insensitive" } }
- ],
+ where: {
+ OR: [{ nama: { contains: query, mode: "insensitive" } }],
},
skip,
take: limitNum,
@@ -983,10 +1308,8 @@ export default async function searchFindMany(context: Context) {
if (type === "siswa") {
const data = await prisma.siswa.findMany({
- where: {
- OR: [
- { nama: { contains: query, mode: "insensitive" } }
- ],
+ where: {
+ OR: [{ nama: { contains: query, mode: "insensitive" } }],
},
skip,
take: limitNum,
@@ -996,10 +1319,8 @@ export default async function searchFindMany(context: Context) {
if (type === "pengajar") {
const data = await prisma.pengajar.findMany({
- where: {
- OR: [
- { nama: { contains: query, mode: "insensitive" } }
- ],
+ where: {
+ OR: [{ nama: { contains: query, mode: "insensitive" } }],
},
skip,
take: limitNum,
@@ -1010,10 +1331,10 @@ export default async function searchFindMany(context: Context) {
// ========================================= BEASISWA DESA ========================================= //
if (type === "keunggulanProgram") {
const data = await prisma.keunggulanProgram.findMany({
- where: {
+ where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
- { deskripsi: { contains: query, mode: "insensitive" } }
+ { deskripsi: { contains: query, mode: "insensitive" } },
],
},
skip,
@@ -1025,10 +1346,10 @@ export default async function searchFindMany(context: Context) {
// ========================================= PROGRAM PENDIDIKAN ANAK ========================================= //
if (type === "tujuanProgram") {
const data = await prisma.tujuanProgram.findMany({
- where: {
+ where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
- { deskripsi: { contains: query, mode: "insensitive" } }
+ { deskripsi: { contains: query, mode: "insensitive" } },
],
},
skip,
@@ -1039,10 +1360,10 @@ export default async function searchFindMany(context: Context) {
if (type === "programUnggulan") {
const data = await prisma.programUnggulan.findMany({
- where: {
+ where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
- { deskripsi: { contains: query, mode: "insensitive" } }
+ { deskripsi: { contains: query, mode: "insensitive" } },
],
},
skip,
@@ -1053,10 +1374,10 @@ export default async function searchFindMany(context: Context) {
if (type === "lokasiJadwalBimbinganBelajarDesa") {
const data = await prisma.lokasiJadwalBimbinganBelajarDesa.findMany({
- where: {
+ where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
- { deskripsi: { contains: query, mode: "insensitive" } }
+ { deskripsi: { contains: query, mode: "insensitive" } },
],
},
skip,
@@ -1067,10 +1388,10 @@ export default async function searchFindMany(context: Context) {
if (type === "fasilitasBimbinganBelajarDesa") {
const data = await prisma.fasilitasBimbinganBelajarDesa.findMany({
- where: {
+ where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
- { deskripsi: { contains: query, mode: "insensitive" } }
+ { deskripsi: { contains: query, mode: "insensitive" } },
],
},
skip,
@@ -1082,10 +1403,10 @@ export default async function searchFindMany(context: Context) {
// ========================================= PENDIDIKAN NON FORMAL ========================================= //
if (type === "tujuanPendidikanNonFormal") {
const data = await prisma.tujuanPendidikanNonFormal.findMany({
- where: {
+ where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
- { deskripsi: { contains: query, mode: "insensitive" } }
+ { deskripsi: { contains: query, mode: "insensitive" } },
],
},
skip,
@@ -1096,10 +1417,10 @@ export default async function searchFindMany(context: Context) {
if (type === "tempatKegiatan") {
const data = await prisma.tempatKegiatan.findMany({
- where: {
+ where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
- { deskripsi: { contains: query, mode: "insensitive" } }
+ { deskripsi: { contains: query, mode: "insensitive" } },
],
},
skip,
@@ -1110,10 +1431,10 @@ export default async function searchFindMany(context: Context) {
if (type === "jenisProgramYangDiselenggarakan") {
const data = await prisma.jenisProgramYangDiselenggarakan.findMany({
- where: {
+ where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
- { deskripsi: { contains: query, mode: "insensitive" } }
+ { deskripsi: { contains: query, mode: "insensitive" } },
],
},
skip,
@@ -1125,10 +1446,15 @@ export default async function searchFindMany(context: Context) {
// ========================================= PERPUSTAKAAN ========================================= //
if (type === "dataPerpustakaan") {
const data = await prisma.dataPerpustakaan.findMany({
- where: {
+ where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
- { deskripsi: { contains: query, mode: "insensitive" } }
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ {
+ kategori: {
+ OR: [{ name: { contains: query, mode: "insensitive" } }],
+ },
+ },
],
},
skip,
@@ -1140,10 +1466,10 @@ export default async function searchFindMany(context: Context) {
// ========================================= DATA PENDIDIKAN ========================================= //
if (type === "dataPendidikan") {
const data = await prisma.dataPendidikan.findMany({
- where: {
+ where: {
OR: [
{ name: { contains: query, mode: "insensitive" } },
- { jumlah: { contains: query, mode: "insensitive" } }
+ { jumlah: { contains: query, mode: "insensitive" } },
],
},
skip,
@@ -1240,15 +1566,19 @@ export default async function searchFindMany(context: Context) {
tempatKegiatan,
jenisProgramYangDiselenggarakan,
dataPerpustakaan,
- dataPendidikan
-
+ dataPendidikan,
] = await Promise.all([
prisma.pejabatDesa.findMany({
where: { name: { contains: query, mode: "insensitive" } },
take: limitNum,
}),
prisma.programInovasi.findMany({
- where: { name: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { description: { contains: query, mode: "insensitive" } },
+ ],
+ },
take: limitNum,
}),
prisma.mediaSosial.findMany({
@@ -1256,7 +1586,13 @@ export default async function searchFindMany(context: Context) {
take: limitNum,
}),
prisma.desaAntiKorupsi.findMany({
- where: { name: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ { kategori: { name: { contains: query, mode: "insensitive" } } },
+ ],
+ },
take: limitNum,
}),
prisma.sdgsDesa.findMany({
@@ -1264,11 +1600,22 @@ export default async function searchFindMany(context: Context) {
take: limitNum,
}),
prisma.aPBDes.findMany({
- where: { name: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ ],
+ },
take: limitNum,
}),
prisma.prestasiDesa.findMany({
- where: { name: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ { kategori: { name: { contains: query, mode: "insensitive" } } },
+ ],
+ },
take: limitNum,
}),
prisma.responden.findMany({
@@ -1278,9 +1625,23 @@ export default async function searchFindMany(context: Context) {
// ✅ FIXED
prisma.strukturPPID.findMany({
where: {
- PegawaiPPID: { namaLengkap: { contains: query, mode: "insensitive" } },
+ OR: [
+ {
+ PegawaiPPID: {
+ namaLengkap: { contains: query, mode: "insensitive" },
+ },
+ },
+ {
+ PosisiOrganisasiPPID: {
+ nama: { contains: query, mode: "insensitive" },
+ },
+ },
+ ],
+ },
+ include: {
+ PosisiOrganisasiPPID: true,
+ PegawaiPPID: true,
},
- include: { PegawaiPPID: true },
take: limitNum,
}),
prisma.visiMisiPPID.findMany({
@@ -1293,23 +1654,54 @@ export default async function searchFindMany(context: Context) {
take: limitNum,
}),
prisma.dasarHukumPPID.findMany({
- where: { judul: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { judul: { contains: query, mode: "insensitive" } },
+ { content: { contains: query, mode: "insensitive" } },
+ ],
+ },
take: limitNum,
}),
prisma.profilePPID.findMany({
- where: { name: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { biodata: { contains: query, mode: "insensitive" } },
+ { riwayat: { contains: query, mode: "insensitive" } },
+ { pengalaman: { contains: query, mode: "insensitive" } },
+ { unggulan: { contains: query, mode: "insensitive" } },
+ ],
+ },
take: limitNum,
}),
prisma.daftarInformasiPublik.findMany({
- where: { jenisInformasi: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { jenisInformasi: { contains: query, mode: "insensitive" } },
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ ],
+ },
take: limitNum,
}),
prisma.permohonanInformasiPublik.findMany({
- where: { name: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { nik: { contains: query, mode: "insensitive" } },
+ { alamat: { contains: query, mode: "insensitive" } },
+ { email: { contains: query, mode: "insensitive" } },
+ ],
+ },
take: limitNum,
}),
prisma.formulirPermohonanKeberatan.findMany({
- where: { name: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { email: { contains: query, mode: "insensitive" } },
+ { alasan: { contains: query, mode: "insensitive" } },
+ ],
+ },
take: limitNum,
}),
prisma.indeksKepuasanMasyarakat.findMany({
@@ -1317,7 +1709,12 @@ export default async function searchFindMany(context: Context) {
take: limitNum,
}),
prisma.sejarahDesa.findMany({
- where: { judul: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { judul: { contains: query, mode: "insensitive" } },
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ ],
+ },
take: limitNum,
}),
prisma.visiMisiDesa.findMany({
@@ -1330,23 +1727,55 @@ export default async function searchFindMany(context: Context) {
take: limitNum,
}),
prisma.lambangDesa.findMany({
- where: { judul: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { judul: { contains: query, mode: "insensitive" } },
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ ],
+ },
take: limitNum,
}),
prisma.maskotDesa.findMany({
- where: { judul: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { judul: { contains: query, mode: "insensitive" } },
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ ],
+ },
take: limitNum,
}),
prisma.profilPerbekel.findMany({
- where: { biodata: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { biodata: { contains: query, mode: "insensitive" } },
+ { pengalaman: { contains: query, mode: "insensitive" } },
+ { pengalamanOrganisasi: { contains: query, mode: "insensitive" } },
+ { programUnggulan: { contains: query, mode: "insensitive" } },
+ ],
+ },
take: limitNum,
}),
prisma.perbekelDariMasaKeMasa.findMany({
- where: { nama: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { nama: { contains: query, mode: "insensitive" } },
+ { periode: { contains: query, mode: "insensitive" } },
+ { daerah: { contains: query, mode: "insensitive" } },
+ ],
+ },
take: limitNum,
}),
prisma.berita.findMany({
- where: { judul: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { judul: { contains: query, mode: "insensitive" } },
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ { content: { contains: query, mode: "insensitive" } },
+ {
+ kategoriBerita: { name: { contains: query, mode: "insensitive" } },
+ },
+ ],
+ },
take: limitNum,
}),
prisma.kategoriBerita.findMany({
@@ -1354,52 +1783,123 @@ export default async function searchFindMany(context: Context) {
take: limitNum,
}),
prisma.potensiDesa.findMany({
- where: { name: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ { content: { contains: query, mode: "insensitive" } },
+ { kategori: { nama: { contains: query, mode: "insensitive" } } },
+ ],
+ },
take: limitNum,
}),
prisma.pengumuman.findMany({
- where: { judul: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { judul: { contains: query, mode: "insensitive" } },
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ { content: { contains: query, mode: "insensitive" } },
+ {
+ CategoryPengumuman: {
+ name: { contains: query, mode: "insensitive" },
+ },
+ },
+ ],
+ },
take: limitNum,
}),
prisma.galleryFoto.findMany({
- where: { name: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ ],
+ },
take: limitNum,
}),
prisma.galleryVideo.findMany({
- where: { name: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ ],
+ },
take: limitNum,
}),
prisma.pelayananSuratKeterangan.findMany({
- where: { name: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ ],
+ },
take: limitNum,
}),
prisma.pelayananPerizinanBerusaha.findMany({
- where: { name: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ ],
+ },
take: limitNum,
}),
prisma.pelayananTelunjukSaktiDesa.findMany({
- where: { name: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ ],
+ },
take: limitNum,
}),
prisma.pelayananPendudukNonPermanen.findMany({
- where: { name: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ ],
+ },
take: limitNum,
}),
prisma.penghargaan.findMany({
- where: { name: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { juara: { contains: query, mode: "insensitive" } },
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ ],
+ },
take: limitNum,
}),
prisma.posyandu.findMany({
- where: { name: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { nomor: { contains: query, mode: "insensitive" } },
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ { jadwalPelayanan: { contains: query, mode: "insensitive" } },
+ ],
+ },
take: limitNum,
}),
prisma.fasilitasKesehatan.findMany({
where: {
name: { contains: query, mode: "insensitive" },
- informasiumum: { fasilitas: { contains: query, mode: "insensitive" } },
+ informasiumum: {
+ OR: [
+ { fasilitas: { contains: query, mode: "insensitive" } },
+ { alamat: { contains: query, mode: "insensitive" } },
+ { jamOperasional: { contains: query, mode: "insensitive" } },
+ ],
+ },
layananunggulan: { content: { contains: query, mode: "insensitive" } },
dokterdantenagamedis: {
- name: { contains: query, mode: "insensitive" },
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { specialist: { contains: query, mode: "insensitive" } },
+ { jadwal: { contains: query, mode: "insensitive" } },
+ ],
},
fasilitaspendukung: {
content: { contains: query, mode: "insensitive" },
@@ -1407,7 +1907,12 @@ export default async function searchFindMany(context: Context) {
prosedurpendaftaran: {
content: { contains: query, mode: "insensitive" },
},
- tarifdanlayanan: { layanan: { contains: query, mode: "insensitive" } },
+ tarifdanlayanan: {
+ OR: [
+ { layanan: { contains: query, mode: "insensitive" } },
+ { tarif: { contains: query, mode: "insensitive" } },
+ ],
+ },
},
take: limitNum,
}),
@@ -1415,7 +1920,12 @@ export default async function searchFindMany(context: Context) {
where: {
content: { contains: query, mode: "insensitive" },
informasijadwalkegiatan: {
- name: { contains: query, mode: "insensitive" },
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { tanggal: { contains: query, mode: "insensitive" } },
+ { waktu: { contains: query, mode: "insensitive" } },
+ { lokasi: { contains: query, mode: "insensitive" } },
+ ],
},
deskripsijadwalkegiatan: {
deskripsi: { contains: query, mode: "insensitive" },
@@ -1430,7 +1940,14 @@ export default async function searchFindMany(context: Context) {
content: { contains: query, mode: "insensitive" },
},
pendaftaranjadwalkegiatan: {
- name: { contains: query, mode: "insensitive" },
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { tanggal: { contains: query, mode: "insensitive" } },
+ { namaOrangtua: { contains: query, mode: "insensitive" } },
+ { nomor: { contains: query, mode: "insensitive" } },
+ { alamat: { contains: query, mode: "insensitive" } },
+ { catatan: { contains: query, mode: "insensitive" } },
+ ],
},
},
take: limitNum,
@@ -1443,16 +1960,29 @@ export default async function searchFindMany(context: Context) {
content: { contains: query, mode: "insensitive" },
},
symptom: {
- title: { contains: query, mode: "insensitive" },
+ OR: [
+ { title: { contains: query, mode: "insensitive" } },
+ { content: { contains: query, mode: "insensitive" } },
+ ],
},
prevention: {
- title: { contains: query, mode: "insensitive" },
+ OR: [
+ { title: { contains: query, mode: "insensitive" } },
+ { content: { contains: query, mode: "insensitive" } },
+ ],
},
firstaid: {
- title: { contains: query, mode: "insensitive" },
+ OR: [
+ { title: { contains: query, mode: "insensitive" } },
+ { content: { contains: query, mode: "insensitive" } },
+ ],
},
mythvsfact: {
- title: { contains: query, mode: "insensitive" },
+ OR: [
+ { title: { contains: query, mode: "insensitive" } },
+ { mitos: { contains: query, mode: "insensitive" } },
+ { fakta: { contains: query, mode: "insensitive" } },
+ ],
},
doctorsign: {
content: { contains: query, mode: "insensitive" },
@@ -1461,69 +1991,186 @@ export default async function searchFindMany(context: Context) {
take: limitNum,
}),
prisma.puskesmas.findMany({
- where: { name: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { alamat: { contains: query, mode: "insensitive" } },
+ {
+ jam: {
+ OR: [
+ { workDays: { contains: query, mode: "insensitive" } },
+ { weekDays: { contains: query, mode: "insensitive" } },
+ { holiday: { contains: query, mode: "insensitive" } },
+ ],
+ },
+ },
+ {
+ kontak: {
+ OR: [
+ {
+ kontakPuskesmas: { contains: query, mode: "insensitive" },
+ email: { contains: query, mode: "insensitive" },
+ facebook: { contains: query, mode: "insensitive" },
+ kontakUGD: { contains: query, mode: "insensitive" },
+ },
+ ],
+ },
+ },
+ ],
+ },
take: limitNum,
}),
prisma.programKesehatan.findMany({
- where: { name: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { deskripsiSingkat: { contains: query, mode: "insensitive" } },
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ ],
+ },
take: limitNum,
}),
prisma.penangananDarurat.findMany({
- where: { name: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ ],
+ },
take: limitNum,
}),
prisma.kontakDarurat.findMany({
- where: { name: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ ],
+ },
take: limitNum,
}),
prisma.infoWabahPenyakit.findMany({
- where: { name: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { deskripsiSingkat: { contains: query, mode: "insensitive" } },
+ { deskripsiLengkap: { contains: query, mode: "insensitive" } },
+ ],
+ },
take: limitNum,
}),
prisma.keamananLingkungan.findMany({
- where: { name: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { name: { contains: query, mode: "insensitive" } },
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ ],
+ },
take: limitNum,
}),
prisma.polsekTerdekat.findMany({
- where: { nama: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { nama: { contains: query, mode: "insensitive" } },
+ { alamat: { contains: query, mode: "insensitive" } },
+ { nomorTelepon: { contains: query, mode: "insensitive" } },
+ ],
+ },
take: limitNum,
}),
prisma.kontakDaruratKeamanan.findMany({
- where: { nama: { contains: query, mode: "insensitive" } },
+ where: {
+ nama: { contains: query, mode: "insensitive" },
+ kategori: {
+ OR: [
+ { nama: { contains: query, mode: "insensitive" } },
+ { nomorTelepon: { contains: query, mode: "insensitive" } },
+ ],
+ },
+ },
take: limitNum,
}),
prisma.pencegahanKriminalitas.findMany({
- where: { judul: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { judul: { contains: query, mode: "insensitive" } },
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ { deskripsiSingkat: { contains: query, mode: "insensitive" } },
+ ],
+ },
take: limitNum,
}),
prisma.laporanPublik.findMany({
- where: { judul: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { judul: { contains: query, mode: "insensitive" } },
+ { lokasi: { contains: query, mode: "insensitive" } },
+ ],
+ },
take: limitNum,
}),
prisma.menuTipsKeamanan.findMany({
- where: { judul: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { judul: { contains: query, mode: "insensitive" } },
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ ],
+ },
take: limitNum,
}),
prisma.pasarDesa.findMany({
- where: { nama: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { nama: { contains: query, mode: "insensitive" } },
+ { alamatUsaha: { contains: query, mode: "insensitive" } },
+ { kontak: { contains: query, mode: "insensitive" } },
+ {
+ kategoriProduk: { nama: { contains: query, mode: "insensitive" } },
+ },
+ ],
+ },
take: limitNum,
}),
prisma.lowonganPekerjaan.findMany({
- where: { posisi: { contains: query, mode: "insensitive" } },
+ where: {
+ OR: [
+ { posisi: { contains: query, mode: "insensitive" } },
+ { namaPerusahaan: { contains: query, mode: "insensitive" } },
+ { lokasi: { contains: query, mode: "insensitive" } },
+ { tipePekerjaan: { contains: query, mode: "insensitive" } },
+ { gaji: { contains: query, mode: "insensitive" } },
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ { kualifikasi: { contains: query, mode: "insensitive" } },
+ { notelp: { contains: query, mode: "insensitive" } },
+ ],
+ },
take: limitNum,
}),
prisma.strukturBumDes.findMany({
- where: {
+ where: {
OR: [
{ name: { contains: query, mode: "insensitive" } },
- { PosisiOrganisasiBumDes: { nama: { contains: query, mode: "insensitive" } } },
- { PegawaiBumDes: { namaLengkap: { contains: query, mode: "insensitive" } } },
+ {
+ PosisiOrganisasiBumDes: {
+ nama: { contains: query, mode: "insensitive" },
+ },
+ },
+ {
+ PegawaiBumDes: {
+ OR: [
+ { namaLengkap: { contains: query, mode: "insensitive" } },
+ { email: { contains: query, mode: "insensitive" } },
+ { telepon: { contains: query, mode: "insensitive" } },
+ { alamat: { contains: query, mode: "insensitive" } },
+ { gelarAkademik: { contains: query, mode: "insensitive" } },
+ ],
+ },
+ },
],
},
take: limitNum,
}),
prisma.grafikMenganggurBerdasarkanUsia.findMany({
- where: {
+ where: {
OR: [
{ usia18_25: { contains: query, mode: "insensitive" } },
{ usia26_35: { contains: query, mode: "insensitive" } },
@@ -1534,7 +2181,7 @@ export default async function searchFindMany(context: Context) {
take: limitNum,
}),
prisma.grafikMenganggurBerdasarkanPendidikan.findMany({
- where: {
+ where: {
OR: [
{ SD: { contains: query, mode: "insensitive" } },
{ SMP: { contains: query, mode: "insensitive" } },
@@ -1546,18 +2193,20 @@ export default async function searchFindMany(context: Context) {
take: limitNum,
}),
prisma.grafikJumlahPendudukMiskin.findMany({
- where: {
+ where: {
OR: [
- ...(isNaN(Number(query)) ? [] : [
- { year: { equals: Number(query) } },
- { totalPoorPopulation: { equals: Number(query) } }
- ])
+ ...(isNaN(Number(query))
+ ? []
+ : [
+ { year: { equals: Number(query) } },
+ { totalPoorPopulation: { equals: Number(query) } },
+ ]),
].filter(Boolean),
},
take: limitNum,
}),
prisma.programKemiskinan.findMany({
- where: {
+ where: {
OR: [
{ nama: { contains: query, mode: "insensitive" } },
{ deskripsi: { contains: query, mode: "insensitive" } },
@@ -1566,291 +2215,290 @@ export default async function searchFindMany(context: Context) {
take: limitNum,
}),
prisma.sektorUnggulanDesa.findMany({
- where: {
+ where: {
OR: [
{ name: { contains: query, mode: "insensitive" } },
- { description: { contains: query, mode: "insensitive" } }
+ { description: { contains: query, mode: "insensitive" } },
],
},
take: limitNum,
}),
prisma.dataDemografiPekerjaan.findMany({
- where: {
- pekerjaan: { contains: query, mode: "insensitive" }
+ where: {
+ pekerjaan: { contains: query, mode: "insensitive" },
},
take: limitNum,
}),
prisma.desaDigital.findMany({
- where: {
+ where: {
OR: [
{ name: { contains: query, mode: "insensitive" } },
- { deskripsi: { contains: query, mode: "insensitive" } }
+ { deskripsi: { contains: query, mode: "insensitive" } },
],
},
take: limitNum,
}),
prisma.programKreatif.findMany({
- where: {
+ where: {
OR: [
{ name: { contains: query, mode: "insensitive" } },
{ slug: { contains: query, mode: "insensitive" } },
- { deskripsi: { contains: query, mode: "insensitive" } }
+ { deskripsi: { contains: query, mode: "insensitive" } },
],
},
take: limitNum,
}),
prisma.kolaborasiInovasi.findMany({
- where: {
+ where: {
OR: [
{ name: { contains: query, mode: "insensitive" } },
{ slug: { contains: query, mode: "insensitive" } },
{ deskripsi: { contains: query, mode: "insensitive" } },
- { kolaborator: { contains: query, mode: "insensitive" } }
+ { kolaborator: { contains: query, mode: "insensitive" } },
],
},
take: limitNum,
}),
prisma.mitraKolaborasi.findMany({
- where: {
- OR: [
- { name: { contains: query, mode: "insensitive" } }
- ],
+ where: {
+ OR: [{ name: { contains: query, mode: "insensitive" } }],
},
take: limitNum,
}),
prisma.infoTekno.findMany({
- where: {
+ where: {
OR: [
{ name: { contains: query, mode: "insensitive" } },
- { deskripsi: { contains: query, mode: "insensitive" } }
+ { deskripsi: { contains: query, mode: "insensitive" } },
],
},
take: limitNum,
}),
prisma.pengelolaanSampah.findMany({
- where: {
- OR: [
- { name: { contains: query, mode: "insensitive" } }
- ],
+ where: {
+ OR: [{ name: { contains: query, mode: "insensitive" } }],
},
take: limitNum,
}),
prisma.keteranganBankSampahTerdekat.findMany({
- where: {
+ where: {
OR: [
{ name: { contains: query, mode: "insensitive" } },
{ alamat: { contains: query, mode: "insensitive" } },
{ namaTempatMaps: { contains: query, mode: "insensitive" } },
- { linkPetunjukArah: { contains: query, mode: "insensitive" } }
+ { linkPetunjukArah: { contains: query, mode: "insensitive" } },
],
},
take: limitNum,
}),
prisma.programPenghijauan.findMany({
- where: {
+ where: {
OR: [
{ name: { contains: query, mode: "insensitive" } },
{ judul: { contains: query, mode: "insensitive" } },
- { deskripsi: { contains: query, mode: "insensitive" } }
+ { deskripsi: { contains: query, mode: "insensitive" } },
],
},
take: limitNum,
- }),
+ }),
prisma.dataLingkunganDesa.findMany({
- where: {
+ where: {
OR: [
{ name: { contains: query, mode: "insensitive" } },
- { deskripsi: { contains: query, mode: "insensitive" } }
+ { deskripsi: { contains: query, mode: "insensitive" } },
],
},
take: limitNum,
}),
prisma.kegiatanDesa.findMany({
- where: {
+ where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
{ deskripsiSingkat: { contains: query, mode: "insensitive" } },
- { deskripsiLengkap: { contains: query, mode: "insensitive" } }
+ { deskripsiLengkap: { contains: query, mode: "insensitive" } },
+ { lokasi: { contains: query, mode: "insensitive" } },
+ {
+ kategoriKegiatan: {
+ nama: { contains: query, mode: "insensitive" },
+ },
+ },
],
},
take: limitNum,
}),
prisma.tujuanEdukasiLingkungan.findMany({
- where: {
+ where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
- { deskripsi: { contains: query, mode: "insensitive" } }
+ { deskripsi: { contains: query, mode: "insensitive" } },
],
},
take: limitNum,
}),
prisma.materiEdukasiLingkungan.findMany({
- where: {
+ where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
- { deskripsi: { contains: query, mode: "insensitive" } }
+ { deskripsi: { contains: query, mode: "insensitive" } },
],
},
take: limitNum,
}),
prisma.contohEdukasiLingkungan.findMany({
- where: {
+ where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
- { deskripsi: { contains: query, mode: "insensitive" } }
+ { deskripsi: { contains: query, mode: "insensitive" } },
],
},
take: limitNum,
}),
prisma.filosofiTriHita.findMany({
- where: {
+ where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
- { deskripsi: { contains: query, mode: "insensitive" } }
+ { deskripsi: { contains: query, mode: "insensitive" } },
],
},
take: limitNum,
}),
prisma.bentukKonservasiBerdasarkanAdat.findMany({
- where: {
+ where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
- { deskripsi: { contains: query, mode: "insensitive" } }
+ { deskripsi: { contains: query, mode: "insensitive" } },
],
},
take: limitNum,
}),
prisma.nilaiKonservasiAdat.findMany({
- where: {
+ where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
- { deskripsi: { contains: query, mode: "insensitive" } }
+ { deskripsi: { contains: query, mode: "insensitive" } },
],
},
take: limitNum,
}),
prisma.jenjangPendidikan.findMany({
- where: {
- OR: [
- { nama: { contains: query, mode: "insensitive" } }
- ],
+ where: {
+ OR: [{ nama: { contains: query, mode: "insensitive" } }],
},
take: limitNum,
}),
prisma.lembaga.findMany({
- where: {
- OR: [
- { nama: { contains: query, mode: "insensitive" } }
- ],
+ where: {
+ OR: [{ nama: { contains: query, mode: "insensitive" } }],
},
take: limitNum,
}),
prisma.siswa.findMany({
- where: {
- OR: [
- { nama: { contains: query, mode: "insensitive" } }
- ],
+ where: {
+ OR: [{ nama: { contains: query, mode: "insensitive" } }],
},
take: limitNum,
}),
prisma.pengajar.findMany({
- where: {
- OR: [
- { nama: { contains: query, mode: "insensitive" } }
- ],
+ where: {
+ OR: [{ nama: { contains: query, mode: "insensitive" } }],
},
take: limitNum,
}),
prisma.keunggulanProgram.findMany({
- where: {
+ where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
- { deskripsi: { contains: query, mode: "insensitive" } }
+ { deskripsi: { contains: query, mode: "insensitive" } },
],
},
take: limitNum,
}),
prisma.tujuanProgram.findMany({
- where: {
+ where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
- { deskripsi: { contains: query, mode: "insensitive" } }
+ { deskripsi: { contains: query, mode: "insensitive" } },
],
},
take: limitNum,
}),
prisma.programUnggulan.findMany({
- where: {
+ where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
- { deskripsi: { contains: query, mode: "insensitive" } }
+ { deskripsi: { contains: query, mode: "insensitive" } },
],
},
take: limitNum,
}),
prisma.lokasiJadwalBimbinganBelajarDesa.findMany({
- where: {
+ where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
- { deskripsi: { contains: query, mode: "insensitive" } }
+ { deskripsi: { contains: query, mode: "insensitive" } },
],
},
take: limitNum,
}),
prisma.fasilitasBimbinganBelajarDesa.findMany({
- where: {
+ where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
- { deskripsi: { contains: query, mode: "insensitive" } }
+ { deskripsi: { contains: query, mode: "insensitive" } },
],
},
take: limitNum,
}),
prisma.tujuanPendidikanNonFormal.findMany({
- where: {
+ where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
- { deskripsi: { contains: query, mode: "insensitive" } }
+ { deskripsi: { contains: query, mode: "insensitive" } },
],
},
take: limitNum,
}),
prisma.tempatKegiatan.findMany({
- where: {
+ where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
- { deskripsi: { contains: query, mode: "insensitive" } }
+ { deskripsi: { contains: query, mode: "insensitive" } },
],
},
take: limitNum,
}),
prisma.jenisProgramYangDiselenggarakan.findMany({
- where: {
+ where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
- { deskripsi: { contains: query, mode: "insensitive" } }
+ { deskripsi: { contains: query, mode: "insensitive" } },
],
},
take: limitNum,
}),
prisma.dataPerpustakaan.findMany({
- where: {
+ where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
- { deskripsi: { contains: query, mode: "insensitive" } }
+ { deskripsi: { contains: query, mode: "insensitive" } },
+ {
+ kategori: {
+ OR: [{ name: { contains: query, mode: "insensitive" } }],
+ },
+ },
],
},
take: limitNum,
}),
prisma.dataPendidikan.findMany({
- where: {
+ where: {
OR: [
{ name: { contains: query, mode: "insensitive" } },
- { jumlah: { contains: query, mode: "insensitive" } }
+ { jumlah: { contains: query, mode: "insensitive" } },
],
},
take: limitNum,
- })
+ }),
]);
return {
@@ -1920,16 +2568,31 @@ export default async function searchFindMany(context: Context) {
...infoWabahPenyakit.map((b) => ({ type: "infoWabahPenyakit", ...b })),
...keamananLingkungan.map((b) => ({ type: "keamananLingkungan", ...b })),
...polsekTerdekat.map((b) => ({ type: "polsekTerdekat", ...b })),
- ...kontakDaruratKeamanan.map((b) => ({ type: "kontakDaruratKeamanan", ...b })),
- ...pencegahanKriminalitas.map((b) => ({ type: "pencegahanKriminalitas", ...b })),
+ ...kontakDaruratKeamanan.map((b) => ({
+ type: "kontakDaruratKeamanan",
+ ...b,
+ })),
+ ...pencegahanKriminalitas.map((b) => ({
+ type: "pencegahanKriminalitas",
+ ...b,
+ })),
...laporanPublik.map((b) => ({ type: "laporanPublik", ...b })),
...tipsKeamanan.map((b) => ({ type: "tipsKeamanan", ...b })),
...pasarDesa.map((b) => ({ type: "pasarDesa", ...b })),
...lowonganKerjaLokal.map((b) => ({ type: "lowonganKerjaLokal", ...b })),
...strukturOrganisasi.map((b) => ({ type: "strukturOrganisasi", ...b })),
- ...jumlahPendudukUsiaKerjaYangMenganggurUsia.map((b) => ({ type: "jumlahPendudukUsiaKerjaYangMenganggurUsia", ...b })),
- ...jumlahPendudukUsiaKerjaYangMenganggurPendidikan.map((b) => ({ type: "jumlahPendudukUsiaKerjaYangMenganggurPendidikan", ...b })),
- ...jumlahPendudukMiskin.map((b) => ({ type: "jumlahPendudukMiskin", ...b })),
+ ...jumlahPendudukUsiaKerjaYangMenganggurUsia.map((b) => ({
+ type: "jumlahPendudukUsiaKerjaYangMenganggurUsia",
+ ...b,
+ })),
+ ...jumlahPendudukUsiaKerjaYangMenganggurPendidikan.map((b) => ({
+ type: "jumlahPendudukUsiaKerjaYangMenganggurPendidikan",
+ ...b,
+ })),
+ ...jumlahPendudukMiskin.map((b) => ({
+ type: "jumlahPendudukMiskin",
+ ...b,
+ })),
...programKemiskinan.map((b) => ({ type: "programKemiskinan", ...b })),
...sektorUnggulanDesa.map((b) => ({ type: "sektorUnggulanDesa", ...b })),
...demografiPekerjaan.map((b) => ({ type: "demografiPekerjaan", ...b })),
@@ -1939,16 +2602,34 @@ export default async function searchFindMany(context: Context) {
...mitraKolaborasi.map((b) => ({ type: "mitraKolaborasi", ...b })),
...infoTekno.map((b) => ({ type: "infoTekno", ...b })),
...pengelolaanSampah.map((b) => ({ type: "pengelolaanSampah", ...b })),
- ...keteranganBankSampahTerdekat.map((b) => ({ type: "keteranganBankSampahTerdekat", ...b })),
+ ...keteranganBankSampahTerdekat.map((b) => ({
+ type: "keteranganBankSampahTerdekat",
+ ...b,
+ })),
...programPenghijauan.map((b) => ({ type: "programPenghijauan", ...b })),
...dataLingkunganDesa.map((b) => ({ type: "dataLingkunganDesa", ...b })),
...gotongRoyong.map((b) => ({ type: "gotongRoyong", ...b })),
- ...tujuanEdukasiLingkungan.map((b) => ({ type: "tujuanEdukasiLingkungan", ...b })),
- ...materiEdukasiLingkungan.map((b) => ({ type: "materiEdukasiLingkungan", ...b })),
- ...contohEdukasiLingkungan.map((b) => ({ type: "contohEdukasiLingkungan", ...b })),
+ ...tujuanEdukasiLingkungan.map((b) => ({
+ type: "tujuanEdukasiLingkungan",
+ ...b,
+ })),
+ ...materiEdukasiLingkungan.map((b) => ({
+ type: "materiEdukasiLingkungan",
+ ...b,
+ })),
+ ...contohEdukasiLingkungan.map((b) => ({
+ type: "contohEdukasiLingkungan",
+ ...b,
+ })),
...filosofiTriHita.map((b) => ({ type: "filosofiTriHita", ...b })),
- ...bentukKonservasiBerdasarkanAdat.map((b) => ({ type: "bentukKonservasiBerdasarkanAdat", ...b })),
- ...nilaiKonservasiAdat.map((b) => ({ type: "nilaiKonservasiAdat", ...b })),
+ ...bentukKonservasiBerdasarkanAdat.map((b) => ({
+ type: "bentukKonservasiBerdasarkanAdat",
+ ...b,
+ })),
+ ...nilaiKonservasiAdat.map((b) => ({
+ type: "nilaiKonservasiAdat",
+ ...b,
+ })),
...jenjangPendidikan.map((b) => ({ type: "jenjangPendidikan", ...b })),
...lembaga.map((b) => ({ type: "lembaga", ...b })),
...siswa.map((b) => ({ type: "siswa", ...b })),
@@ -1956,14 +2637,25 @@ export default async function searchFindMany(context: Context) {
...keunggulanProgram.map((b) => ({ type: "keunggulanProgram", ...b })),
...tujuanProgram.map((b) => ({ type: "tujuanProgram", ...b })),
...programUnggulan.map((b) => ({ type: "programUnggulan", ...b })),
- ...tujuanPendidikanNonFormal.map((b) => ({ type: "tujuanPendidikanNonFormal", ...b })),
- ...fasilitasBimbinganBelajarDesa.map((b) => ({ type: "fasilitasBimbinganBelajarDesa", ...b })),
- ...lokasiJadwalBimbinganBelajarDesa.map((b) => ({ type: "lokasiJadwalBimbinganBelajarDesa", ...b })),
+ ...tujuanPendidikanNonFormal.map((b) => ({
+ type: "tujuanPendidikanNonFormal",
+ ...b,
+ })),
+ ...fasilitasBimbinganBelajarDesa.map((b) => ({
+ type: "fasilitasBimbinganBelajarDesa",
+ ...b,
+ })),
+ ...lokasiJadwalBimbinganBelajarDesa.map((b) => ({
+ type: "lokasiJadwalBimbinganBelajarDesa",
+ ...b,
+ })),
...tempatKegiatan.map((b) => ({ type: "tempatKegiatan", ...b })),
- ...jenisProgramYangDiselenggarakan.map((b) => ({ type: "jenisProgramYangDiselenggarakan", ...b })),
+ ...jenisProgramYangDiselenggarakan.map((b) => ({
+ type: "jenisProgramYangDiselenggarakan",
+ ...b,
+ })),
...dataPerpustakaan.map((b) => ({ type: "dataPerpustakaan", ...b })),
...dataPendidikan.map((b) => ({ type: "dataPendidikan", ...b })),
-
],
nextPage: null, // bisa dibuat lebih kompleks kalau perlu
};
diff --git a/src/app/api/[[...slugs]]/_lib/user/delUser.ts b/src/app/api/[[...slugs]]/_lib/user/delUser.ts
new file mode 100644
index 00000000..9f83ec93
--- /dev/null
+++ b/src/app/api/[[...slugs]]/_lib/user/delUser.ts
@@ -0,0 +1,62 @@
+// /api/user/delUser.ts
+import prisma from '@/lib/prisma';
+import { Context } from 'elysia';
+
+export default async function userDeleteAccount(context: Context) {
+ const { id } = context.params as { id: string };
+
+ try {
+ // 1. Cek user dulu
+ const existingUser = await prisma.user.findUnique({
+ where: { id },
+ });
+
+ if (!existingUser) {
+ return {
+ success: false,
+ message: 'User tidak ditemukan',
+ };
+ }
+
+ // ✅ 2. Hapus SEMUA relasi dalam TRANSACTION
+ const result = await prisma.$transaction(async (tx) => {
+ // Hapus UserSession
+ const deletedSessions = await tx.userSession.deleteMany({
+ where: { userId: id },
+ });
+
+ // ✅ Hapus UserMenuAccess
+ const deletedMenuAccess = await tx.userMenuAccess.deleteMany({
+ where: { userId: id },
+ });
+
+ // ✅ Tambahkan relasi lain jika ada (contoh):
+ // await tx.userLog.deleteMany({ where: { userId: id } });
+ // await tx.userNotification.deleteMany({ where: { userId: id } });
+ // await tx.userToken.deleteMany({ where: { userId: id } });
+
+ // Hapus user
+ const deletedUser = await tx.user.delete({
+ where: { id },
+ });
+
+ return {
+ user: deletedUser,
+ sessionsDeleted: deletedSessions.count,
+ menuAccessDeleted: deletedMenuAccess.count,
+ };
+ });
+
+ return {
+ success: true,
+ message: `User berhasil dihapus permanen (${result.sessionsDeleted} session, ${result.menuAccessDeleted} menu access)`,
+ data: result,
+ };
+ } catch (error) {
+ console.error('Error delete user:', error);
+ return {
+ success: false,
+ message: 'Terjadi kesalahan saat menghapus user',
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/app/api/[[...slugs]]/_lib/user/index.ts b/src/app/api/[[...slugs]]/_lib/user/index.ts
index 98dd2d8b..5e6aa75c 100644
--- a/src/app/api/[[...slugs]]/_lib/user/index.ts
+++ b/src/app/api/[[...slugs]]/_lib/user/index.ts
@@ -5,6 +5,7 @@ import userFindMany from "./findMany";
import userFindUnique from "./findUnique";
import userDelete from "./del"; // `delete` nggak boleh jadi nama file JS langsung, jadi biasanya `del.ts`
import userUpdate from "./updt";
+import userDeleteAccount from "./delUser";
const User = new Elysia({ prefix: "/api/user" })
.get("/findMany", userFindMany)
@@ -14,6 +15,21 @@ const User = new Elysia({ prefix: "/api/user" })
id: t.String(),
}),
}) // pakai PUT untuk soft delete
- .put("/updt", userUpdate);
+ .put(
+ "/updt",
+ userUpdate,
+ {
+ body: t.Object({
+ id: t.String(),
+ isActive: t.Optional(t.Boolean()),
+ roleId: t.Optional(t.String()),
+ })
+ }
+ )
+ .delete("/delUser/:id", userDeleteAccount, {
+ params: t.Object({
+ id: t.String(),
+ }),
+ });
export default User;
diff --git a/src/app/api/[[...slugs]]/_lib/user/role/create.ts b/src/app/api/[[...slugs]]/_lib/user/role/create.ts
index 12b6cba1..6b74c529 100644
--- a/src/app/api/[[...slugs]]/_lib/user/role/create.ts
+++ b/src/app/api/[[...slugs]]/_lib/user/role/create.ts
@@ -3,7 +3,6 @@ import { Context } from "elysia";
type FormCreate = {
name: string;
- permissions: string[];
}
export default async function roleCreate(context: Context) {
@@ -13,7 +12,6 @@ export default async function roleCreate(context: Context) {
const result = await prisma.role.create({
data: {
name: body.name,
- permissions: body.permissions,
},
});
return {
diff --git a/src/app/api/[[...slugs]]/_lib/user/role/index.ts b/src/app/api/[[...slugs]]/_lib/user/role/index.ts
index 8e227bcf..8fb78ac2 100644
--- a/src/app/api/[[...slugs]]/_lib/user/role/index.ts
+++ b/src/app/api/[[...slugs]]/_lib/user/role/index.ts
@@ -13,7 +13,6 @@ const Role = new Elysia({
.post("/create", roleCreate, {
body: t.Object({
name: t.String(),
- permissions: t.Array(t.String()),
}),
})
@@ -27,7 +26,6 @@ const Role = new Elysia({
.put("/:id", roleUpdate, {
body: t.Object({
name: t.String(),
- permissions: t.Array(t.String()),
}),
})
.delete("/del/:id", roleDelete);
diff --git a/src/app/api/[[...slugs]]/_lib/user/role/updt.ts b/src/app/api/[[...slugs]]/_lib/user/role/updt.ts
index fbe27b5c..113f6357 100644
--- a/src/app/api/[[...slugs]]/_lib/user/role/updt.ts
+++ b/src/app/api/[[...slugs]]/_lib/user/role/updt.ts
@@ -3,7 +3,6 @@ import { Context } from "elysia";
type FormUpdate = {
name: string;
- permissions: string[];
}
export default async function roleUpdate(context: Context) {
@@ -15,7 +14,6 @@ export default async function roleUpdate(context: Context) {
where: { id },
data: {
name: body.name,
- permissions: body.permissions,
},
});
return {
diff --git a/src/app/api/[[...slugs]]/_lib/user/updt.ts b/src/app/api/[[...slugs]]/_lib/user/updt.ts
index 4e99c92c..5905d272 100644
--- a/src/app/api/[[...slugs]]/_lib/user/updt.ts
+++ b/src/app/api/[[...slugs]]/_lib/user/updt.ts
@@ -2,39 +2,77 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
+// API update user
export default async function userUpdate(context: Context) {
try {
- const { id, isActive } = await context.body as { id: string, isActive: boolean };
+ const { id, isActive, roleId } = (await context.body) as {
+ id: string;
+ isActive?: boolean;
+ roleId?: string;
+ };
if (!id) {
- return {
- success: false,
- message: "ID user wajib ada",
- };
+ return { success: false, message: "ID user wajib ada" };
}
+ // Validasi role
+ if (roleId) {
+ const role = await prisma.role.findUnique({ where: { id: roleId } });
+ if (!role) return { success: false, message: "Role tidak ditemukan" };
+ }
+
+ const currentUser = await prisma.user.findUnique({
+ where: { id },
+ select: { roleId: true, isActive: true }
+ });
+
+ if (!currentUser) {
+ return { success: false, message: "User tidak ditemukan" };
+ }
+
+ const isRoleChanged = roleId && currentUser.roleId !== roleId;
+ const isActiveChanged = isActive !== undefined && currentUser.isActive !== isActive;
+
+ // Update user
const updatedUser = await prisma.user.update({
where: { id },
- data: { isActive },
+ data: {
+ ...(isActive !== undefined && { isActive }),
+ ...(roleId && { roleId }),
+ // Force logout: invalidate semua sesi
+ ...(isRoleChanged ? { sessionInvalid: true } : {}),
+ },
select: {
id: true,
username: true,
nomor: true,
isActive: true,
- updatedAt: true,
+ roleId: true,
+ role: { select: { name: true } }
}
});
+ // ✅ HAPUS SEMUA SESI USER DI DATABASE
+ if (isRoleChanged) {
+ await prisma.userSession.deleteMany({ where: { userId: id } });
+ }
+
return {
success: true,
- message: `User berhasil ${isActive ? "diaktifkan" : "dinonaktifkan"}`,
+ roleChanged: isRoleChanged,
+ isActiveChanged,
data: updatedUser,
+ message: isRoleChanged
+ ? `Role ${updatedUser.username} diubah. User akan logout otomatis.`
+ : isActiveChanged
+ ? `${updatedUser.username} ${isActive ? 'diaktifkan' : 'dinonaktifkan'}.`
+ : "User berhasil diupdate"
};
} catch (e: any) {
- console.error("Error update user:", e);
+ console.error("❌ Error update user:", e);
return {
success: false,
- message: "Gagal mengupdate status user",
+ message: "Gagal mengupdate user: " + (e.message || "Unknown error"),
};
}
-}
+}
\ No newline at end of file
diff --git a/src/app/api/admin/user-menu-access/route.ts b/src/app/api/admin/user-menu-access/route.ts
new file mode 100644
index 00000000..bd22e0a3
--- /dev/null
+++ b/src/app/api/admin/user-menu-access/route.ts
@@ -0,0 +1,65 @@
+// src/app/api/admin/user-menu-access/route.ts
+
+import { NextResponse } from 'next/server'
+import prisma from '@/lib/prisma'
+
+// ❌ HAPUS { params } karena tidak dipakai
+export async function GET(request: Request) {
+ try {
+ const { searchParams } = new URL(request.url)
+ const userId = searchParams.get('userId')
+
+ if (!userId) {
+ return NextResponse.json(
+ { success: false, message: 'User ID diperlukan' },
+ { status: 400 }
+ )
+ }
+
+ const menuAccess = await prisma.userMenuAccess.findMany({
+ where: { userId },
+ select: { menuId: true },
+ })
+
+ return NextResponse.json({
+ success: true,
+ menuIds: menuAccess.map(m => m.menuId),
+ })
+ } catch (error) {
+ console.error('GET User Menu Access Error:', error)
+ return NextResponse.json(
+ { success: false, message: 'Gagal memuat menu akses' },
+ { status: 500 }
+ )
+ }
+}
+
+// POST tetap sama (tanpa perubahan)
+export async function POST(request: Request) {
+ try {
+ const { userId, menuIds } = await request.json()
+
+ if (!userId || !Array.isArray(menuIds)) {
+ return NextResponse.json(
+ { success: false, message: 'Data tidak valid' },
+ { status: 400 }
+ )
+ }
+
+ await prisma.userMenuAccess.deleteMany({ where: { userId } })
+
+ if (menuIds.length > 0) {
+ await prisma.userMenuAccess.createMany({
+ data: menuIds.map((menuId: string) => ({ userId, menuId })),
+ })
+ }
+
+ return NextResponse.json({ success: true })
+ } catch (error) {
+ console.error('POST User Menu Access Error:', error)
+ return NextResponse.json(
+ { success: false, message: 'Gagal menyimpan menu akses' },
+ { status: 500 }
+ )
+ }
+}
\ No newline at end of file
diff --git a/src/app/api/auth/_lib/api_fetch_auth.ts b/src/app/api/auth/_lib/api_fetch_auth.ts
new file mode 100644
index 00000000..19de8fc1
--- /dev/null
+++ b/src/app/api/auth/_lib/api_fetch_auth.ts
@@ -0,0 +1,142 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+// app/api/auth/_lib/api_fetch_auth.ts
+
+// app/api/auth/_lib/api_fetch_auth.ts
+
+export const apiFetchLogin = async ({ nomor }: { nomor: string }) => {
+ if (!nomor || nomor.replace(/\D/g, '').length < 10) {
+ throw new Error('Nomor tidak valid');
+ }
+
+ const cleanPhone = nomor.replace(/\D/g, '');
+
+ const response = await fetch("/api/auth/login", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ nomor: cleanPhone }),
+ });
+
+ // Pastikan respons bisa di-parse sebagai JSON
+ let data;
+ try {
+ data = await response.json();
+ } catch (e) {
+ console.error("Non-JSON response from /api/auth/login:", await response.text());
+ throw new Error('Respons server tidak valid');
+ }
+
+ if (!response.ok) {
+ throw new Error(data.message || 'Gagal memproses login');
+ }
+
+ // Validasi minimal respons
+ if (typeof data.success !== 'boolean' || typeof data.isRegistered !== 'boolean') {
+ throw new Error('Respons tidak sesuai format');
+ }
+
+ if (data.success) {
+ if (data.isRegistered && !data.kodeId) {
+ throw new Error('Kode verifikasi tidak ditemukan untuk user terdaftar');
+ }
+ return data; // { success, isRegistered, kodeId? }
+ } else {
+ throw new Error(data.message || 'Login gagal');
+ }
+};
+
+export const apiFetchRegister = async ({
+ username,
+ nomor,
+}: {
+ username: string;
+ nomor: string;
+}) => {
+ const cleanPhone = nomor.replace(/\D/g, '');
+ if (cleanPhone.length < 10) throw new Error('Nomor tidak valid');
+
+ const response = await fetch("/api/auth/register", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ username: username.trim(), nomor: cleanPhone }),
+ });
+
+ const data = await response.json();
+ if (!response.ok) throw new Error(data.message || 'Gagal mengirim OTP');
+
+ return data;
+};
+
+export const apiFetchOtpData = async ({ kodeId }: { kodeId: string }) => {
+ if (!kodeId) {
+ throw new Error('Kode ID tidak valid');
+ }
+
+ const response = await fetch("/api/auth/otp-data", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ kodeId }),
+ });
+
+ const data = await response.json();
+
+ if (!response.ok) {
+ throw new Error(data.message || 'Gagal memuat data OTP');
+ }
+
+ return data;
+};
+
+// Ganti endpoint ke verify-otp-login
+export const apiFetchVerifyOtp = async ({ nomor, otp, kodeId }: { nomor: string; otp: string; kodeId: string }) => {
+ const response = await fetch('/api/auth/verify-otp-login', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ nomor, otp, kodeId }),
+ });
+ const data = await response.json();
+ return {
+ success: response.ok,
+ ...data,
+ status: response.status,
+ };
+};
+
+// Di dalam api_fetch_auth.ts
+
+export async function apiFetchUserMenuAccess(userId: string): Promise<{
+ success: boolean;
+ menuIds?: string[];
+ message?: string;
+}> {
+ try {
+ const res = await fetch(`/api/admin/user-menu-access/${userId}`, {
+ method: 'GET',
+ headers: { 'Content-Type': 'application/json' },
+ });
+
+ const data = await res.json();
+ return data;
+ } catch (error) {
+ console.error('API Fetch User Menu Access Error:', error);
+ return { success: false, message: 'Gagal memuat menu akses' };
+ }
+}
+
+export async function apiUpdateUserMenuAccess(
+ userId: string,
+ menuIds: string[]
+): Promise<{ success: boolean; message?: string }> {
+ try {
+ const res = await fetch('/api/admin/user-menu-access', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ userId, menuIds }),
+ });
+
+ const data = await res.json();
+ return data;
+ } catch (error) {
+ console.error('API Update User Menu Access Error:', error);
+ return { success: false, message: 'Gagal menyimpan menu akses' };
+ }
+}
\ No newline at end of file
diff --git a/src/app/api/auth/_lib/decrypt.ts b/src/app/api/auth/_lib/decrypt.ts
index e36a5342..fc7893b2 100644
--- a/src/app/api/auth/_lib/decrypt.ts
+++ b/src/app/api/auth/_lib/decrypt.ts
@@ -1,50 +1,32 @@
-/* eslint-disable @typescript-eslint/no-explicit-any */
import { jwtVerify } from "jose";
export async function decrypt({
token,
- encodedKey,
+ jwtSecret,
}: {
token: string;
- encodedKey: string;
-}): Promise | null> {
- if (!token || !encodedKey) {
- console.error("Missing required parameters:", {
- hasToken: !!token,
- hasEncodedKey: !!encodedKey,
- });
- return null;
- }
+ jwtSecret: string;
+}): Promise | null> {
+ if (!token || !jwtSecret) return null;
try {
- const enc = new TextEncoder().encode(encodedKey);
- const { payload } = await jwtVerify(token, enc, {
+ const secret = new TextEncoder().encode(jwtSecret);
+ const { payload } = await jwtVerify(token, secret, {
algorithms: ["HS256"],
});
- if (!payload || !payload.user) {
- console.error("Invalid payload structure:", {
- hasPayload: !!payload,
- hasUser: payload ? !!payload.user : false,
- });
+ if (
+ typeof payload !== "object" ||
+ payload === null ||
+ !("user" in payload) ||
+ typeof payload.user !== "object"
+ ) {
return null;
}
- // Logging untuk debug
- // console.log("Decrypt successful:", {
- // payloadExists: !!payload,
- // userExists: !!payload.user,
- // tokenPreview: token.substring(0, 10) + "...",
- // });
-
- return payload.user as Record;
+ return payload.user as Record;
} catch (error) {
- console.error("Token verification failed:", {
- error,
- tokenLength: token?.length,
- errorName: error instanceof Error ? error.name : "Unknown error",
- errorMessage: error instanceof Error ? error.message : String(error),
- });
+ console.error("JWT Decrypt failed:", error);
return null;
}
-}
+}
\ No newline at end of file
diff --git a/src/app/api/auth/_lib/encrypt.ts b/src/app/api/auth/_lib/encrypt.ts
index cde1ebd9..c17970df 100644
--- a/src/app/api/auth/_lib/encrypt.ts
+++ b/src/app/api/auth/_lib/encrypt.ts
@@ -1,26 +1,23 @@
-/* eslint-disable @typescript-eslint/no-explicit-any */
import { SignJWT } from "jose";
export async function encrypt({
user,
- exp = "7 year",
- encodedKey,
+ exp = "7d",
+ jwtSecret,
}: {
- user: Record;
- exp?: string;
- encodedKey: string;
+ user: Record;
+ exp?: string | number;
+ jwtSecret: string;
}): Promise {
try {
- const enc = new TextEncoder().encode(encodedKey);
+ const secret = new TextEncoder().encode(jwtSecret);
return new SignJWT({ user })
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt()
.setExpirationTime(exp)
- .sign(enc);
+ .sign(secret);
} catch (error) {
- console.error("Gagal mengenkripsi", error);
+ console.error("JWT Encrypt failed:", error);
return null;
}
-}
-
-// wibu:0.2.82
+}
\ No newline at end of file
diff --git a/src/app/api/auth/_lib/session_create.ts b/src/app/api/auth/_lib/session_create.ts
index fffd75cc..121b6b62 100644
--- a/src/app/api/auth/_lib/session_create.ts
+++ b/src/app/api/auth/_lib/session_create.ts
@@ -1,36 +1,65 @@
-/* eslint-disable @typescript-eslint/no-explicit-any */
+// src/app/api/auth/_lib/sessionCreate.ts
import { cookies } from "next/headers";
import { encrypt } from "./encrypt";
+import prisma from "@/lib/prisma";
+import { add } from "date-fns";
export async function sessionCreate({
sessionKey,
- exp = "7 year",
- encodedKey,
+ exp = "30 day",
+ jwtSecret,
user,
+ invalidatePrevious = true, // 🔑 kontrol apakah sesi lama di-nonaktifkan
}: {
sessionKey: string;
exp?: string;
- encodedKey: string;
- user: Record;
+ jwtSecret: string;
+ user: Record & { id: string };
+ invalidatePrevious?: boolean; // default true untuk login, false untuk registrasi
}) {
- const token = await encrypt({
- exp,
- encodedKey,
- user,
+ // ✅ Validasi env vars
+ if (!sessionKey || sessionKey.length === 0) {
+ throw new Error("sessionKey tidak boleh kosong");
+ }
+ if (!jwtSecret || jwtSecret.length < 32) {
+ throw new Error("jwtSecret minimal 32 karakter");
+ }
+
+ const token = await encrypt({ exp, jwtSecret, user });
+ if (!token) {
+ throw new Error("Token generation failed");
+ }
+
+ // ✅ Hitung expiresAt
+ let expiresAt = add(new Date(), { days: 30 });
+ if (exp === "7 day") expiresAt = add(new Date(), { days: 7 });
+
+ // 🔐 Hanya nonaktifkan sesi aktif sebelumnya jika diminta (misal: saat login ulang)
+ if (invalidatePrevious) {
+ await prisma.userSession.updateMany({
+ where: { userId: user.id, active: true },
+ data: { active: false },
+ });
+ }
+
+ // ✅ Simpan sesi baru
+ await prisma.userSession.create({
+ data: {
+ token,
+ userId: user.id,
+ active: true,
+ expiresAt,
+ },
});
- const cookie: any = {
- key: sessionKey,
- value: token,
- options: {
- httpOnly: true,
- sameSite: "lax",
- path: "/",
- },
- };
+ // ✅ Set cookie
+ (await cookies()).set(sessionKey, token, {
+ httpOnly: true,
+ sameSite: "lax",
+ path: "/",
+ secure: process.env.NODE_ENV === "production",
+ maxAge: 30 * 24 * 60 * 60, // 30 hari dalam detik
+ });
- (await cookies()).set(cookie.key, cookie.value, { ...cookie.options });
return token;
-}
-
-// wibu:0.2.82
+}
\ No newline at end of file
diff --git a/src/app/api/auth/_lib/session_delete.ts b/src/app/api/auth/_lib/session_delete.ts
new file mode 100644
index 00000000..7ff111b1
--- /dev/null
+++ b/src/app/api/auth/_lib/session_delete.ts
@@ -0,0 +1,42 @@
+// app/api/auth/_lib/session_delete.ts
+import { cookies } from "next/headers";
+import prisma from "@/lib/prisma";
+
+/**
+ * Hapus session dari database dan cookie
+ */
+export async function sessionDelete({
+ sessionKey,
+ userId,
+}: {
+ sessionKey: string;
+ userId?: string;
+}): Promise {
+ try {
+ const cookieStore = await cookies();
+ const token = cookieStore.get(sessionKey)?.value;
+
+ // Hapus dari database
+ if (token) {
+ const deleted = await prisma.userSession.deleteMany({
+ where: { token },
+ });
+ console.log(`🗑️ Deleted ${deleted.count} session(s) by token`);
+ } else if (userId) {
+ // Fallback: hapus berdasarkan userId
+ const deleted = await prisma.userSession.deleteMany({
+ where: { userId },
+ });
+ console.log(`🗑️ Deleted ${deleted.count} session(s) for user ${userId}`);
+ }
+
+ // Hapus cookie
+ cookieStore.delete(sessionKey);
+ console.log('✅ Session deleted successfully');
+
+ return true;
+ } catch (error) {
+ console.error("❌ Error deleting session:", error);
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/src/app/api/auth/_lib/session_verify.ts b/src/app/api/auth/_lib/session_verify.ts
new file mode 100644
index 00000000..2a934a19
--- /dev/null
+++ b/src/app/api/auth/_lib/session_verify.ts
@@ -0,0 +1,39 @@
+// app/api/auth/_lib/session_verify.ts
+import { cookies } from "next/headers";
+import { decrypt } from "./decrypt";
+import prisma from "@/lib/prisma";
+
+export async function verifySession() {
+ try {
+ const sessionKey = process.env.BASE_SESSION_KEY;
+ const jwtSecret = process.env.BASE_TOKEN_KEY;
+ if (!sessionKey || !jwtSecret) throw new Error('Env tidak lengkap');
+
+ const token = (await cookies()).get(sessionKey)?.value;
+ if (!token) return null;
+
+ const jwtUser = await decrypt({ token, jwtSecret });
+ if (!jwtUser?.id) return null;
+
+ // Cari session di DB berdasarkan token
+ const dbSession = await prisma.userSession.findFirst({
+ where: {
+ token,
+ active: true,
+ expiresAt: { gte: new Date() }
+ },
+ include: { user: true }
+ });
+
+ if (!dbSession) {
+ console.log('⚠️ Session tidak ditemukan di DB');
+ return null;
+ }
+
+ // Don't check isActive here, let the frontend handle it
+ return dbSession.user;
+ } catch (error) {
+ console.warn('Session verification failed:', error);
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/src/app/api/auth/finalize-registration/route.ts b/src/app/api/auth/finalize-registration/route.ts
new file mode 100644
index 00000000..fe50ae08
--- /dev/null
+++ b/src/app/api/auth/finalize-registration/route.ts
@@ -0,0 +1,92 @@
+// src/app/api/auth/finalize-registration/route.ts
+import prisma from "@/lib/prisma";
+import { NextResponse } from "next/server";
+import { sessionCreate } from "../_lib/session_create";
+
+export async function POST(req: Request) {
+ try {
+ const { nomor, username, kodeId } = await req.json();
+ const cleanNomor = nomor.replace(/\D/g, "");
+
+ if (!cleanNomor || !username || !kodeId) {
+ return NextResponse.json(
+ { success: false, message: "Data tidak lengkap" },
+ { status: 400 }
+ );
+ }
+
+ const otpRecord = await prisma.kodeOtp.findUnique({ where: { id: kodeId } });
+ if (!otpRecord?.isActive || otpRecord.nomor !== cleanNomor) {
+ return NextResponse.json(
+ { success: false, message: "OTP tidak valid" },
+ { status: 400 }
+ );
+ }
+
+ if (await prisma.user.findFirst({ where: { username } })) {
+ return NextResponse.json(
+ { success: false, message: "Username sudah digunakan" },
+ { status: 409 }
+ );
+ }
+
+ const defaultRole = await prisma.role.findFirst({
+ where: { name: "ADMIN DESA" },
+ select: { id: true },
+ });
+
+ if (!defaultRole) {
+ return NextResponse.json(
+ { success: false, message: "Role default tidak ditemukan" },
+ { status: 500 }
+ );
+ }
+
+ const newUser = await prisma.user.create({
+ data: {
+ username,
+ nomor,
+ roleId: defaultRole.id,
+ isActive: false,
+ },
+ });
+
+ await prisma.kodeOtp.update({
+ where: { id: kodeId },
+ data: { isActive: false },
+ });
+
+ const token = await sessionCreate({
+ sessionKey: process.env.BASE_SESSION_KEY!,
+ jwtSecret: process.env.BASE_TOKEN_KEY!,
+ exp: "30 day",
+ user: {
+ id: newUser.id,
+ nomor: newUser.nomor,
+ username: newUser.username,
+ roleId: newUser.roleId,
+ isActive: false,
+ },
+ invalidatePrevious: false,
+ });
+
+ // ✅ REDIRECT DARI SERVER — cookie pasti tersedia
+ const response = NextResponse.redirect(new URL('/waiting-room', req.url));
+ response.cookies.set(process.env.BASE_SESSION_KEY!, token, {
+ httpOnly: true,
+ secure: process.env.NODE_ENV === "production",
+ path: "/",
+ maxAge: 30 * 24 * 60 * 60,
+ });
+
+ return response;
+ } catch (error) {
+ console.error("❌ Finalize Registration Error:", error);
+ return NextResponse.json(
+ { success: false, message: "Registrasi gagal" },
+ { status: 500 }
+ );
+ } finally {
+ await prisma.$disconnect();
+ }
+}
\ No newline at end of file
diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts
index dc002998..96333148 100644
--- a/src/app/api/auth/login/route.ts
+++ b/src/app/api/auth/login/route.ts
@@ -1,5 +1,5 @@
+// app/api/auth/login/route.ts
import prisma from "@/lib/prisma";
-
import { NextResponse } from "next/server";
import { randomOTP } from "../_lib/randomOTP";
@@ -12,52 +12,66 @@ export async function POST(req: Request) {
}
try {
- const codeOtp = randomOTP();
- const body = await req.json();
- const { nomor } = body;
- const res = await fetch(
- `https://wa.wibudev.com/code?nom=${nomor}&text=Website Desa Darmasaba - Kode ini bersifat RAHASIA dan JANGAN DI BAGIKAN KEPADA SIAPAPUN, termasuk anggota ataupun Admin lainnya.
- \n
- >> Kode OTP anda: ${codeOtp}.
- `
- );
+ const { nomor } = await req.json();
- const sendWa = await res.json();
-
- if (sendWa.status !== "success")
+ if (!nomor || typeof nomor !== "string") {
return NextResponse.json(
- { success: false, message: "Nomor Whatsapp Tidak Aktif" },
+ { success: false, message: "Nomor tidak valid" },
{ status: 400 }
);
+ }
- const createOtpId = await prisma.kodeOtp.create({
- data: {
- nomor: nomor,
- otp: codeOtp,
- },
+ const existingUser = await prisma.user.findUnique({
+ where: { nomor },
+ select: { id: true, isActive: true },
});
- if (!createOtpId)
- return NextResponse.json(
- { success: false, message: "Gagal mengirim kode OTP" },
- { status: 400 }
- );
+ const isRegistered = !!existingUser;
- return NextResponse.json(
- {
+ if (isRegistered) {
+ // ✅ User terdaftar → kirim OTP
+ const codeOtp = randomOTP();
+ const otpNumber = Number(codeOtp);
+
+ const waMessage = `Website Desa Darmasaba - Kode verifikasi Anda: ${codeOtp}`;
+ const waUrl = `https://wa.wibudev.com/code?nom=${encodeURIComponent(nomor)}&text=${encodeURIComponent(waMessage)}`;
+
+ const res = await fetch(waUrl);
+ const sendWa = await res.json();
+
+ if (sendWa.status !== "success") {
+ return NextResponse.json(
+ { success: false, message: "Gagal mengirim OTP via WhatsApp" },
+ { status: 400 }
+ );
+ }
+
+ const createOtpId = await prisma.kodeOtp.create({
+ data: { nomor, otp: otpNumber, isActive: true },
+ });
+
+ return NextResponse.json({
success: true,
- message: "Kode verifikasi terkirim",
+ message: "Kode verifikasi dikirim",
kodeId: createOtpId.id,
- },
- { status: 200 }
- );
+ isRegistered: true,
+ });
+ } else {
+ // ❌ User belum terdaftar → JANGAN kirim OTP
+ return NextResponse.json({
+ success: true,
+ message: "Nomor belum terdaftar",
+ isRegistered: false,
+ // Tidak ada kodeId
+ });
+ }
} catch (error) {
- console.log("Error Login", error);
+ console.error("Error Login:", error);
return NextResponse.json(
- { success: false, message: "Terjadi masalah saat login" , reason: error as Error },
+ { success: false, message: "Terjadi kesalahan saat login" },
{ status: 500 }
);
} finally {
await prisma.$disconnect();
}
-}
+}
\ No newline at end of file
diff --git a/src/app/api/auth/logout/route.ts b/src/app/api/auth/logout/route.ts
new file mode 100644
index 00000000..b546c0f6
--- /dev/null
+++ b/src/app/api/auth/logout/route.ts
@@ -0,0 +1,35 @@
+// app/api/auth/logout/route.ts
+import { NextResponse } from "next/server";
+import { sessionDelete } from "../_lib/session_delete";
+
+export async function POST() {
+ try {
+ const deleted = await sessionDelete({
+ sessionKey: process.env.BASE_SESSION_KEY!,
+ });
+
+ if (deleted) {
+ return NextResponse.json({
+ success: true,
+ message: "Logout berhasil",
+ });
+ } else {
+ return NextResponse.json(
+ {
+ success: false,
+ message: "Gagal logout",
+ },
+ { status: 500 }
+ );
+ }
+ } catch (error) {
+ console.error("❌ Logout Error:", error);
+ return NextResponse.json(
+ {
+ success: false,
+ message: "Terjadi kesalahan saat logout",
+ },
+ { status: 500 }
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/app/api/auth/me/route.ts b/src/app/api/auth/me/route.ts
new file mode 100644
index 00000000..18165deb
--- /dev/null
+++ b/src/app/api/auth/me/route.ts
@@ -0,0 +1,59 @@
+// src/app/api/auth/me/route.ts
+import { NextResponse } from 'next/server';
+import { verifySession } from '../_lib/session_verify';
+import prisma from '@/lib/prisma';
+
+export async function GET() {
+ try {
+ const sessionUser = await verifySession();
+ if (!sessionUser) {
+ return NextResponse.json(
+ { success: false, message: "Unauthorized", user: null },
+ { status: 401 }
+ );
+ }
+
+ const [dbUser, menuAccess] = await Promise.all([
+ prisma.user.findUnique({
+ where: { id: sessionUser.id },
+ select: {
+ id: true,
+ username: true,
+ nomor: true,
+ roleId: true, // STRING!
+ isActive: true, // BOOLEAN!
+ },
+ }),
+ prisma.userMenuAccess.findMany({
+ where: { userId: sessionUser.id },
+ select: { menuId: true },
+ }),
+ ]);
+
+ if (!dbUser) {
+ return NextResponse.json(
+ { success: false, message: "User not found", user: null },
+ { status: 404 }
+ );
+ }
+
+ return NextResponse.json({
+ success: true,
+ user: {
+ id: dbUser.id,
+ name: dbUser.username,
+ username: dbUser.username,
+ nomor: dbUser.nomor,
+ roleId: dbUser.roleId, // STRING!
+ isActive: dbUser.isActive, // BOOLEAN!
+ menuIds: menuAccess.map(m => m.menuId),
+ },
+ });
+ } catch (error) {
+ console.error("❌ Error in /api/auth/me:", error);
+ return NextResponse.json(
+ { success: false, message: "Internal server error", user: null },
+ { status: 500 }
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/app/api/auth/otp-data/route.ts b/src/app/api/auth/otp-data/route.ts
new file mode 100644
index 00000000..81b1c8c9
--- /dev/null
+++ b/src/app/api/auth/otp-data/route.ts
@@ -0,0 +1,42 @@
+// app/api/auth/otp-data/route.ts
+import { NextResponse } from "next/server";
+import prisma from "@/lib/prisma";
+
+export async function GET(request: Request) {
+ try {
+ const { searchParams } = new URL(request.url);
+ const kodeId = searchParams.get("kodeId");
+
+ if (!kodeId) {
+ return NextResponse.json(
+ { success: false, message: "Kode ID tidak diberikan" },
+ { status: 400 }
+ );
+ }
+
+ const otpRecord = await prisma.kodeOtp.findUnique({
+ where: { id: kodeId },
+ select: { nomor: true, isActive: true },
+ });
+
+ if (!otpRecord || !otpRecord.isActive) {
+ return NextResponse.json(
+ { success: false, message: "Kode verifikasi tidak valid atau sudah digunakan" },
+ { status: 400 }
+ );
+ }
+
+ return NextResponse.json({
+ success: true,
+ data: { nomor: otpRecord.nomor },
+ });
+ } catch (error) {
+ console.error("❌ Gagal mengambil data OTP:", error);
+ return NextResponse.json(
+ { success: false, message: "Terjadi kesalahan internal" },
+ { status: 500 }
+ );
+ } finally {
+ await prisma.$disconnect();
+ }
+}
\ No newline at end of file
diff --git a/src/app/api/auth/refresh-session/route.ts b/src/app/api/auth/refresh-session/route.ts
new file mode 100644
index 00000000..16baf54c
--- /dev/null
+++ b/src/app/api/auth/refresh-session/route.ts
@@ -0,0 +1,55 @@
+import { NextResponse } from 'next/server';
+import { verifySession } from '../_lib/session_verify';
+import { sessionCreate } from '../_lib/session_create';
+import prisma from '@/lib/prisma';
+
+export async function POST() {
+ try {
+ const sessionUser = await verifySession();
+ if (!sessionUser) {
+ return NextResponse.json(
+ { success: false, message: "Unauthorized" },
+ { status: 401 }
+ );
+ }
+
+ // Get fresh user data
+ const user = await prisma.user.findUnique({
+ where: { id: sessionUser.id },
+ select: {
+ id: true,
+ username: true,
+ roleId: true,
+ isActive: true,
+ },
+ });
+
+ if (!user) {
+ return NextResponse.json(
+ { success: false, message: "User not found" },
+ { status: 404 }
+ );
+ }
+
+ // Create new session with updated data
+ await sessionCreate({
+ sessionKey: process.env.BASE_SESSION_KEY!,
+ jwtSecret: process.env.BASE_TOKEN_KEY!,
+ user: {
+ id: user.id,
+ username: user.username,
+ roleId: user.roleId,
+ isActive: user.isActive,
+ },
+ invalidatePrevious: false, // Keep existing sessions
+ });
+
+ return NextResponse.json({ success: true });
+ } catch (error) {
+ console.error('Error refreshing session:', error);
+ return NextResponse.json(
+ { success: false, message: "Internal server error" },
+ { status: 500 }
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/app/api/auth/register/route.ts b/src/app/api/auth/register/route.ts
index eb7af548..ff87c8e3 100644
--- a/src/app/api/auth/register/route.ts
+++ b/src/app/api/auth/register/route.ts
@@ -1,62 +1,51 @@
-import prisma from "@/lib/prisma";
-import { NextResponse } from "next/server";
+import { NextResponse } from 'next/server';
+import prisma from '@/lib/prisma';
+import { randomOTP } from '../_lib/randomOTP'; // pastikan ada
export async function POST(req: Request) {
- if (req.method !== "POST") {
- return NextResponse.json(
- { success: false, message: "Method Not Allowed" },
- { status: 405 }
- );
- }
-
try {
- const { data } = await req.json();
+ const { username, nomor } = await req.json();
- const cekUsername = await prisma.user.findUnique({
- where: {
- username: data.username,
- nomor: data.nomor,
- },
+ if (!username || !nomor) {
+ return NextResponse.json({ success: false, message: 'Data tidak lengkap' }, { status: 400 });
+ }
+
+ // Cek duplikat
+ if (await prisma.user.findUnique({ where: { nomor } })) {
+ return NextResponse.json({ success: false, message: 'Nomor sudah terdaftar' }, { status: 409 });
+ }
+ if (await prisma.user.findFirst({ where: { username } })) {
+ return NextResponse.json({ success: false, message: 'Username sudah digunakan' }, { status: 409 });
+ }
+
+ // ✅ Generate dan kirim OTP
+ const codeOtp = randomOTP();
+ const otpNumber = Number(codeOtp);
+
+ const waMessage = `Website Desa Darmasaba - Kode verifikasi Anda: ${codeOtp}`;
+ const waUrl = `https://wa.wibudev.com/code?nom=${encodeURIComponent(nomor)}&text=${encodeURIComponent(waMessage)}`;
+ const waRes = await fetch(waUrl);
+ const waData = await waRes.json();
+
+ if (waData.status !== "success") {
+ return NextResponse.json({ success: false, message: 'Gagal mengirim OTP via WhatsApp' }, { status: 400 });
+ }
+
+ // ✅ Simpan OTP ke database
+ const otpRecord = await prisma.kodeOtp.create({
+ data: { nomor, otp: otpNumber, isActive: true }
});
- if (cekUsername)
- return NextResponse.json({
- success: false,
- message: "Username sudah digunakan",
- });
-
- const createUser = await prisma.user.create({
- data: {
- username: data.username,
- nomor: data.nomor,
- },
+ // ✅ Kembalikan kodeId (jangan buat user di sini!)
+ return NextResponse.json({
+ success: true,
+ message: 'Kode verifikasi dikirim',
+ kodeId: otpRecord.id,
});
-
- if (!createUser)
- return NextResponse.json(
- { success: false, message: "Gagal Registrasi" },
- { status: 500 }
- );
-
- return NextResponse.json(
- {
- success: true,
- message: "Registrasi Berhasil, Anda Sedang Login",
- // data: createUser,
- },
- { status: 201 }
- );
} catch (error) {
- console.error("Error registrasi:", error);
- return NextResponse.json(
- {
- success: false,
- message: "Maaf, Terjadi Keselahan",
- reason: (error as Error).message,
- },
- { status: 500 }
- );
+ console.error('Register OTP Error:', error);
+ return NextResponse.json({ success: false, message: 'Gagal mengirim OTP' }, { status: 500 });
} finally {
await prisma.$disconnect();
}
-}
+}
\ No newline at end of file
diff --git a/src/app/api/auth/resend/route.ts b/src/app/api/auth/resend/route.ts
new file mode 100644
index 00000000..dafb5e44
--- /dev/null
+++ b/src/app/api/auth/resend/route.ts
@@ -0,0 +1,58 @@
+// src/app/api/auth/resend-otp/route.ts
+import prisma from "@/lib/prisma";
+
+import { NextResponse } from "next/server";
+import { randomOTP } from "../_lib/randomOTP";
+
+export async function POST(req: Request) {
+ try {
+ const { nomor } = await req.json();
+
+ if (!nomor || typeof nomor !== 'string') {
+ return NextResponse.json(
+ { success: false, message: "Nomor tidak valid" },
+ { status: 400 }
+ );
+ }
+
+ const codeOtp = randomOTP();
+ const otpNumber = Number(codeOtp);
+
+ // Kirim OTP via WhatsApp
+ const waMessage = `Kode verifikasi Anda: ${codeOtp}`;
+ const waUrl = `https://wa.wibudev.com/code?nom=${encodeURIComponent(nomor)}&text=${encodeURIComponent(waMessage)}`;
+ const waRes = await fetch(waUrl);
+ const waData = await waRes.json();
+
+ if (waData.status !== "success") {
+ return NextResponse.json(
+ { success: false, message: "Gagal mengirim OTP via WhatsApp" },
+ { status: 400 }
+ );
+ }
+
+ // Simpan OTP ke database
+ const otpRecord = await prisma.kodeOtp.create({
+ data: {
+ nomor,
+ otp: otpNumber,
+ isActive: true,
+ },
+ });
+
+ return NextResponse.json({
+ success: true,
+ message: "OTP baru dikirim",
+ kodeId: otpRecord.id,
+ });
+
+ } catch (error) {
+ console.error("Error Resend OTP:", error);
+ return NextResponse.json(
+ { success: false, message: "Gagal mengirim ulang OTP" },
+ { status: 500 }
+ );
+ } finally {
+ await prisma.$disconnect();
+ }
+}
\ No newline at end of file
diff --git a/src/app/api/auth/send-otp-register/route.ts b/src/app/api/auth/send-otp-register/route.ts
new file mode 100644
index 00000000..501ea316
--- /dev/null
+++ b/src/app/api/auth/send-otp-register/route.ts
@@ -0,0 +1,51 @@
+import prisma from "@/lib/prisma";
+import { NextResponse } from "next/server";
+import { randomOTP } from "../_lib/randomOTP";
+
+export async function POST(req: Request) {
+ try {
+ const { username, nomor } = await req.json();
+
+ if (!username || !nomor) {
+ return NextResponse.json({ success: false, message: 'Data tidak lengkap' }, { status: 400 });
+ }
+
+ // Cek duplikat
+ if (await prisma.user.findUnique({ where: { nomor } })) {
+ return NextResponse.json({ success: false, message: 'Nomor sudah terdaftar' }, { status: 409 });
+ }
+ if (await prisma.user.findFirst({ where: { username } })) {
+ return NextResponse.json({ success: false, message: 'Username sudah digunakan' }, { status: 409 });
+ }
+
+ // Generate OTP
+ const codeOtp = randomOTP();
+ const otpNumber = Number(codeOtp);
+
+ // Kirim WA
+ const waUrl = `https://wa.wibudev.com/code?nom=${encodeURIComponent(nomor)}&text=Website Desa Darmasaba - Kode verifikasi Anda: ${codeOtp}`;
+ const res = await fetch(waUrl);
+ const sendWa = await res.json();
+
+ if (sendWa.status !== "success") {
+ return NextResponse.json({ success: false, message: 'Gagal mengirim OTP' }, { status: 400 });
+ }
+
+ // Simpan OTP
+ const otpRecord = await prisma.kodeOtp.create({
+ data: { nomor, otp: otpNumber, isActive: true }
+ });
+
+ return NextResponse.json({
+ success: true,
+ message: 'Kode verifikasi dikirim',
+ kodeId: otpRecord.id,
+ nomor,
+ });
+ } catch (error) {
+ console.error('Send OTP for Register Error:', error);
+ return NextResponse.json({ success: false, message: 'Gagal mengirim OTP' }, { status: 500 });
+ } finally {
+ await prisma.$disconnect();
+ }
+}
\ No newline at end of file
diff --git a/src/app/api/auth/validasi/route.ts b/src/app/api/auth/validasi/route.ts
new file mode 100644
index 00000000..2e23908a
--- /dev/null
+++ b/src/app/api/auth/validasi/route.ts
@@ -0,0 +1,78 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import prisma from "@/lib/prisma";
+import { NextResponse } from "next/server";
+import { sessionCreate } from "../_lib/session_create";
+
+export async function POST(req: Request) {
+ if (req.method !== "POST") {
+ return NextResponse.json(
+ { success: false, message: "Method Not Allowed" },
+ { status: 405 }
+ );
+ }
+
+ try {
+ const { nomor } = await req.json();
+ const dataUser = await prisma.user.findUnique({
+ where: {
+ nomor: nomor,
+ },
+ select: {
+ id: true,
+ nomor: true,
+ username: true,
+ roleId: true,
+ },
+ });
+
+ if (dataUser == null)
+ return NextResponse.json(
+ { success: false, message: "Nomor Belum Terdaftar" },
+ { status: 200 }
+ );
+
+ const token = await sessionCreate({
+ sessionKey: process.env.BASE_SESSION_KEY!,
+ jwtSecret: process.env.BASE_TOKEN_KEY!,
+ user: dataUser as any,
+ });
+
+ if (!token) {
+ return NextResponse.json(
+ { success: false, message: "Gagal membuat session" },
+ { status: 500 }
+ );
+ }
+ // Buat response dengan token dalam cookie
+ const response = NextResponse.json(
+ {
+ success: true,
+ message: "Berhasil Login",
+ roleId: dataUser.roleId,
+ },
+ { status: 200 }
+ );
+
+ // Set cookie dengan token yang sudah dipastikan tidak null
+ response.cookies.set(process.env.NEXT_PUBLIC_BASE_SESSION_KEY!, token, {
+ path: "/",
+ sameSite: "lax",
+ secure: process.env.NODE_ENV === "production",
+ maxAge: 30 * 24 * 60 * 60, // 30 hari dalam detik (1 bulan)
+ });
+
+ return response;
+ } catch (error) {
+ console.error("API Error or Server Error", error);
+ return NextResponse.json(
+ {
+ success: false,
+ message: "Maaf, Terjadi Keselahan",
+ reason: (error as Error).message,
+ },
+ { status: 500 }
+ );
+ } finally {
+ await prisma.$disconnect();
+ }
+}
diff --git a/src/app/api/auth/verify-otp-login/route.ts b/src/app/api/auth/verify-otp-login/route.ts
new file mode 100644
index 00000000..bc468297
--- /dev/null
+++ b/src/app/api/auth/verify-otp-login/route.ts
@@ -0,0 +1,101 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+// src/app/api/auth/verify-otp-login/route.ts
+import prisma from "@/lib/prisma";
+import { NextResponse } from "next/server";
+import { sessionCreate } from "../_lib/session_create";
+
+export async function POST(req: Request) {
+ try {
+ const { nomor, otp, kodeId } = await req.json();
+
+ if (!nomor || !otp || !kodeId) {
+ return NextResponse.json(
+ { success: false, message: "Data tidak lengkap" },
+ { status: 400 }
+ );
+ }
+
+ const otpRecord = await prisma.kodeOtp.findUnique({ where: { id: kodeId } });
+ if (!otpRecord || !otpRecord.isActive || otpRecord.nomor !== nomor) {
+ return NextResponse.json(
+ { success: false, message: "Kode verifikasi tidak valid" },
+ { status: 400 }
+ );
+ }
+
+ const receivedOtp = Number(otp);
+ if (isNaN(receivedOtp) || otpRecord.otp !== receivedOtp) {
+ return NextResponse.json(
+ { success: false, message: "Kode OTP salah" },
+ { status: 400 }
+ );
+ }
+
+ // 🔍 CARI USER — JANGAN BUAT BARU!
+ const user = await prisma.user.findUnique({
+ where: { nomor },
+ select: { id: true, nomor: true, username: true, roleId: true, isActive: true },
+ });
+
+ if (!user) {
+ // ❌ Nomor belum terdaftar → suruh registrasi
+ return NextResponse.json(
+ { success: false, message: "Akun tidak ditemukan. Silakan registrasi terlebih dahulu." },
+ { status: 404 }
+ );
+ }
+
+ // ✅ Buat sesi
+ const token = await sessionCreate({
+ sessionKey: process.env.BASE_SESSION_KEY!,
+ jwtSecret: process.env.BASE_TOKEN_KEY!,
+ exp: "30 day",
+ user: {
+ id: user.id,
+ nomor: user.nomor,
+ username: user.username,
+ roleId: user.roleId,
+ isActive: user.isActive,
+ },
+ });
+
+ await prisma.$transaction([
+ prisma.kodeOtp.update({ where: { id: kodeId }, data: { isActive: false } }),
+ prisma.user.update({ where: { id: user.id }, data: { lastLogin: new Date() } }),
+ ]);
+
+ const response = NextResponse.json({
+ success: true,
+ message: user.isActive ? "Berhasil login" : "Menunggu persetujuan",
+ user: {
+ id: user.id,
+ name: user.username,
+ roleId: user.roleId,
+ isActive: user.isActive,
+ },
+ });
+
+ response.cookies.set(process.env.BASE_SESSION_KEY!, token, {
+ httpOnly: true,
+ secure: process.env.NODE_ENV === "production",
+ path: "/",
+ maxAge: 30 * 24 * 60 * 60,
+ });
+
+ return response;
+ } catch (error: any) {
+ console.error("❌ Verify OTP Login Error:", error);
+ if (error.message.includes("sessionKey") || error.message.includes("jwtSecret")) {
+ return NextResponse.json(
+ { success: false, message: "Konfigurasi server tidak lengkap" },
+ { status: 500 }
+ );
+ }
+ return NextResponse.json(
+ { success: false, message: "Terjadi kesalahan saat login" },
+ { status: 500 }
+ );
+ } finally {
+ await prisma.$disconnect();
+ }
+}
\ No newline at end of file
diff --git a/src/app/api/auth/verify-otp-register/route.ts b/src/app/api/auth/verify-otp-register/route.ts
new file mode 100644
index 00000000..66cbf2d6
--- /dev/null
+++ b/src/app/api/auth/verify-otp-register/route.ts
@@ -0,0 +1,61 @@
+// src/app/api/auth/verify-otp-register/route.ts
+import prisma from "@/lib/prisma";
+import { NextResponse } from "next/server";
+
+export async function POST(req: Request) {
+ try {
+ const { nomor, otp, kodeId } = await req.json();
+
+ if (!nomor || !otp || !kodeId) {
+ return NextResponse.json(
+ { success: false, message: "Data tidak lengkap" },
+ { status: 400 }
+ );
+ }
+
+ const otpRecord = await prisma.kodeOtp.findUnique({
+ where: { id: kodeId },
+ });
+
+ if (!otpRecord || !otpRecord.isActive) {
+ return NextResponse.json(
+ { success: false, message: "Kode verifikasi tidak valid atau sudah kadaluarsa" },
+ { status: 400 }
+ );
+ }
+
+ if (otpRecord.nomor !== nomor) {
+ return NextResponse.json(
+ { success: false, message: "Nomor tidak sesuai dengan kode verifikasi" },
+ { status: 400 }
+ );
+ }
+
+ const receivedOtp = Number(otp);
+ if (isNaN(receivedOtp) || otpRecord.otp !== receivedOtp) {
+ return NextResponse.json(
+ { success: false, message: "Kode OTP salah" },
+ { status: 400 }
+ );
+ }
+
+ // ✅ Hanya validasi — jangan update isActive!
+ return NextResponse.json({
+ success: true,
+ message: "OTP valid. Lanjutkan ke finalisasi registrasi.",
+ data: {
+ nomor,
+ kodeId,
+ },
+ });
+
+ } catch (error) {
+ console.error("❌ Verify OTP Register Error:", error);
+ return NextResponse.json(
+ { success: false, message: "Terjadi kesalahan saat verifikasi OTP" },
+ { status: 500 }
+ );
+ } finally {
+ await prisma.$disconnect();
+ }
+}
\ No newline at end of file
diff --git a/src/app/darmasaba/(pages)/ppid/indeks-kepuasan-masyarakat/page.tsx b/src/app/darmasaba/(pages)/ppid/indeks-kepuasan-masyarakat/page.tsx
index f5ac3373..8e8b40f7 100644
--- a/src/app/darmasaba/(pages)/ppid/indeks-kepuasan-masyarakat/page.tsx
+++ b/src/app/darmasaba/(pages)/ppid/indeks-kepuasan-masyarakat/page.tsx
@@ -41,7 +41,7 @@ const state = useProxy(indeksKepuasanState.responden);
indeksKepuasanState.jenisKelaminResponden.findMany.load()
indeksKepuasanState.pilihanRatingResponden.findMany.load()
indeksKepuasanState.kelompokUmurResponden.findMany.load()
- })
+ },[])
const handleSubmit = async () => {
try {
diff --git a/src/app/darmasaba/_com/main-page/kepuasan/index.tsx b/src/app/darmasaba/_com/main-page/kepuasan/index.tsx
index 9d96292e..f7480598 100644
--- a/src/app/darmasaba/_com/main-page/kepuasan/index.tsx
+++ b/src/app/darmasaba/_com/main-page/kepuasan/index.tsx
@@ -41,7 +41,7 @@ function Kepuasan() {
indeksKepuasanState.jenisKelaminResponden.findMany.load()
indeksKepuasanState.pilihanRatingResponden.findMany.load()
indeksKepuasanState.kelompokUmurResponden.findMany.load()
- })
+ },[])
const handleSubmit = async () => {
try {
diff --git a/src/app/darmasaba/_com/main-page/landing-page/index.tsx b/src/app/darmasaba/_com/main-page/landing-page/index.tsx
index d2e40772..563f1dc1 100644
--- a/src/app/darmasaba/_com/main-page/landing-page/index.tsx
+++ b/src/app/darmasaba/_com/main-page/landing-page/index.tsx
@@ -59,6 +59,35 @@ const getWorkStatus = (day: string, currentTime: string): { status: string; mess
: { status: "Tutup", message: "08:00 - 17:00" };
};
+// Skeleton component untuk Social Media
+const SosmedSkeleton = () => (
+
+ {[1, 2, 3, 4].map((i) => (
+
+ ))}
+
+);
+
+// Skeleton component untuk Profile
+const ProfileSkeleton = () => (
+
+
+
+
+
+
+
+
+
+);
+
function LandingPage() {
const [socialMedia, setSocialMedia] = useState<
Prisma.MediaSosialGetPayload<{ include: { image: true } }>[]
@@ -66,9 +95,8 @@ function LandingPage() {
const [profile, setProfile] = useState<
Prisma.PejabatDesaGetPayload<{ include: { image: true } }> | null
>(null);
- const [isLoading, setIsLoading] = useState(true);
-
-
+ const [isLoadingSosmed, setIsLoadingSosmed] = useState(true);
+ const [isLoadingProfile, setIsLoadingProfile] = useState(true);
useEffect(() => {
const fetchSocialMedia = async () => {
@@ -86,7 +114,7 @@ function LandingPage() {
} catch {
setSocialMedia([]);
} finally {
- setIsLoading(false);
+ setIsLoadingSosmed(false);
}
};
@@ -98,6 +126,8 @@ function LandingPage() {
setProfile(result.data || null);
} catch {
setProfile(null);
+ } finally {
+ setIsLoadingProfile(false);
}
};
@@ -189,8 +219,8 @@ function LandingPage() {
- {isLoading ? (
-
+ {isLoadingSosmed ? (
+
) : socialMedia.length > 0 ? (
) : (
@@ -207,19 +237,27 @@ function LandingPage() {
- {isLoading ? (
-
+ {isLoadingProfile ? (
+
) : profile ? (
) : (
-
- Informasi profil belum tersedia
-
+
+
+ Informasi profil belum tersedia
+
+
)}
-
);
}
-export default LandingPage;
+export default LandingPage;
\ No newline at end of file
diff --git a/src/app/darmasaba/page.tsx b/src/app/darmasaba/page.tsx
index 36e652c3..52cd29e9 100644
--- a/src/app/darmasaba/page.tsx
+++ b/src/app/darmasaba/page.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import DesaAntiKorupsi from "@/app/darmasaba/_com/main-page/desaantikorupsi";
import Kepuasan from "@/app/darmasaba/_com/main-page/kepuasan";
@@ -13,32 +14,34 @@ import Apbdes from "./_com/main-page/apbdes";
import Prestasi from "./_com/main-page/prestasi";
import ScrollToTopButton from "./_com/scrollToTopButton";
-import NewsReaderLanding from "./_com/NewsReaderalanding";
-import ModernNewsNotification from "./_com/ModernNeewsNotification";
-import { useMemo } from "react";
-import { useProxy } from "valtio/utils";
+import { useEffect, useMemo } from "react";
+import { useSnapshot } from "valtio";
import stateDashboardBerita from "../admin/(dashboard)/_state/desa/berita";
import stateDesaPengumuman from "../admin/(dashboard)/_state/desa/pengumuman";
-import { useEffect } from "react";
+import ModernNewsNotification from "./_com/ModernNeewsNotification";
+import NewsReaderLanding from "./_com/NewsReaderalanding";
export default function Page() {
- const featured = useProxy(stateDashboardBerita.berita.findFirst);
+ const snap1 = useSnapshot(stateDashboardBerita.berita.findFirst);
+ const snap2 = useSnapshot(stateDesaPengumuman.pengumuman.findFirst);
+
+ const featured = snap1;
+ const pengumuman = snap2;
const loadingFeatured = featured.loading;
- const pengumuman = useProxy(stateDesaPengumuman.pengumuman.findFirst);
const loadingPengumuman = pengumuman.loading;
useEffect(() => {
if (!featured.data && !loadingFeatured) {
stateDashboardBerita.berita.findFirst.load();
}
- }, [featured.data, loadingFeatured]);
+ }, []);
useEffect(() => {
if (!pengumuman.data && !loadingPengumuman) {
stateDesaPengumuman.pengumuman.findFirst.load();
}
- }, [pengumuman.data, loadingPengumuman]);
+ }, []);
const newsData = useMemo(() => {
diff --git a/src/app/waiting-room/page.tsx b/src/app/waiting-room/page.tsx
new file mode 100644
index 00000000..a6ec5c74
--- /dev/null
+++ b/src/app/waiting-room/page.tsx
@@ -0,0 +1,193 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+'use client';
+
+import colors from '@/con/colors';
+import {
+ Button,
+ Center,
+ Loader,
+ Paper,
+ Stack,
+ Text,
+ Title,
+} from '@mantine/core';
+import { useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { authStore } from '@/store/authStore'; // ✅ integrasi authStore
+
+async function fetchUser() {
+ const res = await fetch('/api/auth/me');
+ if (!res.ok) {
+ const text = await res.text();
+ throw new Error(`HTTP ${res.status}: ${text}`);
+ }
+ return res.json();
+}
+
+export default function WaitingRoom() {
+ const router = useRouter();
+ const [user, setUser] = useState(null);
+ const [error, setError] = useState(null);
+ const [isRedirecting, setIsRedirecting] = useState(false);
+ const [retryCount, setRetryCount] = useState(0);
+ const MAX_RETRIES = 2;
+
+ useEffect(() => {
+ let isMounted = true;
+ let interval: ReturnType;
+
+ const poll = async () => {
+ if (isRedirecting || !isMounted) return;
+
+ try {
+ const data = await fetchUser();
+ if (!isMounted) return;
+
+ const currentUser = data.user;
+ setUser(currentUser);
+
+ // ✅ Update authStore
+ if (currentUser) {
+ authStore.setUser({
+ id: currentUser.id,
+ name: currentUser.name,
+ roleId: Number(currentUser.roleId),
+ menuIds: currentUser.menuIds || null,
+ });
+ }
+
+ // In the poll function
+ if (currentUser?.isActive === true) {
+ setIsRedirecting(true);
+ clearInterval(interval);
+
+ // Update authStore with the current user data
+ authStore.setUser({
+ id: currentUser.id,
+ name: currentUser.name || 'User',
+ roleId: Number(currentUser.roleId),
+ menuIds: currentUser.menuIds || null,
+ isActive: true
+ });
+
+ // Clean up storage
+ localStorage.removeItem('auth_kodeId');
+ localStorage.removeItem('auth_nomor');
+ localStorage.removeItem('auth_username');
+
+ // Force a session refresh
+ try {
+ const res = await fetch('/api/auth/refresh-session', {
+ method: 'POST',
+ credentials: 'include'
+ });
+
+ if (res.ok) {
+ // Redirect based on role
+ let redirectPath = '/admin';
+ switch (String(currentUser.roleId)) {
+ case "0": case "1": case "2":
+ redirectPath = '/admin/landing-page/profil/program-inovasi';
+ break;
+ case "3":
+ redirectPath = '/admin/kesehatan/posyandu';
+ break;
+ case "4":
+ redirectPath = '/admin/pendidikan/info-sekolah/jenjang-pendidikan';
+ break;
+ }
+ window.location.href = redirectPath; // Use window.location to force full page reload
+ }
+ } catch (error) {
+ console.error('Error refreshing session:', error);
+ router.refresh(); // Fallback to client-side refresh
+ }
+ }
+ } catch (err: any) {
+ if (!isMounted) return;
+
+ if (err.message.includes('401')) {
+ if (retryCount < MAX_RETRIES) {
+ setRetryCount((prev) => prev + 1);
+ setTimeout(() => {
+ if (isMounted) interval = setInterval(poll, 3000);
+ }, 800);
+ } else {
+ setError('Sesi tidak valid. Silakan login ulang.');
+ clearInterval(interval);
+ authStore.setUser(null); // ✅ clear sesi
+ }
+ } else {
+ console.error('Error polling:', err);
+ }
+ }
+ };
+
+ interval = setInterval(poll, 3000);
+ return () => {
+ isMounted = false;
+ if (interval) clearInterval(interval);
+ };
+ }, [router, isRedirecting, retryCount]);
+
+ // ✅ UI Error
+ if (error) {
+ return (
+
+
+
+
+ Sesi Tidak Valid
+
+ {error}
+ router.push('/login')}>
+ Login Ulang
+
+
+
+
+ );
+ }
+
+ // ✅ UI Redirecting
+ if (isRedirecting) {
+ return (
+
+
+
+
+ Akun Disetujui! ✅
+
+
+ Mengalihkan ke dashboard...
+
+
+
+
+
+ );
+ }
+
+ // ✅ UI Default (MENUNGGU) — INI YANG KAMU HILANGKAN!
+ return (
+
+
+
+
+ Menunggu Persetujuan
+
+
+ Akun Anda sedang dalam proses verifikasi oleh Superadmin.
+
+
+ Nomor: {user?.nomor || '...'}
+
+
+
+ Jangan tutup halaman ini. Anda akan dialihkan otomatis setelah disetujui.
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/middleware.ts b/src/middleware.ts
index 3a6f0da3..0f45a9c2 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -1,46 +1,98 @@
-// app/middleware.js
-import { NextResponse, NextRequest } from 'next/server';
+// // app/middleware.js
+// import { NextResponse, NextRequest } from 'next/server';
-// Daftar route yang diizinkan tanpa login (public routes)
-const publicRoutes = [
- '/*', // Home page
- '/about', // About page
- '/public/*', // Wildcard untuk semua route di bawah /public
- '/login', // Halaman login
+// // Daftar route yang diizinkan tanpa login (public routes)
+// const publicRoutes = [
+// '/*', // Home page
+// '/about', // About page
+// '/public/*', // Wildcard untuk semua route di bawah /public
+// '/login', // Halaman login
+// ];
+
+// // Fungsi untuk memeriksa apakah route saat ini adalah route publik
+// function isPublicRoute(pathname: string) {
+// return publicRoutes.some((route) => {
+// // Jika route mengandung wildcard (*), gunakan regex untuk mencocokkan
+// if (route.endsWith('*')) {
+// const baseRoute = route.replace('*', ''); // Hapus wildcard
+// return pathname.startsWith(baseRoute); // Cocokkan dengan pathname
+// }
+// return pathname === route; // Cocokkan exact path
+// });
+// }
+
+// export function middleware(request: NextRequest) {
+// const { pathname } = request.nextUrl;
+
+// // Jika route adalah public, izinkan akses
+// if (isPublicRoute(pathname)) {
+// return NextResponse.next();
+// }
+
+// // Jika bukan public route, periksa apakah pengguna sudah login
+// const isLoggedIn = request.cookies.get('darmasaba-auth-token'); // Contoh: cek cookie auth-token
+// if (!isLoggedIn) {
+// // Redirect ke halaman login jika belum login
+// return NextResponse.redirect(new URL('/login', request.url));
+// }
+
+// // Jika sudah login, izinkan akses
+// return NextResponse.next();
+// }
+
+// // Konfigurasi untuk menentukan path mana yang akan dijalankan middleware
+// export const config = {
+// matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'], // Jalankan middleware untuk semua route kecuali file statis
+// };
+
+/* eslint-disable @typescript-eslint/no-explicit-any */
+// src/app/admin/middleware.ts
+import { NextResponse } from 'next/server';
+import type { NextRequest } from 'next/server';
+import { jwtVerify } from 'jose';
+
+// Route publik di dalam /admin (boleh diakses tanpa login penuh)
+const PUBLIC_ADMIN_ROUTES = [
+ '/admin/login',
+ '/admin/registrasi',
+ '/admin/validasi',
+ '/admin/waiting-room',
];
-// Fungsi untuk memeriksa apakah route saat ini adalah route publik
-function isPublicRoute(pathname: string) {
- return publicRoutes.some((route) => {
- // Jika route mengandung wildcard (*), gunakan regex untuk mencocokkan
- if (route.endsWith('*')) {
- const baseRoute = route.replace('*', ''); // Hapus wildcard
- return pathname.startsWith(baseRoute); // Cocokkan dengan pathname
- }
- return pathname === route; // Cocokkan exact path
- });
-}
+export async function middleware(request: NextRequest) {
+ const path = request.nextUrl.pathname;
-export function middleware(request: NextRequest) {
- const { pathname } = request.nextUrl;
-
- // Jika route adalah public, izinkan akses
- if (isPublicRoute(pathname)) {
- return NextResponse.next();
- }
-
- // Jika bukan public route, periksa apakah pengguna sudah login
- const isLoggedIn = request.cookies.get('darmasaba-auth-token'); // Contoh: cek cookie auth-token
- if (!isLoggedIn) {
- // Redirect ke halaman login jika belum login
- return NextResponse.redirect(new URL('/login', request.url));
- }
-
- // Jika sudah login, izinkan akses
+ // Izinkan akses ke route publik di /admin
+ if (PUBLIC_ADMIN_ROUTES.some(route => path.startsWith(route))) {
return NextResponse.next();
+ }
+
+ // Ambil token dari cookie
+ const token = request.cookies.get(process.env.BASE_SESSION_KEY!)?.value;
+ if (!token) {
+ return NextResponse.redirect(new URL('/login', request.url));
+ }
+
+ try {
+ // Verifikasi JWT
+ const secret = new TextEncoder().encode(process.env.BASE_TOKEN_KEY!);
+ const { payload } = await jwtVerify(token, secret);
+ const user = (payload as any).user;
+
+ // Cek apakah user aktif
+ if (!user || !user.isActive) {
+ return NextResponse.redirect(new URL('/login', request.url));
+ }
+
+ // ✅ User valid → izinkan akses
+ return NextResponse.next();
+ } catch (error) {
+ console.error('Middleware auth error:', error);
+ return NextResponse.redirect(new URL('/login', request.url));
+ }
}
-// Konfigurasi untuk menentukan path mana yang akan dijalankan middleware
+// Hanya berlaku untuk /admin/*
export const config = {
- matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'], // Jalankan middleware untuk semua route kecuali file statis
+ matcher: ['/admin/:path*'],
};
\ No newline at end of file
diff --git a/src/store/authStore.ts b/src/store/authStore.ts
new file mode 100644
index 00000000..f597367e
--- /dev/null
+++ b/src/store/authStore.ts
@@ -0,0 +1,20 @@
+// src/store/authStore.ts
+import { proxy } from 'valtio';
+
+export type User = {
+ id: string;
+ name: string;
+ roleId: number;
+ menuIds?: string[] | null; // ✅ Pastikan pakai `string[]`
+ isActive?: boolean;
+};
+
+export const authStore = proxy<{
+ user: User | null;
+ setUser: (user: User | null) => void;
+}>({
+ user: null,
+ setUser(user) {
+ authStore.user = user;
+ },
+});
\ No newline at end of file