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 ( - + - - - + + + - + <Title ta="center" order={2} fw="bold" c={colors['blue-button']}> Login
- + Logo
- - {/* - Masuk Untuk Akses Admin - setUsername(e.target.value)} - required - /> - */} + { - setPhone(val); - }} + value={phone} + onChange={(val) => setPhone(val)} /> - {isError ? ( - toast.error("Masukan nomor telepon anda") - ) : ( - "" - )} - + - - Belum punya akun? - -
@@ -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 ( - + - - - - + <Stack justify="center" align="center" h="80vh"> + <Paper p="xl" radius="md" bg={colors['white-trans-1']} w={{ base: '100%', sm: 400 }}> + <Stack align="center"> + <Title order={2} fw="bold" c={colors['blue-button']}> 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 /> - - + + + - - + + +
@@ -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 + <Title ta="center" order={2} fw="bold" c={colors['blue-button']}> + {isRegistrationFlow ? 'Verifikasi Registrasi' : 'Verifikasi Login'} + + Kami telah mengirim kode ke nomor {nomor} + - - - Masukkan Kode Verifikasi - + + + + Masukkan Kode Verifikasi + +
+ +
- - + + + Tidak menerima kode?{' '} + - +
@@ -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: + 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 ? : } + + + + )) ) : ( - +
- 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 ( - Logo Darmasaba { - 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} + + + +
+ ); + } + + // ✅ 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