Compare commits

...

21 Commits

Author SHA1 Message Date
124dfb8160 UI & API Menu Ekonomi, Submenu Demografi 2025-07-08 23:30:48 +08:00
46c79b8ded UI & API Submenu Sektor Unggulan, Menu Ekonomi 2025-07-08 17:16:13 +08:00
d105293149 UI & API Menu Ekonomi, Submenu Jumlah Penduduk Miskin 2025-07-08 16:30:33 +08:00
adcbe3aa3d UI & API Menu Ekonomi, Submenu Jumlah Penduduk usia yang menganggur 2025-07-08 15:20:26 +08:00
bffe648802 Fix Tabs Pegawai, Submenu Struktur & Organisasi, Menu Ekonomi 2025-07-08 12:05:10 +08:00
7e95d5fbb4 Add Fitur Search Di setiap menu 2025-07-08 00:30:55 +08:00
2725c2c064 UI & API Menu Struktur Organisasi Tabs Hubungan Organisasi 2025-07-07 22:24:46 +08:00
be189df37c UI & API Struktur organisasi sudah bisa aktif & tidak aktif 2025-07-07 21:45:04 +08:00
c0b941395d UI & API Struktur organisasi sudah bisa aktif & tidak aktif 2025-07-07 21:41:27 +08:00
a2e25a3e3a API & UI Menu Struktur bagian pegawai 2025-07-07 17:14:44 +08:00
d86824a943 API & UI Struktur Organisasi Posisi Organisasi 2025-07-07 11:33:05 +08:00
4f97c01501 Perbaikan UI & API Menu Ekonomi Pasar Desa 2025-07-04 11:09:06 +08:00
0fd47e3e94 API & UI Pasar Desa Menu Ekonomi 2025-07-04 00:11:55 +08:00
b92a974dcd API & UI Admin Menu Keamanan Done 2025-07-03 16:09:39 +08:00
10361770b4 API & UI Program Kemiskinan Menu Ekonomi 2025-07-03 12:21:08 +08:00
aec2f5094a Push 1 Program Kemiskinan 2025-07-03 11:24:54 +08:00
72d39b020a API/UI Admin Ekonomi Lowongan kerja 2025-07-02 17:03:20 +08:00
51d67736ef API & UI Admin Keamanan Laporan Publik 2025-07-02 15:35:10 +08:00
406c6f3c9f UI & API Menu Keamanan, Kontak Darurat 2025-07-02 14:10:22 +08:00
1c5e4410c4 Save 2025-07-01 20:57:32 +08:00
4724b7473d Try Fix UI & API Menu Ekonomi Sub Menu Pasar Desa 2025-07-01 16:48:44 +08:00
238 changed files with 14920 additions and 2254 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -15,7 +15,7 @@
"dependencies": {
"@cubejs-client/core": "^0.31.0",
"@elysiajs/cors": "^1.2.0",
"@elysiajs/eden": "^1.2.0",
"@elysiajs/eden": "^1.3.2",
"@elysiajs/static": "^1.3.0",
"@elysiajs/stream": "^1.1.0",
"@elysiajs/swagger": "^1.2.0",
@@ -46,7 +46,7 @@
"bun": "^1.2.2",
"chart.js": "^4.4.8",
"dayjs": "^1.11.13",
"elysia": "^1.2.12",
"elysia": "^1.3.5",
"embla-carousel-autoplay": "^8.5.2",
"embla-carousel-react": "^7.1.0",
"form-data": "^4.0.2",

View File

@@ -0,0 +1,10 @@
[
{
"id": "4b95bge6-012e-5ged-9552-4d8g65d44959",
"nama": "Makanan"
},
{
"id": "5c06chf7-123f-6hfe-0663-5e9h76e55060",
"nama": "Minuman"
}
]

View File

@@ -0,0 +1,8 @@
[
{
"id": "650e8400-e29b-41d4-a716-446655440001",
"atasanId": "550e8400-e29b-41d4-a716-446655440001",
"bawahanId": "550e8400-e29b-41d4-a716-446655440002",
"tipe": "Langsung Melapor"
}
]

View File

@@ -0,0 +1,24 @@
[
{
"id": "550e8400-e29b-41d4-a716-446655440001",
"namaLengkap": "Budi Santoso",
"gelarAkademik": "S.IP",
"tanggalMasuk": "2020-01-01T00:00:00.000Z",
"email": "budi@desa.id",
"telepon": "081234567891",
"alamat": "Jl. Raya Desa No. 1",
"posisiId": "kepala_desa",
"isActive": true
},
{
"id": "550e8400-e29b-41d4-a716-446655440002",
"namaLengkap": "Ani Lestari",
"gelarAkademik": "S.Pd",
"tanggalMasuk": "2020-02-01T00:00:00.000Z",
"email": "ani@desa.id",
"telepon": "081234567892",
"alamat": "Jl. Raya Desa No. 2",
"posisiId": "sekretaris_desa",
"isActive": true
}
]

View File

@@ -0,0 +1,27 @@
[
{
"id": "kepala_desa",
"nama": "Kepala Desa",
"deskripsi": "Kepala Desa",
"hierarki": 1
},
{
"id": "sekretaris_desa",
"nama": "Sekretaris Desa",
"deskripsi": "Sekretaris Desa",
"hierarki": 2
},
{
"id": "bendahara_desa",
"nama": "Bendahara Desa",
"deskripsi": "Bendahara Desa",
"hierarki": 3
},
{
"id": "staff_umum",
"nama": "Staff Umum",
"deskripsi": "Staff Umum",
"hierarki": 4
}
]

View File

@@ -0,0 +1,75 @@
/*
Warnings:
- You are about to drop the column `deletedAt` on the `KontakDarurat` table. All the data in the column will be lost.
- You are about to drop the column `deskripsi` on the `KontakDarurat` table. All the data in the column will be lost.
- You are about to drop the column `imageId` on the `KontakDarurat` table. All the data in the column will be lost.
- You are about to drop the column `isActive` on the `KontakDarurat` table. All the data in the column will be lost.
- You are about to drop the column `name` on the `KontakDarurat` table. All the data in the column will be lost.
- Added the required column `nama` to the `KontakDarurat` table without a default value. This is not possible if the table is not empty.
*/
-- DropForeignKey
ALTER TABLE "KontakDarurat" DROP CONSTRAINT "KontakDarurat_imageId_fkey";
-- AlterTable
ALTER TABLE "KontakDarurat" DROP COLUMN "deletedAt",
DROP COLUMN "deskripsi",
DROP COLUMN "imageId",
DROP COLUMN "isActive",
DROP COLUMN "name",
ADD COLUMN "icon" TEXT,
ADD COLUMN "nama" TEXT NOT NULL,
ADD COLUMN "urutan" INTEGER;
-- CreateTable
CREATE TABLE "KontakItem" (
"id" TEXT NOT NULL,
"nama" TEXT NOT NULL,
"nomorTelepon" TEXT NOT NULL,
"icon" TEXT,
"kategoriId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "KontakItem_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PasarDesa" (
"id" TEXT NOT NULL,
"nama" TEXT NOT NULL,
"harga" INTEGER NOT NULL,
"satuan" TEXT NOT NULL,
"alamat" TEXT NOT NULL,
"imageId" TEXT NOT NULL,
"rating" DOUBLE PRECISION 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,
"kategoriId" TEXT NOT NULL,
CONSTRAINT "PasarDesa_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "KategoriMakanan" (
"id" TEXT NOT NULL,
"nama" 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 "KategoriMakanan_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "KontakItem" ADD CONSTRAINT "KontakItem_kategoriId_fkey" FOREIGN KEY ("kategoriId") REFERENCES "KontakDarurat"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PasarDesa" ADD CONSTRAINT "PasarDesa_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PasarDesa" ADD CONSTRAINT "PasarDesa_kategoriId_fkey" FOREIGN KEY ("kategoriId") REFERENCES "KategoriMakanan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -0,0 +1,38 @@
/*
Warnings:
- You are about to drop the column `icon` on the `KontakDarurat` table. All the data in the column will be lost.
- You are about to drop the column `nama` on the `KontakDarurat` table. All the data in the column will be lost.
- You are about to drop the column `urutan` on the `KontakDarurat` table. All the data in the column will be lost.
- You are about to drop the column `deletedAt` on the `KontakDaruratKeamanan` table. All the data in the column will be lost.
- You are about to drop the column `isActive` on the `KontakDaruratKeamanan` table. All the data in the column will be lost.
- You are about to drop the column `kontak` on the `KontakDaruratKeamanan` table. All the data in the column will be lost.
- Added the required column `deskripsi` to the `KontakDarurat` table without a default value. This is not possible if the table is not empty.
- Added the required column `imageId` to the `KontakDarurat` table without a default value. This is not possible if the table is not empty.
- Added the required column `name` to the `KontakDarurat` table without a default value. This is not possible if the table is not empty.
*/
-- DropForeignKey
ALTER TABLE "KontakItem" DROP CONSTRAINT "KontakItem_kategoriId_fkey";
-- AlterTable
ALTER TABLE "KontakDarurat" DROP COLUMN "icon",
DROP COLUMN "nama",
DROP COLUMN "urutan",
ADD COLUMN "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "deskripsi" TEXT NOT NULL,
ADD COLUMN "imageId" TEXT NOT NULL,
ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN "name" TEXT NOT NULL;
-- AlterTable
ALTER TABLE "KontakDaruratKeamanan" DROP COLUMN "deletedAt",
DROP COLUMN "isActive",
DROP COLUMN "kontak",
ADD COLUMN "urutan" INTEGER;
-- AddForeignKey
ALTER TABLE "KontakDarurat" ADD CONSTRAINT "KontakDarurat_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "KontakItem" ADD CONSTRAINT "KontakItem_kategoriId_fkey" FOREIGN KEY ("kategoriId") REFERENCES "KontakDaruratKeamanan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -0,0 +1,85 @@
/*
Warnings:
- The values [SELESAI,PROSES,GAGAL] on the enum `StatusLaporan` will be removed. If these variants are still used in the database, this will fail.
- You are about to drop the column `icon` on the `KontakDaruratKeamanan` table. All the data in the column will be lost.
- You are about to drop the column `urutan` on the `KontakDaruratKeamanan` table. All the data in the column will be lost.
- You are about to drop the column `icon` on the `KontakItem` table. All the data in the column will be lost.
*/
-- AlterEnum
BEGIN;
CREATE TYPE "StatusLaporan_new" AS ENUM ('Selesai', 'Proses', 'Gagal');
ALTER TABLE "LaporanPublik" ALTER COLUMN "status" TYPE "StatusLaporan_new" USING ("status"::text::"StatusLaporan_new");
ALTER TYPE "StatusLaporan" RENAME TO "StatusLaporan_old";
ALTER TYPE "StatusLaporan_new" RENAME TO "StatusLaporan";
DROP TYPE "StatusLaporan_old";
COMMIT;
-- AlterTable
ALTER TABLE "KontakDaruratKeamanan" DROP COLUMN "icon",
DROP COLUMN "urutan",
ADD COLUMN "imageId" TEXT;
-- AlterTable
ALTER TABLE "KontakItem" DROP COLUMN "icon",
ADD COLUMN "imageId" TEXT;
-- CreateTable
CREATE TABLE "LowonganPekerjaan" (
"id" TEXT NOT NULL,
"posisi" TEXT NOT NULL,
"namaPerusahaan" TEXT NOT NULL,
"lokasi" TEXT NOT NULL,
"tipePekerjaan" TEXT NOT NULL,
"gaji" TEXT NOT NULL,
"deskripsi" TEXT NOT NULL,
"kualifikasi" TEXT NOT NULL,
"tanggalPosting" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "LowonganPekerjaan_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ProgramKemiskinan" (
"id" TEXT NOT NULL,
"nama" TEXT NOT NULL,
"deskripsi" TEXT NOT NULL,
"ikonUrl" TEXT,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"statistikId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ProgramKemiskinan_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "StatistikKemiskinan" (
"id" TEXT NOT NULL,
"tahun" INTEGER NOT NULL,
"jumlah" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "StatistikKemiskinan_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "ProgramKemiskinan_statistikId_key" ON "ProgramKemiskinan"("statistikId");
-- CreateIndex
CREATE UNIQUE INDEX "StatistikKemiskinan_tahun_key" ON "StatistikKemiskinan"("tahun");
-- AddForeignKey
ALTER TABLE "KontakDaruratKeamanan" ADD CONSTRAINT "KontakDaruratKeamanan_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "KontakItem" ADD CONSTRAINT "KontakItem_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ProgramKemiskinan" ADD CONSTRAINT "ProgramKemiskinan_statistikId_fkey" FOREIGN KEY ("statistikId") REFERENCES "StatistikKemiskinan"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -0,0 +1,59 @@
/*
Warnings:
- You are about to drop the column `alamat` on the `PasarDesa` table. All the data in the column will be lost.
- You are about to drop the column `deletedAt` on the `PasarDesa` table. All the data in the column will be lost.
- You are about to drop the column `isActive` on the `PasarDesa` table. All the data in the column will be lost.
- You are about to drop the column `kategoriId` on the `PasarDesa` table. All the data in the column will be lost.
- You are about to drop the column `satuan` on the `PasarDesa` table. All the data in the column will be lost.
- You are about to drop the `KategoriMakanan` table. If the table is not empty, all the data it contains will be lost.
- Added the required column `alamatUsaha` to the `PasarDesa` table without a default value. This is not possible if the table is not empty.
*/
-- DropForeignKey
ALTER TABLE "PasarDesa" DROP CONSTRAINT "PasarDesa_imageId_fkey";
-- DropForeignKey
ALTER TABLE "PasarDesa" DROP CONSTRAINT "PasarDesa_kategoriId_fkey";
-- AlterTable
ALTER TABLE "PasarDesa" DROP COLUMN "alamat",
DROP COLUMN "deletedAt",
DROP COLUMN "isActive",
DROP COLUMN "kategoriId",
DROP COLUMN "satuan",
ADD COLUMN "alamatUsaha" TEXT NOT NULL,
ALTER COLUMN "imageId" DROP NOT NULL;
-- DropTable
DROP TABLE "KategoriMakanan";
-- CreateTable
CREATE TABLE "KategoriProduk" (
"id" TEXT NOT NULL,
"nama" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "KategoriProduk_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "_ProdukToKategori" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL,
CONSTRAINT "_ProdukToKategori_AB_pkey" PRIMARY KEY ("A","B")
);
-- CreateIndex
CREATE INDEX "_ProdukToKategori_B_index" ON "_ProdukToKategori"("B");
-- AddForeignKey
ALTER TABLE "PasarDesa" ADD CONSTRAINT "PasarDesa_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_ProdukToKategori" ADD CONSTRAINT "_ProdukToKategori_A_fkey" FOREIGN KEY ("A") REFERENCES "KategoriProduk"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_ProdukToKategori" ADD CONSTRAINT "_ProdukToKategori_B_fkey" FOREIGN KEY ("B") REFERENCES "PasarDesa"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,41 @@
/*
Warnings:
- You are about to drop the `_ProdukToKategori` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropForeignKey
ALTER TABLE "_ProdukToKategori" DROP CONSTRAINT "_ProdukToKategori_A_fkey";
-- DropForeignKey
ALTER TABLE "_ProdukToKategori" DROP CONSTRAINT "_ProdukToKategori_B_fkey";
-- AlterTable
ALTER TABLE "KategoriProduk" ADD COLUMN "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true;
-- AlterTable
ALTER TABLE "PasarDesa" ADD COLUMN "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true;
-- DropTable
DROP TABLE "_ProdukToKategori";
-- CreateTable
CREATE TABLE "KategoriToPasar" (
"id" TEXT NOT NULL,
"kategoriId" TEXT NOT NULL,
"pasarDesaId" 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 "KategoriToPasar_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "KategoriToPasar" ADD CONSTRAINT "KategoriToPasar_kategoriId_fkey" FOREIGN KEY ("kategoriId") REFERENCES "KategoriProduk"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "KategoriToPasar" ADD CONSTRAINT "KategoriToPasar_pasarDesaId_fkey" FOREIGN KEY ("pasarDesaId") REFERENCES "PasarDesa"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -0,0 +1,11 @@
/*
Warnings:
- Added the required column `kategoriProdukId` to the `PasarDesa` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "PasarDesa" ADD COLUMN "kategoriProdukId" TEXT NOT NULL;
-- AddForeignKey
ALTER TABLE "PasarDesa" ADD CONSTRAINT "PasarDesa_kategoriProdukId_fkey" FOREIGN KEY ("kategoriProdukId") REFERENCES "KategoriProduk"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -0,0 +1,78 @@
-- CreateTable
CREATE TABLE "posisi_organisasi" (
"id" VARCHAR(50) NOT NULL,
"nama" VARCHAR(100) NOT NULL,
"deskripsi" TEXT,
"hierarki" INTEGER NOT NULL,
CONSTRAINT "posisi_organisasi_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "pegawai" (
"id" UUID 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 "pegawai_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "hubungan_organisasi" (
"id" UUID NOT NULL,
"atasanId" UUID NOT NULL,
"bawahanId" UUID NOT NULL,
"tipe" VARCHAR(50),
CONSTRAINT "hubungan_organisasi_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "struktur_organisasi" (
"id" TEXT NOT NULL,
"posisiOrganisasiId" VARCHAR(50) NOT NULL,
"pegawaiId" UUID NOT NULL,
"hubunganOrganisasiId" UUID 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 "struktur_organisasi_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "pegawai_email_key" ON "pegawai"("email");
-- CreateIndex
CREATE UNIQUE INDEX "hubungan_organisasi_atasanId_bawahanId_key" ON "hubungan_organisasi"("atasanId", "bawahanId");
-- AddForeignKey
ALTER TABLE "pegawai" ADD CONSTRAINT "pegawai_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "pegawai" ADD CONSTRAINT "pegawai_posisiId_fkey" FOREIGN KEY ("posisiId") REFERENCES "posisi_organisasi"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "hubungan_organisasi" ADD CONSTRAINT "hubungan_organisasi_atasanId_fkey" FOREIGN KEY ("atasanId") REFERENCES "pegawai"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "hubungan_organisasi" ADD CONSTRAINT "hubungan_organisasi_bawahanId_fkey" FOREIGN KEY ("bawahanId") REFERENCES "pegawai"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "struktur_organisasi" ADD CONSTRAINT "struktur_organisasi_posisiOrganisasiId_fkey" FOREIGN KEY ("posisiOrganisasiId") REFERENCES "posisi_organisasi"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "struktur_organisasi" ADD CONSTRAINT "struktur_organisasi_pegawaiId_fkey" FOREIGN KEY ("pegawaiId") REFERENCES "pegawai"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "struktur_organisasi" ADD CONSTRAINT "struktur_organisasi_hubunganOrganisasiId_fkey" FOREIGN KEY ("hubunganOrganisasiId") REFERENCES "hubungan_organisasi"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -0,0 +1,77 @@
-- CreateTable
CREATE TABLE "GrafikMenganggurBerdasarkanUsia" (
"id" TEXT NOT NULL,
"usia18_25" TEXT NOT NULL,
"usia26_35" TEXT NOT NULL,
"usia36_45" TEXT NOT NULL,
"usia46_keatas" 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 "GrafikMenganggurBerdasarkanUsia_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "GrafikMenganggurBerdasarkanPendidikan" (
"id" TEXT NOT NULL,
"SD" TEXT NOT NULL,
"SMP" TEXT NOT NULL,
"SMA" TEXT NOT NULL,
"D3" TEXT NOT NULL,
"S1" 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 "GrafikMenganggurBerdasarkanPendidikan_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "GrafikJumlahPendudukMiskin" (
"id" UUID NOT NULL,
"year" INTEGER NOT NULL,
"totalPoorPopulation" INTEGER 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 "GrafikJumlahPendudukMiskin_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "SektorUnggulanDesa" (
"id" UUID NOT NULL,
"name" VARCHAR(100) NOT NULL,
"description" TEXT,
"value" DOUBLE PRECISION,
"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 "SektorUnggulanDesa_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "DataDemografiPekerjaan" (
"id" TEXT NOT NULL,
"pekerjaan" TEXT NOT NULL,
"lakiLaki" INTEGER NOT NULL,
"perempuan" INTEGER 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 "DataDemografiPekerjaan_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "GrafikJumlahPendudukMiskin_year_key" ON "GrafikJumlahPendudukMiskin"("year");
-- CreateIndex
CREATE UNIQUE INDEX "SektorUnggulanDesa_name_key" ON "SektorUnggulanDesa"("name");

View File

@@ -82,6 +82,14 @@ model FileStorage {
MenuTipsKeamanan MenuTipsKeamanan[]
Pelapor Pelapor[]
PasarDesa PasarDesa[]
KontakDaruratKeamanan KontakDaruratKeamanan[]
KontakItem KontakItem[]
Pegawai Pegawai[]
}
//========================================= MENU PPID ========================================= //
@@ -924,18 +932,27 @@ model LayananPolsek {
PolsekTerdekat PolsekTerdekat[]
}
// ========================================= KONTAK DARURAT ========================================= //
model KontakDaruratKeamanan {
id String @id @default(cuid())
nama String // contoh: "Polisi", "Ambulans", "Pemadam Kebakaran"
kontak String // contoh: "081xxxxxxxxxx"
icon String? // opsional, untuk simpan nama icon-nya jika mau
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
id String @id @default(uuid())
nama String // contoh: "Layanan Darurat", "Fasilitas Kesehatan"
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
kontakItems KontakItem[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model KontakItem {
id String @id @default(uuid())
nama String // contoh: "Polisi", "Ambulans", "Puskesmas Darmasaba"
nomorTelepon String
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
kategori KontakDaruratKeamanan @relation(fields: [kategoriId], references: [id])
kategoriId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// ========================================= PENCEGAHAN KRIMINALITAS ========================================= //
@@ -1005,9 +1022,9 @@ model PenangananLaporanPublik {
}
enum StatusLaporan {
SELESAI
PROSES
GAGAL
Selesai
Proses
Gagal
}
model Pelapor {
@@ -1031,3 +1048,217 @@ model MenuTipsKeamanan {
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= MENU EKONOMI ========================================= //
// ========================================= PASAR DESA ========================================= //
model PasarDesa {
id String @id @default(uuid())
nama String
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
harga Int
rating Float
alamatUsaha String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
kategoriProduk KategoriProduk @relation(fields: [kategoriProdukId], references: [id])
kategoriProdukId String
KategoriToPasar KategoriToPasar[]
}
model KategoriProduk {
id String @id @default(uuid())
nama String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
KategoriToPasar KategoriToPasar[]
PasarDesa PasarDesa[]
}
model KategoriToPasar {
id String @id @default(uuid())
kategori KategoriProduk @relation(fields: [kategoriId], references: [id])
kategoriId String
pasarDesa PasarDesa @relation(fields: [pasarDesaId], references: [id])
pasarDesaId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= LOWONGAN KERJA LOKAL ========================================= //
model LowonganPekerjaan {
id String @id @default(uuid())
posisi String
namaPerusahaan String
lokasi String
tipePekerjaan String
gaji String
deskripsi String
kualifikasi String
tanggalPosting DateTime @default(now())
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
}
// ========================================= STRUKTUR ORGANISASI ========================================= //
model PosisiOrganisasi {
id String @id @default(uuid()) @db.VarChar(50)
nama String @db.VarChar(100)
deskripsi String? @db.Text
hierarki Int
pegawai Pegawai[]
strukturOrganisasi StrukturOrganisasi[] // Relasi balik
@@map("posisi_organisasi")
}
model Pegawai {
id String @id @default(uuid()) @db.Uuid
namaLengkap String @db.VarChar(255)
gelarAkademik String? @db.VarChar(100)
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
tanggalMasuk DateTime? @db.Date
email String? @unique @db.VarChar(255)
telepon String? @db.VarChar(20)
alamat String? @db.Text
posisiId String @db.VarChar(50)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
posisi PosisiOrganisasi @relation(fields: [posisiId], references: [id])
sebagaiAtasan HubunganOrganisasi[] @relation("AtasanToBawahan")
sebagaiBawahan HubunganOrganisasi[] @relation("BawahanToAtasan")
strukturOrganisasi StrukturOrganisasi[] // Relasi balik
@@map("pegawai")
}
model HubunganOrganisasi {
id String @id @default(uuid()) @db.Uuid
atasanId String @db.Uuid
bawahanId String @db.Uuid
tipe String? @db.VarChar(50)
atasan Pegawai @relation("AtasanToBawahan", fields: [atasanId], references: [id])
bawahan Pegawai @relation("BawahanToAtasan", fields: [bawahanId], references: [id])
strukturOrganisasi StrukturOrganisasi[] // Relasi balik
@@unique([atasanId, bawahanId])
@@map("hubungan_organisasi")
}
model StrukturOrganisasi {
id String @id @default(uuid())
posisiOrganisasiId String @db.VarChar(50)
pegawaiId String @db.Uuid
hubunganOrganisasiId String @db.Uuid
posisiOrganisasi PosisiOrganisasi @relation(fields: [posisiOrganisasiId], references: [id])
pegawai Pegawai @relation(fields: [pegawaiId], references: [id])
hubunganOrganisasi HubunganOrganisasi @relation(fields: [hubunganOrganisasiId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
isActive Boolean @default(true)
@@map("struktur_organisasi")
}
// ========================================= PROGRAM KEMISKINAN ========================================= //
model ProgramKemiskinan {
id String @id @default(uuid())
nama String
deskripsi String
ikonUrl String?
isActive Boolean @default(true)
statistikId String? @unique
statistik StatistikKemiskinan? @relation(fields: [statistikId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model StatistikKemiskinan {
id String @id @default(uuid())
tahun Int @unique
jumlah Int
programKemiskinan ProgramKemiskinan?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// ========================================= JUMLAH PENDUDUK USIA KERJA YANG MENGANGGUR ========================================= //
model GrafikMenganggurBerdasarkanUsia {
id String @id @default(cuid())
usia18_25 String
usia26_35 String
usia36_45 String
usia46_keatas String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model GrafikMenganggurBerdasarkanPendidikan {
id String @id @default(cuid())
SD String
SMP String
SMA String
D3 String
S1 String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= JUMLAH PENDUDUK MISKIN ========================================= //
model GrafikJumlahPendudukMiskin {
id String @id @default(uuid()) @db.Uuid // Menggunakan UUID sebagai primary key
year Int @unique // Tahun data (e.g., 2024, 2025)
totalPoorPopulation Int // Jumlah penduduk miskin (e.g., 4800000)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= SEKTOR UNGGULAN DESA ========================================= //
model SektorUnggulanDesa {
id String @id @default(uuid()) @db.Uuid // Menggunakan UUID sebagai primary key
name String @unique @db.VarChar(100) // Nama sektor (e.g., "Sektor Pertanian", "Sektor Peternakan")
description String? @db.Text // Deskripsi lengkap tentang sektor
value Float? // Nilai kuantitatif sektor (misalnya, kontribusi PDB, jumlah produksi, dll.)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= DEMOGRAFI PEKERJAAN ========================================= //
model DataDemografiPekerjaan {
id String @id @default(cuid())
pekerjaan String
lakiLaki Int
perempuan Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

View File

@@ -17,6 +17,10 @@ import visiMisiDesa from "./data/desa/profile/visi_misi_desa.json";
import lambangDesa from "./data/desa/profile/lambang_desa.json";
import maskotDesa from "./data/desa/profile/maskot_desa.json";
import profilPerbekel from "./data/desa/profile/profil_perbekel.json";
import kategoriProduk from "./data/ekonomi/pasar-desa/kategori-produk.json";
import hubunganOrganisasi from "./data/ekonomi/struktur-organisasi/hubungan-organisasi.json";
import posisiOrganisasi from "./data/ekonomi/struktur-organisasi/posisi-organisasi.json";
import pegawai from "./data/ekonomi/struktur-organisasi/pegawai.json";
(async () => {
for (const l of layanan) {
@@ -340,6 +344,93 @@ import profilPerbekel from "./data/desa/profile/profil_perbekel.json";
});
}
console.log("dasar hukum PPID success ...");
for (const k of kategoriProduk) {
await prisma.kategoriProduk.upsert({
where: {
id: k.id,
},
update: {
nama: k.nama,
},
create: {
id: k.id,
nama: k.nama,
},
});
}
console.log("kategori produk success ...");
for (const p of posisiOrganisasi) {
await prisma.posisiOrganisasi.upsert({
where: {
id: p.id,
},
update: {
nama: p.nama,
deskripsi: p.deskripsi,
hierarki: p.hierarki,
},
create: {
id: p.id,
nama: p.nama,
deskripsi: p.deskripsi,
hierarki: p.hierarki,
},
});
}
console.log("posisi organisasi success ...");
for (const p of pegawai) {
await prisma.pegawai.upsert({
where: {
id: p.id,
},
update: {
namaLengkap: p.namaLengkap,
gelarAkademik: p.gelarAkademik,
tanggalMasuk: new Date(p.tanggalMasuk),
email: p.email,
telepon: p.telepon,
alamat: p.alamat,
posisiId: p.posisiId,
isActive: p.isActive,
},
create: {
id: p.id,
namaLengkap: p.namaLengkap,
gelarAkademik: p.gelarAkademik,
tanggalMasuk: new Date(p.tanggalMasuk),
email: p.email,
telepon: p.telepon,
alamat: p.alamat,
posisiId: p.posisiId,
isActive: p.isActive,
},
});
}
console.log("pegawai success ...");
for (const p of hubunganOrganisasi) {
await prisma.hubunganOrganisasi.upsert({
where: {
atasanId_bawahanId: {
atasanId: p.atasanId,
bawahanId: p.bawahanId,
},
},
update: {
tipe: p.tipe,
},
create: {
atasanId: p.atasanId,
bawahanId: p.bawahanId,
tipe: p.tipe,
},
});
}
console.log("hubungan organisasi success ...");
})()
.then(() => prisma.$disconnect())
.catch((e) => {

View File

@@ -1,22 +1,37 @@
import React from 'react';
import { Grid, GridCol, Paper, TextInput, Title } from '@mantine/core';
import { IconSearch } from '@tabler/icons-react'; // Sesuaikan jika kamu pakai icon lain
import { IconSearch } from '@tabler/icons-react';
import colors from '@/con/colors';
type HeaderSearchProps = {
title: string;
placeholder?: string;
searchIcon?: React.ReactNode;
value?: string;
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
};
const HeaderSearch = ({ title = "", placeholder = "pencarian", searchIcon = <IconSearch size={20} /> }: { title: string, placeholder?: string, searchIcon?: React.ReactNode }) => {
const HeaderSearch = ({
title = "",
placeholder = "pencarian",
searchIcon = <IconSearch size={20} />,
value,
onChange,
}: HeaderSearchProps) => {
return (
<Grid>
<GridCol span={{ base: 12, md: 9 }}>
<Title order={3}>{title}</Title>
</GridCol>
<GridCol span={{ base: 12, md: 3 }}>
<Paper radius={"lg"} bg={colors['white-1']}>
<Paper radius="lg" bg={colors['white-1']}>
<TextInput
radius="lg"
placeholder={placeholder}
leftSection={searchIcon}
w="100%"
value={value}
onChange={onChange}
/>
</Paper>
</GridCol>
@@ -24,4 +39,4 @@ const HeaderSearch = ({ title = "", placeholder = "pencarian", searchIcon = <Ico
);
};
export default HeaderSearch;
export default HeaderSearch;

View File

@@ -5,31 +5,48 @@ import { IconCircleDashedPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import React from 'react';
const JudulListTab = ({ title = "", href = "#", placeholder = "pencarian", searchIcon = <IconSearch size={20} /> }) => {
type JudulListTabProps = {
title: string;
href: string;
placeholder: string;
searchIcon: React.ReactNode;
value?: string;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
}
const JudulListTab = ({
title = "",
href = "#",
placeholder = "pencarian",
searchIcon = <IconSearch size={20} />,
value,
onChange
}: JudulListTabProps) => {
const router = useRouter();
const handleNavigate = () => {
router.push(href);
};
return (
<Grid mb={10}>
<GridCol span={{ base: 12, md: 8 }}>
<Text fz={{base: "md", md: "xl"}} fw={"bold"}>{title}</Text>
<Text fz={{ base: "md", md: "xl" }} fw={"bold"}>{title}</Text>
</GridCol>
<GridCol span={{ base: 9, md: 3}} ta="right">
<GridCol span={{ base: 9, md: 3 }} ta="right">
<Paper radius={"lg"} bg={colors['white-1']}>
<TextInput
radius="lg"
placeholder={placeholder}
leftSection={searchIcon}
w="100%"
value={value}
onChange={onChange}
/>
</Paper>
</GridCol>
<GridCol span={{ base: 3, md: 1}} ta="right">
<GridCol span={{ base: 3, md: 1 }} ta="right">
<Button onClick={handleNavigate} bg={colors['blue-button']}>
<IconCircleDashedPlus size={25} />
</Button>

View File

@@ -0,0 +1,197 @@
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
const templateDemografiPekerjaan = z.object({
pekerjaan: z.string().min(1, "Pekerjaan harus diisi"),
lakiLaki: z.number().min(1, "Laki - Laki harus diisi"),
perempuan: z.number().min(1, "Perempuan harus diisi"),
});
type DemografiPekerjaan = Prisma.DataDemografiPekerjaanGetPayload<{
select: {
pekerjaan: true;
lakiLaki: true;
perempuan: true;
};
}>;
const defaultForm: DemografiPekerjaan = {
pekerjaan: "",
lakiLaki: 0,
perempuan: 0,
};
const demografiPekerjaan = proxy({
create: {
form: defaultForm,
loading: false,
async create() {
const cek = templateDemografiPekerjaan.safeParse(
demografiPekerjaan.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return null;
}
try {
demografiPekerjaan.create.loading = true;
const res = await ApiFetch.api.ekonomi.demografipekerjaan[
"create"
].post(demografiPekerjaan.create.form);
if (res.status === 200) {
const id = res.data?.data?.id;
if (id) {
toast.success("Success create");
demografiPekerjaan.create.form = { ...defaultForm };
demografiPekerjaan.findMany.load();
return id;
}
}
toast.error("failed create");
return null;
} catch (error) {
console.log((error as Error).message);
return null;
} finally {
demografiPekerjaan.create.loading = false;
}
},
},
findMany: {
data: null as
| Prisma.DataDemografiPekerjaanGetPayload<{
omit: { isActive: true };
}>[]
| null,
async load() {
const res = await ApiFetch.api.ekonomi.demografipekerjaan[
"find-many"
].get();
if (res.status === 200) {
demografiPekerjaan.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.DataDemografiPekerjaanGetPayload<{
omit: { isActive: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/ekonomi/demografipekerjaan/${id}`);
if (res.ok) {
const data = await res.json();
demografiPekerjaan.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch demografiPekerjaan:", res.statusText);
demografiPekerjaan.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching demografiPekerjaan:", error);
demografiPekerjaan.findUnique.data = null;
}
},
},
update: {
id: "",
form: { ...defaultForm },
loading: false,
async submit() {
const id = this.id;
if (!id) {
toast.warn("ID tidak valid");
return null;
}
const formData = {
pekerjaan: this.form.pekerjaan,
lakiLaki: this.form.lakiLaki,
perempuan: this.form.perempuan,
};
const cek = templateDemografiPekerjaan.safeParse(formData);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return null;
}
try {
this.loading = true;
const res = await fetch(`/api/ekonomi/demografipekerjaan/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(formData),
});
const result = await res.json();
if (!res.ok || !result?.success) {
throw new Error(result?.message || "Gagal update data");
}
toast.success("Berhasil update data!");
await demografiPekerjaan.findMany.load();
return result.data;
} catch (error) {
console.error("Update error:", error);
toast.error("Gagal update data demografi pekerjaan");
throw error;
} finally {
this.loading = false;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
demografiPekerjaan.delete.loading = true;
const response = await fetch(
`/api/ekonomi/demografipekerjaan/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(
result.message || "Demografi pekerjaan berhasil dihapus"
);
await demografiPekerjaan.findMany.load();
} else {
toast.error(result?.message || "Gagal menghapus demografi pekerjaan");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus persentase kelahiran");
} finally {
demografiPekerjaan.delete.loading = false;
}
},
},
});
export default demografiPekerjaan

View File

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

View File

@@ -0,0 +1,225 @@
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
const templateForm = z.object({
posisi: z.string(),
namaPerusahaan: z.string(),
lokasi: z.string(),
tipePekerjaan: z.string(),
gaji: z.string(),
deskripsi: z.string(),
kualifikasi: z.string(),
});
const defaultForm = {
posisi: "",
namaPerusahaan: "",
lokasi: "",
tipePekerjaan: "",
gaji: "",
deskripsi: "",
kualifikasi: "",
};
const lowonganKerjaState = proxy({
create: {
form: { ...defaultForm },
loading: false,
async create() {
const cek = templateForm.safeParse(lowonganKerjaState.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
lowonganKerjaState.create.loading = true;
const res = await ApiFetch.api.ekonomi.lowongankerja["create"].post(
lowonganKerjaState.create.form
);
if (res.status === 200) {
lowonganKerjaState.create.loading = false;
return toast.success("success create");
}
console.log(res);
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
} finally {
lowonganKerjaState.create.loading = false;
}
},
resetForm() {
lowonganKerjaState.create.form = { ...defaultForm };
},
},
findMany: {
data: null as
| Prisma.LowonganPekerjaanGetPayload<{
omit: { isActive: true };
}>[]
| null,
async load() {
const res = await ApiFetch.api.ekonomi.lowongankerja["find-many"].get();
if (res.status === 200) {
lowonganKerjaState.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.LowonganPekerjaanGetPayload<{
omit: { isActive: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/ekonomi/lowongankerja/${id}`);
if (res.ok) {
const data = await res.json();
lowonganKerjaState.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
lowonganKerjaState.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
lowonganKerjaState.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
lowonganKerjaState.delete.loading = true;
const response = await fetch(`/api/ekonomi/lowongankerja/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Lowongan kerja berhasil dihapus");
await lowonganKerjaState.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus lowongan kerja");
}
} catch (error) {
console.error("Error deleting data:", error);
toast.error("Terjadi kesalahan saat menghapus lowongan kerja");
} finally {
lowonganKerjaState.delete.loading = false;
}
},
},
update: {
id: "",
form: { ...defaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/ekonomi/lowongankerja/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
posisi: data.posisi,
namaPerusahaan: data.namaPerusahaan,
lokasi: data.lokasi,
tipePekerjaan: data.tipePekerjaan,
gaji: data.gaji,
deskripsi: data.deskripsi,
kualifikasi: data.kualifikasi,
};
return data;
} else {
throw new Error(
result?.message || "Gagal memuat data lowongan kerja"
);
}
} catch (error) {
console.error("Error fetching data:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateForm.safeParse(lowonganKerjaState.update.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
lowonganKerjaState.update.loading = true;
const response = await fetch(`/api/ekonomi/lowongankerja/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
posisi: this.form.posisi,
namaPerusahaan: this.form.namaPerusahaan,
lokasi: this.form.lokasi,
tipePekerjaan: this.form.tipePekerjaan,
gaji: this.form.gaji,
deskripsi: this.form.deskripsi,
kualifikasi: this.form.kualifikasi,
}),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update lowongan kerja");
await lowonganKerjaState.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal update lowongan kerja");
}
} catch (error) {
console.error("Error updating data:", error);
toast.error(error instanceof Error ? error.message : "Terjadi kesalahan saat mengupdate lowongan kerja");
return false;
} finally {
lowonganKerjaState.update.loading = false;
}
},
reset() {
lowonganKerjaState.update.id = "";
lowonganKerjaState.update.form = { ...defaultForm };
},
},
});
export default lowonganKerjaState;

View File

@@ -0,0 +1,459 @@
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
const templatePasarDesaForm = z.object({
nama: z.string().min(1, "Nama minimal 1 karakter"),
harga: z.number().min(1, "Harga minimal 1"),
alamatUsaha: z.string().min(1, "Alamat minimal 1 karakter"),
imageId: z.string().min(1, "Gambar wajib dipilih"),
rating: z.number().min(1, "Rating minimal 1"),
kategoriId: z.array(z.string()).min(1, "Minimal pilih satu kategori"),
});
const defaultPasarDesaForm = {
nama: "",
harga: 0,
alamatUsaha: "",
imageId: "",
rating: 0,
kategoriId: [] as string[],
};
const pasarDesa = proxy({
create: {
form: { ...defaultPasarDesaForm },
loading: false,
async create() {
const cek = templatePasarDesaForm.safeParse(pasarDesa.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
pasarDesa.create.loading = true;
const res = await ApiFetch.api.ekonomi.pasardesa["create"].post(
pasarDesa.create.form
);
if (res.status === 200) {
pasarDesa.findMany.load();
return toast.success("Data berhasil ditambahkan");
}
return toast.error("Gagal menambahkan data");
} catch (error) {
console.log(error);
toast.error("Gagal menambahkan data");
} finally {
pasarDesa.create.loading = false;
}
},
},
findMany: {
data: null as Array<
Prisma.PasarDesaGetPayload<{
include: {
image: true;
KategoriToPasar: {
include: {
kategori: true;
};
};
};
}>
> | null,
async load() {
const res = await ApiFetch.api.ekonomi.pasardesa["find-many"].get();
if (res.status === 200) {
pasarDesa.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.PasarDesaGetPayload<{
include: {
image: true;
KategoriToPasar: {
include: {
kategori: true;
};
};
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/ekonomi/pasardesa/${id}`);
if (res.ok) {
const data = await res.json();
pasarDesa.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
pasarDesa.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
pasarDesa.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
pasarDesa.delete.loading = true;
const response = await fetch(`/api/ekonomi/pasardesa/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Pasar desa berhasil dihapus");
await pasarDesa.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus pasar desa");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus pasar desa");
} finally {
pasarDesa.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...defaultPasarDesaForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/ekonomi/pasardesa/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
nama: data.nama,
harga: data.harga,
alamatUsaha: data.alamatUsaha,
imageId: data.imageId,
rating: data.rating,
kategoriId: data.kategoriId,
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading pasar desa:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templatePasarDesaForm.safeParse(pasarDesa.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
pasarDesa.edit.loading = true;
const response = await fetch(`/api/ekonomi/pasardesa/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
nama: this.form.nama,
harga: this.form.harga,
alamatUsaha: this.form.alamatUsaha,
imageId: this.form.imageId,
rating: this.form.rating,
kategoriId: this.form.kategoriId,
}),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update pasar desa");
await pasarDesa.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal mengupdate pasar desa");
}
} catch (error) {
console.error("Error updating pasar desa:", error);
toast.error(
error instanceof Error ? error.message : "Gagal mengupdate pasar desa"
);
return false;
} finally {
pasarDesa.edit.loading = false;
}
},
reset() {
pasarDesa.edit.id = "";
pasarDesa.edit.form = { ...defaultPasarDesaForm };
},
},
});
// ========================================= KATEGORI PRODUK ========================================= //
const kategoriProdukForm = z.object({
nama: z.string().min(1, "Nama minimal 1 karakter"),
});
const kategoriProdukDefaultForm = {
nama: "",
};
const kategoriProduk = proxy({
create: {
form: { ...kategoriProdukDefaultForm },
loading: false,
async create() {
const cek = kategoriProdukForm.safeParse(kategoriProduk.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
kategoriProduk.create.loading = true;
const res = await ApiFetch.api.ekonomi.kategoriproduk["create"].post(
kategoriProduk.create.form
);
if (res.status === 200) {
kategoriProduk.findMany.load();
return toast.success("Data berhasil ditambahkan");
}
return toast.error("Gagal menambahkan data");
} catch (error) {
console.log(error);
toast.error("Gagal menambahkan data");
} finally {
kategoriProduk.create.loading = false;
}
},
},
findMany: {
data: null as Array<{
id: string;
nama: string;
}>
| null,
async load() {
const res = await ApiFetch.api.ekonomi.kategoriproduk["find-many"].get();
if (res.status === 200) {
kategoriProduk.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.KategoriProdukGetPayload<{
omit: { isActive: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/ekonomi/kategoriproduk/${id}`);
if (res.ok) {
const data = await res.json();
kategoriProduk.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
kategoriProduk.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
kategoriProduk.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
kategoriProduk.delete.loading = true;
const response = await fetch(`/api/ekonomi/kategoriproduk/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Kategori produk berhasil dihapus");
await kategoriProduk.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus kategori produk");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus kategori produk");
} finally {
kategoriProduk.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...kategoriProdukDefaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/ekonomi/kategoriproduk/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
nama: data.nama,
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading kategori produk:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = kategoriProdukForm.safeParse(kategoriProduk.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
kategoriProduk.edit.loading = true;
const response = await fetch(
`/api/ekonomi/kategoriproduk/${kategoriProduk.edit.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
nama: kategoriProduk.edit.form.nama,
}),
}
);
// Clone the response to avoid 'body already read' error
const responseClone = response.clone();
try {
const result = await response.json();
if (!response.ok) {
console.error('Update failed with status:', response.status, 'Response:', result);
throw new Error(
result?.message || `Gagal mengupdate kategori produk (${response.status})`
);
}
if (result.success) {
toast.success(result.message || "Berhasil memperbarui kategori produk");
await kategoriProduk.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal mengupdate kategori produk");
}
} catch (error) {
// If JSON parsing fails, try to get the response text for better error messages
try {
const text = await responseClone.text();
console.error('Error response text:', text);
throw new Error(`Gagal memproses respons dari server: ${text}`);
} catch (textError) {
console.error('Error parsing response as text:', textError);
console.error('Original error:', error);
throw new Error('Gagal memproses respons dari server');
}
}
} catch (error) {
console.error("Error updating kategori produk:", error);
toast.error(
error instanceof Error
? error.message
: "Gagal mengupdate kategori produk"
);
return false;
} finally {
kategoriProduk.edit.loading = false;
}
},
reset() {
kategoriProduk.edit.id = "";
kategoriProduk.edit.form = { ...kategoriProdukDefaultForm };
},
},
});
const pasarDesaState = proxy({
pasarDesa,
kategoriProduk
});
export default pasarDesaState;

View File

@@ -0,0 +1,249 @@
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
const templateForm = z.object({
nama: z.string().min(1, "Nama minimal 1 karakter"),
deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
ikonUrl: z.string().optional(),
statistik: z.object({
tahun: z.string().min(1, "Tahun minimal 1 karakter"),
jumlah: z.string().min(1, "Jumlah minimal 1 karakter"),
})
});
const defaultForm = {
nama: "",
deskripsi: "",
ikonUrl: "",
statistik: {
tahun: "",
jumlah: ""
}
};
const programKemiskinanState = proxy({
create: {
form: { ...defaultForm },
loading: false,
async create() {
const cek = templateForm.safeParse(programKemiskinanState.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
programKemiskinanState.create.loading = true;
const res = await ApiFetch.api.ekonomi.programkemiskinan["create"].post(
programKemiskinanState.create.form
);
if (res.status === 200) {
programKemiskinanState.findMany.load();
return toast.success("success create");
}
console.log(res);
return toast.error("failed create");
} catch (error) {
console.log(error);
return toast.error("failed create");
} finally {
programKemiskinanState.create.loading = false;
}
},
},
findMany: {
data: [] as Prisma.ProgramKemiskinanGetPayload<{
include: {
statistik: true;
};
}>[],
loading: false,
async load() {
const res = await ApiFetch.api.ekonomi.programkemiskinan[
"find-many"
].get();
if (res.status === 200) {
programKemiskinanState.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.ProgramKemiskinanGetPayload<{
include: {
statistik: true;
};
}> | null,
loading: false,
async load(id: string) {
try {
const res = await fetch(`/api/ekonomi/programkemiskinan/${id}`);
if (res.ok) {
const data = await res.json();
programKemiskinanState.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
programKemiskinanState.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
programKemiskinanState.findUnique.data = null;
}
},
},
update: {
id: "",
form: { ...defaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/ekonomi/programkemiskinan/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
nama: data.nama,
deskripsi: data.deskripsi,
ikonUrl: data.ikonUrl || "",
statistik: {
tahun: data.statistik.tahun,
jumlah: data.statistik.jumlah,
},
};
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading program kemiskinan:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateForm.safeParse(programKemiskinanState.update.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
programKemiskinanState.update.loading = true;
const response = await fetch(
`/api/ekonomi/programkemiskinan/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
nama: this.form.nama,
deskripsi: this.form.deskripsi,
ikonUrl: this.form.ikonUrl,
statistik: {
tahun: this.form.statistik.tahun,
jumlah: this.form.statistik.jumlah,
},
}),
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update program kemiskinan");
await programKemiskinanState.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal update program kemiskinan");
}
} catch (error) {
console.error("Error updating program kemiskinan:", error);
toast.error(
error instanceof Error
? error.message
: "Terjadi kesalahan saat update program kemiskinan"
);
return false;
} finally {
programKemiskinanState.update.loading = false;
}
},
reset() {
programKemiskinanState.update.id = "";
programKemiskinanState.update.form = { ...defaultForm };
},
},
delete: {
loading: false,
async delete(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
programKemiskinanState.delete.loading = true;
const response = await fetch(
`/api/ekonomi/programkemiskinan/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(
result.message || "Program kemiskinan berhasil dihapus"
);
await programKemiskinanState.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus program kemiskinan");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus program kemiskinan");
} finally {
programKemiskinanState.delete.loading = false;
}
},
},
});
export default programKemiskinanState;

View File

@@ -0,0 +1,202 @@
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
const templateGrafikSektorUnggulan = z.object({
name: z.string().min(2, "Nama harus diisi"),
description: z.string().min(2, "Deskripsi harus diisi"),
value: z.number().min(1, "Nilai harus diisi"),
});
interface SektorUnggulanForm {
id?: string;
name: string;
description: string;
value: number;
}
const defaultForm: SektorUnggulanForm = {
name: "",
description: "",
value: 0,
};
const grafikSektorUnggulan = proxy({
create: {
form: defaultForm,
loading: false,
async create() {
const cek = templateGrafikSektorUnggulan.safeParse(
grafikSektorUnggulan.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
grafikSektorUnggulan.create.loading = true;
const res = await ApiFetch.api.ekonomi.sektourunggulandesa[
"create"
].post(grafikSektorUnggulan.create.form);
if (res.status === 200) {
const id = res.data?.data?.id;
if (id) {
toast.success("Success create");
grafikSektorUnggulan.create.form = {
name: "",
description: "",
value: 0,
};
grafikSektorUnggulan.findMany.load();
return id;
}
}
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
} finally {
grafikSektorUnggulan.create.loading = false;
}
},
},
findMany: {
data: null as
| Prisma.SektorUnggulanDesaGetPayload<{
select: {
id: true;
name: true;
description: true;
value: true;
createdAt: true;
updatedAt: true;
};
}>[]
| null,
loading: false,
async load() {
const res = await ApiFetch.api.ekonomi.sektourunggulandesa[
"find-many"
].get();
if (res.status === 200) {
grafikSektorUnggulan.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.SektorUnggulanDesaGetPayload<{
select: {
id: true;
name: true;
description: true;
value: true;
createdAt: true;
updatedAt: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/ekonomi/sektourunggulandesa/${id}`);
if (res.ok) {
const data = await res.json();
grafikSektorUnggulan.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
grafikSektorUnggulan.findUnique.data = null;
}
} catch (error) {
console.error("Error loading grafik sektor unggulan desa:", error);
grafikSektorUnggulan.findUnique.data = null;
}
},
},
update: {
id: "",
form: { ...defaultForm },
loading: false,
async byId() {
// Method implementation if needed
},
async submit() {
const id = this.id;
if (!id) {
toast.warn("ID tidak valid");
return null;
}
const cek = templateGrafikSektorUnggulan.safeParse(this.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return null;
}
this.loading = true;
try {
const response = await fetch(`/api/ekonomi/sektourunggulandesa/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
});
const result = await response.json();
if (!response.ok || !result?.success) {
throw new Error(result?.message || "Gagal update data");
}
toast.success("Berhasil update data!");
await grafikSektorUnggulan.findMany.load();
return result.data;
} catch (error) {
console.error("Error update data:", error);
toast.error("Gagal update data grafik sektor unggulan desa");
} finally {
this.loading = false;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
grafikSektorUnggulan.delete.loading = true;
const response = await fetch(
`/api/ekonomi/sektourunggulandesa/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(
result.message || "Grafik sektor unggulan desa berhasil dihapus"
);
await grafikSektorUnggulan.findMany.load(); // refresh list
} else {
toast.error(
result?.message || "Gagal menghapus grafik sektor unggulan desa"
);
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error(
"Terjadi kesalahan saat menghapus grafik sektor unggulan desa"
);
} finally {
grafikSektorUnggulan.delete.loading = false;
}
},
},
});
export default grafikSektorUnggulan;

View File

@@ -0,0 +1,720 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { proxy } from "valtio";
import { z } from "zod";
import { toast } from "react-toastify";
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
const templatePosisiOrganisasi = z.object({
nama: z.string().min(1, "Nama harus diisi"),
deskripsi: z.string().optional(),
hierarki: z.number().int().positive("Hierarki harus angka positif"),
});
const posisiOrganisasiDefaultForm = {
nama: "",
deskripsi: "",
hierarki: 0,
};
const posisiOrganisasi = proxy({
create: {
form: { ...posisiOrganisasiDefaultForm },
loading: false,
async submit() {
const cek = templatePosisiOrganisasi.safeParse(this.form);
if (!cek.success) {
const err = cek.error.issues.map((v) => v.message).join("\n");
return toast.error(err);
}
try {
this.loading = true;
const res = await ApiFetch.api.ekonomi["struktur-organisasi"]["posisi-organisasi"]["create"].post(
this.form
);
if (res.status === 200) {
toast.success("Berhasil menambahkan posisi organisasi");
posisiOrganisasi.findMany.load();
this.reset();
} else {
toast.error(res.data?.message || "Gagal menambahkan posisi");
}
} catch (error) {
console.error("Create error:", error);
toast.error("Terjadi kesalahan saat menambahkan posisi");
} finally {
this.loading = false;
}
},
reset() {
this.form = { ...posisiOrganisasiDefaultForm };
},
},
edit: {
id: "",
form: { ...posisiOrganisasiDefaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/ekonomi/struktur-organisasi/posisi-organisasi/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
nama: data.nama,
deskripsi: data.deskripsi,
hierarki: data.hierarki,
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading posisi organisasi:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templatePosisiOrganisasi.safeParse(this.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
this.loading = true;
const response = await fetch(
`/api/ekonomi/struktur-organisasi/posisi-organisasi/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
nama: this.form.nama,
deskripsi: this.form.deskripsi,
hierarki: this.form.hierarki,
}),
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update posisi organisasi");
await posisiOrganisasi.findMany.load(); // refresh list
return true;
} else {
throw new Error(
result.message || "Gagal mengupdate posisi organisasi"
);
}
} catch (error) {
console.error("Error updating posisi organisasi:", error);
toast.error(
error instanceof Error
? error.message
: "Gagal mengupdate posisi organisasi"
);
return false;
} finally {
this.loading = false;
}
},
reset() {
this.id = "";
this.form = { ...posisiOrganisasiDefaultForm };
},
},
findMany: {
data: [] as Array<{
id: string;
nama: string;
deskripsi: string | null;
hierarki: number;
}>,
async load() {
try {
const res = await ApiFetch.api.ekonomi["struktur-organisasi"]["posisi-organisasi"]["find-many"].get();
if (res.status === 200) {
// The API now returns the id field, so we can use it directly
this.data = res.data?.data ?? [];
}
} catch (error) {
console.error("Find many error:", error);
this.data = [];
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
posisiOrganisasi.delete.loading = true;
const response = await fetch(
`/api/ekonomi/struktur-organisasi/posisi-organisasi/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Posisi organisasi berhasil dihapus");
await posisiOrganisasi.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus posisi organisasi");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus posisi organisasi");
} finally {
posisiOrganisasi.delete.loading = false;
}
},
},
});
const templatePegawai = z.object({
namaLengkap: z.string().min(1, "Nama wajib diisi"),
gelarAkademik: z.string().optional(),
imageId: z.string().nullable().optional(),
tanggalMasuk: z.string().optional(), // ISO format
email: z.string().email("Email tidak valid").optional(),
telepon: z.string().optional(),
alamat: z.string().optional(),
posisiId: z.string().min(1, "Posisi wajib diisi"),
isActive: z.boolean().default(true),
});
const pegawaiDefaultForm = {
namaLengkap: "",
gelarAkademik: "",
imageId: "",
tanggalMasuk: "",
email: "",
telepon: "",
alamat: "",
posisiId: "",
isActive: true,
};
const pegawai = proxy({
create: {
form: { ...pegawaiDefaultForm },
loading: false,
async submit() {
const cek = templatePegawai.safeParse(pegawai.create.form);
if (!cek.success) {
const err = cek.error.issues.map(i => i.message).join("\n");
toast.error(err);
return;
}
try {
pegawai.create.loading = true;
const res = await ApiFetch.api.ekonomi["struktur-organisasi"]["pegawai"]["create"].post(
pegawai.create.form
);
if (res.status === 200) {
toast.success("Pegawai berhasil ditambahkan");
await pegawai.findMany.load();
} else {
toast.error(res.data?.message ?? "Gagal tambah pegawai");
}
} catch (error) {
console.error("Gagal create:", error);
toast.error("Terjadi kesalahan saat menambahkan pegawai");
} finally {
pegawai.create.loading = false;
}
},
},
findMany: {
data: null as (Prisma.PegawaiGetPayload<{ include: { posisi: true, image: true } }> & { isActive: boolean })[] | null,
async load() {
try {
const res = await ApiFetch.api.ekonomi["struktur-organisasi"]["pegawai"]["find-many"].get();
if (res.status === 200) {
pegawai.findMany.data = (res.data?.data ?? []).map((item: any) => ({
...item,
posisi: item.posisi || { id: '', nama: '' }, // Ensure posisi exists with required fields
isActive: item.isActive ?? true // Default to true if not provided
}));
} else {
console.error('Failed to load pegawai:', res.data?.message);
}
} catch (error) {
console.error('Error loading pegawai:', error);
pegawai.findMany.data = [];
}
},
},
findUnique: {
data: null as (Prisma.PegawaiGetPayload<{ include: { posisi: true, image: true } }> & { isActive: boolean }) | null,
async load(id: string) {
const res = await fetch(`/api/ekonomi/struktur-organisasi/pegawai/${id}`);
if (res.ok) {
const json = await res.json();
pegawai.findUnique.data = json.data ? {
...json.data,
isActive: json.data.isActive ?? json.data.aktif ?? true // Fallback ke aktif:true jika tidak ada data
} : null;
} else {
pegawai.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
pegawai.delete.loading = true;
const res = await fetch(`/api/ekonomi/struktur-organisasi/pegawai/del/${id}`, {
method: "DELETE",
});
const json = await res.json();
if (res.ok) {
toast.success(json.message ?? "Berhasil hapus pegawai");
await pegawai.findMany.load();
} else {
toast.error(json.message ?? "Gagal hapus pegawai");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus");
} finally {
pegawai.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...pegawaiDefaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/ekonomi/struktur-organisasi/pegawai/${id}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
namaLengkap: data.namaLengkap,
gelarAkademik: data.gelarAkademik,
imageId: data.imageId,
tanggalMasuk: data.tanggalMasuk,
email: data.email,
telepon: data.telepon,
alamat: data.alamat,
posisiId: data.posisiId,
isActive: data.isActive,
};
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading berita:", error);
toast.error(error instanceof Error ? error.message : "Gagal memuat data");
return null;
}
},
async submit() {
const cek = templatePegawai.safeParse(pegawai.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
pegawai.edit.loading = true;
// Format tanggalMasuk to ISO string if it exists
const formattedTanggalMasuk = this.form.tanggalMasuk
? new Date(this.form.tanggalMasuk).toISOString()
: undefined;
const response = await fetch(`/api/ekonomi/struktur-organisasi/pegawai/${this.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
id: this.id,
namaLengkap: this.form.namaLengkap,
gelarAkademik: this.form.gelarAkademik,
imageId: this.form.imageId || null,
tanggalMasuk: formattedTanggalMasuk,
email: this.form.email,
telepon: this.form.telepon,
alamat: this.form.alamat,
posisiId: this.form.posisiId,
isActive: this.form.isActive,
}),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update pegawai");
await pegawai.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal update pegawai");
}
} catch (error) {
console.error("Error updating pegawai:", error);
toast.error(error instanceof Error ? error.message : "Terjadi kesalahan saat update pegawai");
return false;
} finally {
pegawai.edit.loading = false;
}
},
reset() {
pegawai.edit.id = "";
pegawai.edit.form = { ...pegawaiDefaultForm };
},
},
});
// Schema Zod untuk form validasi
const templateHubunganOrganisasiForm = z.object({
atasanId: z.string().min(1, "Atasan wajib dipilih"),
bawahanId: z.string().min(1, "Bawahan wajib dipilih"),
tipe: z.string().optional(),
});
// Default form state
const defaultHubunganOrganisasiForm = {
atasanId: "",
bawahanId: "",
tipe: "",
};
// ====================== STATE ===========================
const hubunganOrganisasi = proxy({
create: {
form: { ...defaultHubunganOrganisasiForm },
loading: false,
async create() {
const cek = templateHubunganOrganisasiForm.safeParse(
hubunganOrganisasi.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}: ${v.message}`)
.join("\n")}]`;
return toast.error(err);
}
try {
hubunganOrganisasi.create.loading = true;
const res = await ApiFetch.api.ekonomi["struktur-organisasi"]["hubungan-organisasi"]["create"].post(hubunganOrganisasi.create.form);
if (res.status === 200 && res.data?.success) {
hubunganOrganisasi.findMany.load();
return toast.success("Berhasil menambahkan hubungan organisasi");
} else {
return toast.error(res.data?.message || "Gagal menambahkan data");
}
} catch (error) {
console.error("Gagal create:", error);
toast.error("Terjadi kesalahan saat menambahkan");
} finally {
hubunganOrganisasi.create.loading = false;
}
},
},
findMany: {
data: null as Array<{
id: string;
atasanId: string;
bawahanId: string;
tipe?: string | null;
atasan: {
id: string;
namaLengkap: string;
gelarAkademik: string | null;
imageId: string | null;
tanggalMasuk: Date | null;
email: string | null;
telepon: string | null;
alamat: string | null;
posisiId: string;
isActive: boolean;
createdAt: Date;
updatedAt: Date;
};
bawahan: {
id: string;
namaLengkap: string;
gelarAkademik: string | null;
imageId: string | null;
tanggalMasuk: Date | null;
email: string | null;
telepon: string | null;
alamat: string | null;
posisiId: string;
isActive: boolean;
createdAt: Date;
updatedAt: Date;
};
}> | null,
async load() {
try {
const res = await ApiFetch.api.ekonomi["struktur-organisasi"]["hubungan-organisasi"]["find-many"].get();
if (res.status === 200) {
hubunganOrganisasi.findMany.data = (res.data?.data ?? []).map((item: any) => ({
...item,
atasan: item.atasan ? {
...item.atasan,
isActive: item.atasan.isActive ?? item.atasan.aktif ?? true
} : null,
bawahan: item.bawahan ? {
...item.bawahan,
isActive: item.bawahan.isActive ?? item.bawahan.aktif ?? true
} : null
}));
} else {
hubunganOrganisasi.findMany.data = [];
}
} catch (error) {
console.error("Fetch list error:", error);
toast.error("Gagal memuat data hubungan organisasi");
hubunganOrganisasi.findMany.data = [];
}
},
},
findUnique: {
data: null as {
id: string;
atasanId: string;
bawahanId: string;
tipe?: string | null;
atasan?: {
id: string;
namaLengkap: string;
gelarAkademik: string | null;
imageId: string;
tanggalMasuk: Date | null;
email: string | null;
telepon: string | null;
alamat: string | null;
posisiId: string;
aktif: boolean;
createdAt: Date;
updatedAt: Date;
};
bawahan?: {
id: string;
namaLengkap: string;
gelarAkademik: string | null;
imageId: string;
tanggalMasuk: Date | null;
email: string | null;
telepon: string | null;
alamat: string | null;
posisiId: string;
aktif: boolean;
createdAt: Date;
updatedAt: Date;
};
} | null,
async load(id: string) {
try {
const res = await fetch(`/api/ekonomi/struktur-organisasi/hubungan-organisasi/${id}`);
const result = await res.json();
if (res.ok && result?.success) {
hubunganOrganisasi.findUnique.data = result.data;
} else {
hubunganOrganisasi.findUnique.data = null;
toast.error(result?.message || "Gagal mengambil data");
}
} catch (error) {
console.error("Find unique error:", error);
hubunganOrganisasi.findUnique.data = null;
}
},
},
edit: {
id: "",
form: { ...defaultHubunganOrganisasiForm },
loading: false,
async load(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
const res = await fetch(`/api/ekonomi/struktur-organisasi/hubungan-organisasi/${id}`);
const result = await res.json();
if (res.ok && result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
atasanId: data.atasanId,
bawahanId: data.bawahanId,
tipe: data.tipe || "",
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading:", error);
toast.error(error instanceof Error ? error.message : "Gagal memuat data");
return null;
}
},
async update() {
const cek = templateHubunganOrganisasiForm.safeParse(this.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}: ${v.message}`)
.join("\n")}]`;
return toast.error(err);
}
try {
this.loading = true;
const res = await fetch(
`/api/ekonomi/struktur-organisasi/hubungan-organisasi/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
}
);
const result = await res.json();
if (res.ok && result.success) {
await hubunganOrganisasi.findMany.load();
toast.success("Berhasil mengupdate hubungan organisasi");
return true;
} else {
throw new Error(result?.message || "Gagal mengupdate");
}
} catch (error) {
console.error("Update error:", error);
toast.error(error instanceof Error ? error.message : "Gagal update");
return false;
} finally {
this.loading = false;
}
},
reset() {
hubunganOrganisasi.edit.id = "";
hubunganOrganisasi.edit.form = { ...defaultHubunganOrganisasiForm };
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
hubunganOrganisasi.delete.loading = true;
const res = await fetch(`/api/ekonomi/struktur-organisasi/hubungan-organisasi/del/${id}`, {
method: "DELETE",
});
const result = await res.json();
if (res.ok && result?.success) {
toast.success("Hubungan organisasi berhasil dihapus");
hubunganOrganisasi.findMany.load();
} else {
toast.error(result?.message || "Gagal menghapus hubungan organisasi");
}
} catch (error) {
console.error("Delete error:", error);
toast.error("Terjadi kesalahan saat menghapus");
} finally {
hubunganOrganisasi.delete.loading = false;
}
},
},
});
const strukturorganisasiState = proxy({
posisiOrganisasi,
pegawai,
hubunganOrganisasi,
});
export default strukturorganisasiState;

View File

@@ -0,0 +1,376 @@
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
const templateGrafikUsiaKerjaYangMenganggur = z.object({
usia18_25: z.string().min(1, "Data usia 18-25 harus diisi"),
usia26_35: z.string().min(1, "Data usia 26-35 harus diisi"),
usia36_45: z.string().min(1, "Data usia 36-45 harus diisi"),
usia46_keatas: z.string().min(1, "Data usia 46 keatas harus diisi"),
});
type GrafikUsiaKerjaYangMenganggur = Prisma.GrafikMenganggurBerdasarkanUsiaGetPayload<{
select: {
id: true;
usia18_25: true;
usia26_35: true;
usia36_45: true;
usia46_keatas: true;
};
}>;
const defaultForm: Omit<GrafikUsiaKerjaYangMenganggur, 'id'> & { id?: string } = {
usia18_25: "",
usia26_35: "",
usia36_45: "",
usia46_keatas: "",
};
const grafikBerdasarkanUsiaKerjaNganggur = proxy({
create: {
form: defaultForm,
loading: false,
async create() {
const cek = templateGrafikUsiaKerjaYangMenganggur.safeParse(
grafikBerdasarkanUsiaKerjaNganggur.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
grafikBerdasarkanUsiaKerjaNganggur.create.loading = true;
const res = await ApiFetch.api.ekonomi.grafikusiakerjayangmenganggur[
"create"
].post(grafikBerdasarkanUsiaKerjaNganggur.create.form);
if (res.status === 200) {
const id = res.data?.data?.id;
if (id) {
toast.success("Success create");
grafikBerdasarkanUsiaKerjaNganggur.create.form = {
usia18_25: "",
usia26_35: "",
usia36_45: "",
usia46_keatas: "",
};
grafikBerdasarkanUsiaKerjaNganggur.findMany.load();
return id;
}
}
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
} finally {
grafikBerdasarkanUsiaKerjaNganggur.create.loading = false;
}
},
},
findMany: {
data: null as
| Prisma.GrafikMenganggurBerdasarkanUsiaGetPayload<{
omit: { isActive: true };
}>[]
| null,
loading: false,
async load() {
const res = await ApiFetch.api.ekonomi.grafikusiakerjayangmenganggur[
"find-many"
].get();
if (res.status === 200) {
grafikBerdasarkanUsiaKerjaNganggur.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.GrafikMenganggurBerdasarkanUsiaGetPayload<{
omit: { isActive: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(
`/api/ekonomi/grafikusiakerjayangmenganggur/${id}`
);
if (res.ok) {
const data = await res.json();
grafikBerdasarkanUsiaKerjaNganggur.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
grafikBerdasarkanUsiaKerjaNganggur.findUnique.data = null;
}
} catch (error) {
console.error("Error loading grafik berdasarkan usia kerja yang menganggur:", error);
grafikBerdasarkanUsiaKerjaNganggur.findUnique.data = null;
}
},
},
update: {
id: "",
form: {...defaultForm},
loading: false,
async byId() {
// Method implementation if needed
},
async submit() {
const id = this.id;
if (!id) {
toast.warn("ID tidak valid");
return null;
}
const cek = templateGrafikUsiaKerjaYangMenganggur.safeParse(this.form);
if (!cek.success) {
const err = `[${cek.error.issues.map((v) => `${v.path.join(".")}`).join("\n")}] required`;
toast.error(err);
return null;
}
this.loading = true;
try {
const response = await fetch(
`/api/ekonomi/grafikusiakerjayangmenganggur/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
});
const result = await response.json();
if (!response.ok || !result?.success) {
throw new Error(result?.message || "Gagal update data");
}
toast.success("Berhasil update data!");
await grafikBerdasarkanUsiaKerjaNganggur.findMany.load();
return result.data;
} catch (error) {
console.error("Error update data:", error);
toast.error("Gagal update data grafik berdasarkan usia kerja yang menganggur");
} finally {
this.loading = false;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
grafikBerdasarkanUsiaKerjaNganggur.delete.loading = true;
const response = await fetch(`/api/ekonomi/grafikusiakerjayangmenganggur/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Grafik berdasarkan usia kerja yang menganggur berhasil dihapus");
await grafikBerdasarkanUsiaKerjaNganggur.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus grafik berdasarkan usia kerja yang menganggur");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus grafik berdasarkan usia kerja yang menganggur");
} finally {
grafikBerdasarkanUsiaKerjaNganggur.delete.loading = false;
}
},
}
});
const templateGrafikBerpendidikanYangMenganggur = z.object({
SD: z.string().min(1, "Data SD harus diisi"),
SMP: z.string().min(1, "Data SMP harus diisi"),
SMA: z.string().min(1, "Data SMA harus diisi"),
D3: z.string().min(1, "Data D3 harus diisi"),
S1: z.string().min(1, "Data S1 harus diisi"),
});
type GrafikBerpendidikanYangMenganggur = Prisma.GrafikMenganggurBerdasarkanPendidikanGetPayload<{
select: {
id: true;
SD: true;
SMP: true;
SMA: true;
D3: true;
S1: true;
};
}>;
const defaultFormBerpendidikan: Omit<GrafikBerpendidikanYangMenganggur, 'id'> & { id?: string } = {
SD: "",
SMP: "",
SMA: "",
D3: "",
S1: "",
};
const grafikBerdasarkanPendidikan = proxy({
create: {
form: defaultFormBerpendidikan,
loading: false,
async create() {
const cek = templateGrafikBerpendidikanYangMenganggur.safeParse(
grafikBerdasarkanPendidikan.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
grafikBerdasarkanPendidikan.create.loading = true;
const res = await ApiFetch.api.ekonomi.grafikmenganggurberdasarkanpendidikan[
"create"
].post(grafikBerdasarkanPendidikan.create.form);
if (res.status === 200) {
const id = res.data?.data?.id;
if (id) {
toast.success("Success create");
grafikBerdasarkanPendidikan.create.form = {
SD: "",
SMP: "",
SMA: "",
D3: "",
S1: "",
};
grafikBerdasarkanPendidikan.findMany.load();
return id;
}
}
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
} finally {
grafikBerdasarkanPendidikan.create.loading = false;
}
},
},
findMany: {
data: null as
| Prisma.GrafikMenganggurBerdasarkanPendidikanGetPayload<{
omit: { isActive: true };
}>[]
| null,
loading: false,
async load() {
const res = await ApiFetch.api.ekonomi.grafikmenganggurberdasarkanpendidikan[
"find-many"
].get();
if (res.status === 200) {
grafikBerdasarkanPendidikan.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.GrafikMenganggurBerdasarkanPendidikanGetPayload<{
omit: { isActive: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(
`/api/ekonomi/grafikmenganggurberdasarkanpendidikan/${id}`
);
if (res.ok) {
const data = await res.json();
grafikBerdasarkanPendidikan.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
grafikBerdasarkanPendidikan.findUnique.data = null;
}
} catch (error) {
console.error("Error loading grafik berdasarkan usia kerja yang menganggur:", error);
grafikBerdasarkanPendidikan.findUnique.data = null;
}
},
},
update: {
id: "",
form: {...defaultFormBerpendidikan},
loading: false,
async byId() {
// Method implementation if needed
},
async submit() {
const id = this.id;
if (!id) {
toast.warn("ID tidak valid");
return null;
}
const cek = templateGrafikBerpendidikanYangMenganggur.safeParse(this.form);
if (!cek.success) {
const err = `[${cek.error.issues.map((v) => `${v.path.join(".")}`).join("\n")}] required`;
toast.error(err);
return null;
}
this.loading = true;
try {
const response = await fetch(
`/api/ekonomi/grafikmenganggurberdasarkanpendidikan/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
});
const result = await response.json();
if (!response.ok || !result?.success) {
throw new Error(result?.message || "Gagal update data");
}
toast.success("Berhasil update data!");
await grafikBerdasarkanPendidikan.findMany.load();
return result.data;
} catch (error) {
console.error("Error update data:", error);
toast.error("Gagal update data grafik berdasarkan pendidikan yang menganggur");
} finally {
this.loading = false;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
grafikBerdasarkanPendidikan.delete.loading = true;
const response = await fetch(`/api/ekonomi/grafikmenganggurberdasarkanpendidikan/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Grafik berdasarkan pendidikan yang menganggur berhasil dihapus");
await grafikBerdasarkanPendidikan.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus grafik berdasarkan pendidikan yang menganggur");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus grafik berdasarkan pendidikan yang menganggur");
} finally {
grafikBerdasarkanPendidikan.delete.loading = false;
}
},
}
});
const grafikNganggur = proxy({
grafikBerdasarkanUsiaKerjaNganggur,
grafikBerdasarkanPendidikan
})
export default grafikNganggur;

View File

@@ -179,17 +179,20 @@ const keamananLingkunganState = proxy({
try {
keamananLingkunganState.edit.loading = true;
const response = await fetch(`/api/keamanan/keamananlingkungan/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
deskripsi: this.form.deskripsi,
imageId: this.form.imageId,
}),
});
const response = await fetch(
`/api/keamanan/keamananlingkungan/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
deskripsi: this.form.deskripsi,
imageId: this.form.imageId,
}),
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));

View File

@@ -0,0 +1,259 @@
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
const templateForm = z.object({
nama: z.string().min(1, "Nama minimal 1 karakter"),
imageId: z.string().nonempty(),
kontakItems: z.array(
z.object({
nama: z.string().min(1, "Nama minimal 1 karakter"),
nomorTelepon: z.string().min(1, "Nomor Telepon minimal 1 karakter"),
imageId: z.string().nonempty(),
})
),
});
const defaultForm = {
nama: "",
imageId: "",
kontakItems: [
{
nama: "",
nomorTelepon: "",
imageId: "",
},
],
};
const kontakDaruratKeamananState = proxy({
create: {
form: { ...defaultForm },
loading: false,
async create() {
const cek = templateForm.safeParse(
kontakDaruratKeamananState.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
kontakDaruratKeamananState.create.loading = true;
const res = await ApiFetch.api.keamanan.kontakdaruratkeamanan[
"create"
].post(kontakDaruratKeamananState.create.form);
if (res.status === 200) {
kontakDaruratKeamananState.findMany.load();
return toast.success("success create");
}
console.log(res);
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
} finally {
kontakDaruratKeamananState.create.loading = false;
}
},
},
findMany: {
data: null as
| Prisma.KontakDaruratKeamananGetPayload<{
include: {
kontakItems: true;
image: true;
};
}>[]
| null,
async load() {
const res = await ApiFetch.api.keamanan.kontakdaruratkeamanan[
"find-many"
].get();
if (res.status === 200) {
kontakDaruratKeamananState.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.KontakDaruratKeamananGetPayload<{
include: {
kontakItems: {
include: {
image: true;
};
};
image: true;
};
}> | null,
loading: false,
async load(id: string) {
try {
const res = await fetch(`/api/keamanan/kontakdaruratkeamanan/${id}`);
if (res.ok) {
const data = await res.json();
kontakDaruratKeamananState.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
kontakDaruratKeamananState.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
kontakDaruratKeamananState.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
kontakDaruratKeamananState.delete.loading = true;
const response = await fetch(
`/api/keamanan/kontakdaruratkeamanan/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Kontak darurat berhasil dihapus");
await kontakDaruratKeamananState.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus kontak darurat");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus kontak darurat");
} finally {
kontakDaruratKeamananState.delete.loading = false;
}
},
},
update: {
id: "",
form: { ...defaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(
`/api/keamanan/kontakdaruratkeamanan/${id}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
}
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
nama: data.nama,
imageId: data.imageId,
kontakItems: [
{
nama: data.kontakItems.nama,
nomorTelepon: data.kontakItems.nomorTelepon,
imageId: data.kontakItems.imageId,
},
],
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading kontak darurat:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateForm.safeParse(
kontakDaruratKeamananState.update.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
kontakDaruratKeamananState.update.loading = true;
const response = await fetch(
`/api/keamanan/kontakdaruratkeamanan/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
nama: this.form.nama,
imageId: this.form.imageId,
kontakItems: [
{
nama: this.form.kontakItems[0].nama,
nomorTelepon: this.form.kontakItems[0].nomorTelepon,
imageId: this.form.kontakItems[0].imageId,
},
],
}),
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update kontak darurat");
await kontakDaruratKeamananState.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal mengupdate kontak darurat");
}
} catch (error) {
console.error("Error updating kontak darurat:", error);
toast.error(
error instanceof Error
? error.message
: "Gagal mengupdate kontak darurat"
);
return false;
} finally {
kontakDaruratKeamananState.update.loading = false;
}
},
reset() {
kontakDaruratKeamananState.update.id = "";
kontakDaruratKeamananState.update.form = { ...defaultForm };
},
},
});
export default kontakDaruratKeamananState;

View File

@@ -0,0 +1,273 @@
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
export type Status = "Selesai" | "Proses" | "Gagal";
const templateForm = z.object({
judul: z.string().min(3, "Judul minimal 3 karakter"),
lokasi: z.string().min(3, "Lokasi minimal 3 karakter"),
tanggalWaktu: z.string().min(3, "Tanggal Waktu minimal 3 karakter"),
status: z.enum(["Selesai", "Proses", "Gagal"]),
penanganan: z.string(),
kronologi: z.string().optional(),
});
interface FormData {
judul: string;
lokasi: string;
tanggalWaktu: string;
status: Status;
penanganan: string;
kronologi: string;
}
const defaultForm: FormData = {
judul: "",
lokasi: "",
tanggalWaktu: new Date().toISOString(),
status: "Proses",
penanganan: "",
kronologi: "",
};
const laporanPublikState = proxy({
create: {
form: { ...defaultForm },
loading: false,
async create() {
const cek = templateForm.safeParse(laporanPublikState.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
laporanPublikState.create.loading = true;
// Ensure we have a valid date
if (!laporanPublikState.create.form.tanggalWaktu) {
return toast.error("Tanggal laporan harus diisi");
}
// Format the data before sending
const formData = {
...laporanPublikState.create.form,
// Ensure the date is in the correct format for the API
tanggalWaktu: new Date(laporanPublikState.create.form.tanggalWaktu).toISOString()
};
console.log("Sending form data:", formData); // Debug log
const res = await ApiFetch.api.keamanan.laporanpublik["create"].post(
formData
);
if (res.error) {
console.error("API Error:", res.error);
throw new Error("Failed to create laporan publik");
}
if (res.status === 200) {
laporanPublikState.findMany.load();
return toast.success("success create");
}
console.log(res);
return toast.error("failed create");
} catch (error) {
console.error("Error creating laporan publik:", error);
toast.error(error instanceof Error ? error.message : "Gagal membuat laporan publik");
throw error; // Re-throw to be handled by the caller
} finally {
laporanPublikState.create.loading = false;
}
},
resetForm() {
laporanPublikState.create.form = { ...defaultForm };
},
},
findMany: {
data: null as
| Prisma.LaporanPublikGetPayload<{
include: { penanganan: true };
}>[]
| null,
async load() {
const res = await ApiFetch.api.keamanan.laporanpublik["find-many"].get();
if (res.status === 200) {
laporanPublikState.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.LaporanPublikGetPayload<{
include: { penanganan: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/keamanan/laporanpublik/${id}`);
if (res.ok) {
const data = await res.json();
laporanPublikState.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
laporanPublikState.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
laporanPublikState.findUnique.data = null;
}
},
resetForm() {
laporanPublikState.findUnique.data = null;
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
laporanPublikState.delete.loading = true;
const response = await fetch(`/api/keamanan/laporanpublik/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(
result.message || "Laporan publik berhasil dihapus"
);
await laporanPublikState.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus laporan publik");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus laporan publik");
} finally {
laporanPublikState.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...defaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/keamanan/laporanpublik/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
judul: data.judul,
lokasi: data.lokasi,
tanggalWaktu: data.tanggalWaktu,
status: data.status,
penanganan: data.penanganan,
kronologi: data.kronologi,
};
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading keamanan lingkungan:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateForm.safeParse(laporanPublikState.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
laporanPublikState.edit.loading = true;
const response = await fetch(
`/api/keamanan/laporanpublik/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
judul: this.form.judul,
lokasi: this.form.lokasi,
tanggalWaktu: this.form.tanggalWaktu,
status: this.form.status,
penanganan: this.form.penanganan,
kronologi: this.form.kronologi,
}),
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update laporan publik");
await laporanPublikState.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal update laporan publik");
}
} catch (error) {
console.error("Error updating laporan publik:", error);
toast.error(
error instanceof Error
? error.message
: "Terjadi kesalahan saat update laporan publik"
);
return false;
} finally {
laporanPublikState.edit.loading = false;
}
},
reset() {
laporanPublikState.edit.id = "";
laporanPublikState.edit.form = { ...defaultForm };
},
}
});
export default laporanPublikState;

View File

@@ -0,0 +1,314 @@
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
const templateForm = z.object({
pencegahanKriminalitas: z.object({
programKeamanan: z.object({
nama: z.string().min(1, "Nama minimal 1 karakter"),
deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
slug: z.string().min(1, "Slug minimal 1 karakter"),
}),
tipsKeamanan: z.object({
judul: z.string().min(1, "Judul minimal 1 karakter"),
konten: z.string().min(1, "Konten minimal 1 karakter"),
slug: z.string().min(1, "Slug minimal 1 karakter"),
}),
videoKeamanan: z.object({
judul: z.string().min(1, "Judul minimal 1 karakter"),
deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
videoUrl: z.string().min(1, "Video URL minimal 1 karakter"),
slug: z.string().min(1, "Slug minimal 1 karakter"),
}),
}),
});
const defaultForm = {
pencegahanKriminalitas: {
programKeamanan: {
nama: "",
deskripsi: "",
slug: "",
},
tipsKeamanan: {
judul: "",
konten: "",
slug: "",
},
videoKeamanan: {
judul: "",
deskripsi: "",
videoUrl: "",
slug: "",
},
},
};
const pencegahanKriminalitasState = proxy({
create: {
form: { ...defaultForm },
loading: false,
async create() {
const cek = templateForm.safeParse(
pencegahanKriminalitasState.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
pencegahanKriminalitasState.create.loading = true;
const res = await ApiFetch.api.keamanan.pencegahankriminalitas[
"create"
].post(pencegahanKriminalitasState.create.form.pencegahanKriminalitas);
if (res.status === 200) {
pencegahanKriminalitasState.findMany.load();
return toast.success("success create");
}
console.log(res);
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
} finally {
pencegahanKriminalitasState.create.loading = false;
}
},
},
findMany: {
data: null as
| Prisma.PencegahanKriminalitasGetPayload<{
include: {
programKeamanan: true;
tipsKeamanan: true;
videoKeamanan: true;
};
}>[]
| null,
async load() {
const res = await ApiFetch.api.keamanan.pencegahankriminalitas[
"find-many"
].get();
if (res.status === 200) {
pencegahanKriminalitasState.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.PencegahanKriminalitasGetPayload<{
include: {
programKeamanan: true;
tipsKeamanan: true;
videoKeamanan: true;
};
}> | null,
loading: false,
async load(id: string) {
try {
const res = await fetch(`/api/keamanan/pencegahankriminalitas/${id}`);
if (res.ok) {
const data = await res.json();
pencegahanKriminalitasState.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
pencegahanKriminalitasState.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
pencegahanKriminalitasState.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
pencegahanKriminalitasState.delete.loading = true;
const response = await fetch(
`/api/keamanan/pencegahankriminalitas/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(
result.message || "Pencegahan kriminalitas berhasil dihapus"
);
await pencegahanKriminalitasState.findMany.load(); // refresh list
} else {
toast.error(
result?.message || "Gagal menghapus pencegahan kriminalitas"
);
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus pencegahan kriminalitas");
} finally {
pencegahanKriminalitasState.delete.loading = false;
}
},
},
update: {
id: "",
form: { ...defaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
pencegahanKriminalitasState.update.loading = true;
const response = await fetch(
`/api/keamanan/pencegahankriminalitas/${id}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
}
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
pencegahanKriminalitasState.update.id = data.id;
pencegahanKriminalitasState.update.form = {
pencegahanKriminalitas: {
programKeamanan: {
nama: data.programKeamanan.nama,
deskripsi: data.programKeamanan.deskripsi,
slug: data.programKeamanan.slug,
},
tipsKeamanan: {
judul: data.tipsKeamanan.judul,
konten: data.tipsKeamanan.konten,
slug: data.tipsKeamanan.slug,
},
videoKeamanan: {
judul: data.videoKeamanan.judul,
deskripsi: data.videoKeamanan.deskripsi,
videoUrl: data.videoKeamanan.videoUrl,
slug: data.videoKeamanan.slug,
},
},
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Gagal update:", error);
toast.error(
"Terjadi kesalahan saat mengupdate pencegahan kriminalitas"
);
return null;
}
},
async update() {
const cek = templateForm.safeParse(
pencegahanKriminalitasState.update.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
pencegahanKriminalitasState.update.loading = true;
const response = await fetch(
`/api/keamanan/pencegahankriminalitas/${pencegahanKriminalitasState.update.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
pencegahanKriminalitas: {
programKeamanan: {
nama: pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.programKeamanan.nama,
deskripsi:
pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.programKeamanan.deskripsi,
slug: pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.programKeamanan.slug,
},
tipsKeamanan: {
judul:
pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.tipsKeamanan.judul,
konten:
pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.tipsKeamanan.konten,
slug: pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.tipsKeamanan.slug,
},
videoKeamanan: {
judul:
pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.videoKeamanan.judul,
deskripsi:
pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.videoKeamanan.deskripsi,
videoUrl:
pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.videoKeamanan.videoUrl,
slug: pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.videoKeamanan.slug,
},
},
}),
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update pencegahan kriminalitas");
await pencegahanKriminalitasState.findMany.load(); // refresh list
return true;
} else {
throw new Error(
result.message || "Gagal mengupdate pencegahan kriminalitas"
);
}
} catch (error) {
console.error("Gagal update:", error);
toast.error(
error instanceof Error
? error.message
: "Gagal mengupdate pencegahan kriminalitas"
);
return false;
} finally {
pencegahanKriminalitasState.update.loading = false;
}
},
reset() {
pencegahanKriminalitasState.update.id = "";
pencegahanKriminalitasState.update.form = { ...defaultForm };
},
},
});
export default pencegahanKriminalitasState;

View File

@@ -5,16 +5,16 @@ import { proxy } from "valtio";
import { z } from "zod";
const templateForm = z.object({
nama: z.string().min(3, "Nama minimal 3 karakter"),
jarakKeDesa: z.string().min(3, "Jarak minimal 3 karakter"),
alamat: z.string().min(3, "Alamat minimal 3 karakter"),
nomorTelepon: z.string().min(3, "Nomor Telepon minimal 3 karakter"),
jamOperasional: z.string().min(3, "Jam Operasional minimal 3 karakter"),
embedMapUrl: z.string().min(3, "Embed Map Url minimal 3 karakter"),
namaTempatMaps: z.string().min(3, "Nama Tempat Maps minimal 3 karakter"),
alamatMaps: z.string().min(3, "Alamat Maps minimal 3 karakter"),
linkPetunjukArah: z.string().min(3, "Link Petunjuk Arah minimal 3 karakter"),
layananPolsekId: z.string().min(3, "Layanan Polsek Id minimal 3 karakter"),
nama: z.string().min(1, "Nama minimal 1 karakter"),
jarakKeDesa: z.string().min(1, "Jarak minimal 1 karakter"),
alamat: z.string().min(1, "Alamat minimal 1 karakter"),
nomorTelepon: z.string().min(1, "Nomor Telepon minimal 1 karakter"),
jamOperasional: z.string().min(1, "Jam Operasional minimal 1 karakter"),
embedMapUrl: z.string().min(1, "Embed Map Url minimal 1 karakter"),
namaTempatMaps: z.string().min(1, "Nama Tempat Maps minimal 1 karakter"),
alamatMaps: z.string().min(1, "Alamat Maps minimal 1 karakter"),
linkPetunjukArah: z.string().min(1, "Link Petunjuk Arah minimal 1 karakter"),
layananPolsekId: z.string().min(1, "Layanan Polsek Id minimal 1 karakter"),
});
const defaultForm = {

View File

@@ -34,15 +34,17 @@ const persentasekelahiran = proxy({
async create() {
const cek = templatePersentase.safeParse(persentasekelahiran.create.form);
if (!cek.success) {
const err = `[${cek.error.issues.map((v) => `${v.path.join(".")}`).join("\n")}] required`;
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return null;
}
try {
persentasekelahiran.create.loading = true;
const res = await ApiFetch.api.kesehatan.persentasekelahiran["create"].post(
persentasekelahiran.create.form
);
const res = await ApiFetch.api.kesehatan.persentasekelahiran[
"create"
].post(persentasekelahiran.create.form);
if (res.status === 200) {
const id = res.data?.data?.id;
@@ -65,11 +67,15 @@ const persentasekelahiran = proxy({
},
findMany: {
data: null as Prisma.DataKematian_KelahiranGetPayload<{
omit: { isActive: true };
}>[] | null,
data: null as
| Prisma.DataKematian_KelahiranGetPayload<{
omit: { isActive: true };
}>[]
| null,
async load() {
const res = await ApiFetch.api.kesehatan.persentasekelahiran["find-many"].get();
const res = await ApiFetch.api.kesehatan.persentasekelahiran[
"find-many"
].get();
if (res.status === 200) {
persentasekelahiran.findMany.data = res.data?.data ?? [];
}
@@ -97,62 +103,61 @@ const persentasekelahiran = proxy({
},
},
update: {
id: "",
form: { ...defaultForm },
loading: false,
async submit() {
const id = this.id;
if (!id) {
toast.warn("ID tidak valid");
return null;
}
const formData = {
tahun: this.form.tahun,
kematianKasar: this.form.kematianKasar,
kelahiranKasar: this.form.kelahiranKasar,
kematianBayi: this.form.kematianBayi,
};
const cek = templatePersentase.safeParse(formData);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return null;
}
try {
this.loading = true;
const res = await fetch(`/api/kesehatan/persentasekelahiran/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(formData),
});
const result = await res.json();
if (!res.ok || !result?.success) {
throw new Error(result?.message || "Gagal update data");
update: {
id: "",
form: { ...defaultForm },
loading: false,
async submit() {
const id = this.id;
if (!id) {
toast.warn("ID tidak valid");
return null;
}
toast.success("Berhasil update data!");
await persentasekelahiran.findMany.load();
return result.data;
} catch (error) {
console.error("Update error:", error);
toast.error("Gagal update data persentase kelahiran");
throw error;
} finally {
this.loading = false;
}
},
},
const formData = {
tahun: this.form.tahun,
kematianKasar: this.form.kematianKasar,
kelahiranKasar: this.form.kelahiranKasar,
kematianBayi: this.form.kematianBayi,
};
const cek = templatePersentase.safeParse(formData);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return null;
}
try {
this.loading = true;
const res = await fetch(`/api/kesehatan/persentasekelahiran/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(formData),
});
const result = await res.json();
if (!res.ok || !result?.success) {
throw new Error(result?.message || "Gagal update data");
}
toast.success("Berhasil update data!");
await persentasekelahiran.findMany.load();
return result.data;
} catch (error) {
console.error("Update error:", error);
toast.error("Gagal update data persentase kelahiran");
throw error;
} finally {
this.loading = false;
}
},
},
delete: {
loading: false,
@@ -162,22 +167,29 @@ update: {
try {
persentasekelahiran.delete.loading = true;
const response = await fetch(`/api/kesehatan/persentasekelahiran/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const response = await fetch(
`/api/kesehatan/persentasekelahiran/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Persentase kelahiran berhasil dihapus");
toast.success(
result.message || "Persentase kelahiran berhasil dihapus"
);
await persentasekelahiran.findMany.load();
} else {
toast.error(result?.message || "Gagal menghapus persentase kelahiran");
toast.error(
result?.message || "Gagal menghapus persentase kelahiran"
);
}
} catch (error) {
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus persentase kelahiran");
} finally {

View File

@@ -1,43 +1,47 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Grid, GridCol, Image, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, TextInput, Title } from '@mantine/core';
import { Box, Button, Grid, GridCol, Image, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import stateDashboardBerita from '../../_state/desa/berita';
function Page() {
function Berita() {
const [search, setSearch] = useState("");
return (
<Box>
<Grid>
<GridCol span={{ base: 12, md: 9 }}>
<Title order={3}>Berita</Title>
</GridCol>
<GridCol span={{ base: 12, md: 3 }}>
<Paper radius={"lg"} bg={colors['white-1']}>
<TextInput
radius={"lg"}
placeholder='pencarian'
leftSection={<IconSearch size={20} />}
/>
</Paper>
</GridCol>
</Grid>
<BeritaList />
<HeaderSearch
title='Berita'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListBerita search={search} />
</Box>
);
}
function BeritaList() {
function ListBerita({ search }: { search: string }) {
const beritaState = useProxy(stateDashboardBerita)
const router = useRouter()
useShallowEffect(() => {
beritaState.berita.findMany.load()
}, [])
const router = useRouter()
const filteredData = (beritaState.berita.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.judul.toLowerCase().includes(keyword) ||
item.kategoriBerita?.name.toLowerCase().includes(keyword)
);
});
if (!beritaState.berita.findMany.data) {
return (
@@ -62,7 +66,7 @@ function BeritaList() {
</GridCol>
</Grid>
<Box style={{ overflowX: "auto" }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
<TableThead>
<TableTr>
<TableTh w={250}>Judul</TableTh>
@@ -73,7 +77,7 @@ function BeritaList() {
</TableTr>
</TableThead>
<TableTbody >
{beritaState.berita.findMany.data?.map((item) => (
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd >
<Box w={100}>
@@ -101,4 +105,4 @@ function BeritaList() {
)
}
export default Page;
export default Berita;

View File

@@ -7,8 +7,26 @@ import JudulListTab from '../../../_com/jusulListTab';
import { useProxy } from 'valtio/utils';
import stateGallery from '../../../_state/desa/gallery';
import { useShallowEffect } from '@mantine/hooks';
import HeaderSearch from '../../../_com/header';
import { useState } from 'react';
function Foto() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Posisi Organisasi'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListFoto search={search} />
</Box>
);
}
function ListFoto({ search }: { search: string }) {
const fotoState = useProxy(stateGallery.foto)
const router = useRouter();
@@ -16,6 +34,14 @@ function Foto() {
fotoState.findMany.load()
}, [])
const filteredData = (fotoState.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.deskripsi.toLowerCase().includes(keyword)
);
});
if (!fotoState.findMany.data) {
return (
<Box py={10}>
@@ -43,7 +69,7 @@ function Foto() {
</TableTr>
</TableThead>
<TableTbody>
{fotoState.findMany.data?.map((item) => (
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>{new Date(item.createdAt).toDateString()}</TableTd>

View File

@@ -7,8 +7,26 @@ import JudulListTab from '../../../_com/jusulListTab';
import { useProxy } from 'valtio/utils';
import stateGallery from '../../../_state/desa/gallery';
import { useShallowEffect } from '@mantine/hooks';
import HeaderSearch from '../../../_com/header';
import { useState } from 'react';
function Video() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Posisi Organisasi'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListVideo search={search} />
</Box>
);
}
function ListVideo({ search }: { search: string }) {
const videoState = useProxy(stateGallery.video)
const router = useRouter();
@@ -16,6 +34,14 @@ function Video() {
videoState.findMany.load()
}, [])
const filteredData = (videoState.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.deskripsi.toLowerCase().includes(keyword)
);
});
if (!videoState.findMany.data) {
return (
<Box py={10}>
@@ -43,7 +69,7 @@ function Video() {
</TableTr>
</TableThead>
<TableTbody>
{videoState.findMany.data?.map((item) => (
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>{new Date(item.createdAt).toDateString()}</TableTd>

View File

@@ -7,8 +7,26 @@ import { useProxy } from 'valtio/utils';
import stateLayananDesa from '../../../_state/desa/layananDesa';
import { useShallowEffect } from '@mantine/hooks';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import HeaderSearch from '../../../_com/header';
function SuratKeterangan() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Posisi Organisasi'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListSuratKeterangan search={search} />
</Box>
);
}
function ListSuratKeterangan({ search }: { search: string }) {
const suratKeteranganState = useProxy(stateLayananDesa.suratKeterangan)
const router = useRouter()
@@ -16,6 +34,14 @@ function SuratKeterangan() {
suratKeteranganState.findMany.load()
}, [])
const filteredData = (suratKeteranganState.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.deskripsi.toLowerCase().includes(keyword)
);
});
if (!suratKeteranganState.findMany.data) {
return (
<Stack py={10}>
@@ -43,7 +69,7 @@ function SuratKeterangan() {
</TableTr>
</TableThead>
<TableTbody>
{suratKeteranganState.findMany.data?.map((item) => (
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>

View File

@@ -7,8 +7,26 @@ import { useProxy } from 'valtio/utils';
import stateLayananDesa from '../../../_state/desa/layananDesa';
import { useShallowEffect } from '@mantine/hooks';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import HeaderSearch from '../../../_com/header';
function PelayananTelunjukSakti() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Posisi Organisasi'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListPelayananTelunjukSakti search={search} />
</Box>
);
}
function ListPelayananTelunjukSakti({ search }: { search: string }) {
const telunjukSaktiState = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa)
const router = useRouter()
@@ -16,6 +34,14 @@ function PelayananTelunjukSakti() {
telunjukSaktiState.findMany.load()
}, [])
const filteredData = (telunjukSaktiState.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.deskripsi.toLowerCase().includes(keyword)
);
});
if (!telunjukSaktiState.findMany.data) {
return (
<Stack py={10}>
@@ -42,7 +68,7 @@ function PelayananTelunjukSakti() {
</TableTr>
</TableThead>
<TableTbody>
{telunjukSaktiState.findMany.data?.map((item) => (
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd><Text truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} /></TableTd>

View File

@@ -7,14 +7,40 @@ import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import JudulListTab from '../../_com/jusulListTab';
import { useState } from 'react';
import HeaderSearch from '../../_com/header';
function Penghargaan() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Posisi Organisasi'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListPenghargaan search={search} />
</Box>
);
}
function ListPenghargaan({ search }: { search: string }) {
const state = useProxy(penghargaanState)
const router = useRouter()
useShallowEffect(() => {
state.findMany.load()
}, [])
const filteredData = (state.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.deskripsi.toLowerCase().includes(keyword)
);
});
if (!state.findMany.data) {
return(
<Stack py={10}>
@@ -41,7 +67,7 @@ function Penghargaan() {
</TableTr>
</TableThead>
<TableTbody>
{state.findMany.data?.map((item) => (
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>

View File

@@ -11,21 +11,25 @@ import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus';
import { useState } from 'react';
function Page() {
function Pengumuman() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Pengumuman'
title='Posisi Organisasi'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<PengumumanList />
<ListPengumuman search={search} />
</Box>
);
}
function PengumumanList() {
function ListPengumuman({ search }: { search: string }) {
const pengumumanState = useProxy(stateDesaPengumuman)
const router = useRouter()
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
@@ -33,7 +37,6 @@ function PengumumanList() {
pengumumanState.pengumuman.findMany.load()
}, [])
const router = useRouter()
const handleHapus = () => {
if (selectedId) {
@@ -43,6 +46,14 @@ function PengumumanList() {
}
}
const filteredData = (pengumumanState.pengumuman.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.judul.toLowerCase().includes(keyword) ||
item.CategoryPengumuman?.name.toLowerCase().includes(keyword)
);
});
if (!pengumumanState.pengumuman.findMany.data) {
return (
<Stack py={10}>
@@ -76,7 +87,7 @@ function PengumumanList() {
</TableTr>
</TableThead>
<TableTbody >
{pengumumanState.pengumuman.findMany.data?.map((item) => (
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd >
<Box w={100}>
@@ -108,4 +119,4 @@ function PengumumanList() {
)
}
export default Page;
export default Pengumuman;

View File

@@ -9,29 +9,40 @@ import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import potensiDesaState from '../../_state/desa/potensi';
import { useState } from 'react';
function Potensi() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Potensi Desa'
title='Posisi Organisasi'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListPotensi />
<ListPotensi search={search} />
</Box>
);
}
function ListPotensi() {
function ListPotensi({ search }: { search: string }) {
const potensiState = useProxy(potensiDesaState)
const router = useRouter()
useShallowEffect(() => {
potensiState.findMany.load()
}, [])
const router = useRouter()
const filteredData = (potensiState.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.kategori.toLowerCase().includes(keyword)
);
});
if (!potensiState.findMany.data) {
return (
@@ -60,7 +71,7 @@ function ListPotensi() {
</TableTr>
</TableThead>
<TableTbody>
{potensiState.findMany.data?.map((item) => (
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Box w={100}>

View File

@@ -0,0 +1,102 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import demografiPekerjaan from '../../../_state/ekonomi/demografi-pekerjaan';
function EditDemografiPekerjaan() {
const router = useRouter()
const params = useParams() as { id: string }
const stateDemografi = useProxy(demografiPekerjaan)
const id = params.id
useEffect(() => {
if (!id) return;
stateDemografi.update.id = id;
stateDemografi.findUnique.load(id)
.then(() => {
const data = stateDemografi.findUnique.data;
if (data) {
stateDemografi.update.form = {
pekerjaan: String(data.pekerjaan || ''),
lakiLaki: Number(data.lakiLaki || 0),
perempuan: Number(data.perempuan || 0)
};
}
})
.catch(error => {
console.error('Error loading data:', error);
toast.error('Gagal memuat data');
});
}, [id]);
// Di handleSubmit, ubah menjadi:
const handleSubmit = async () => {
try {
stateDemografi.update.id = id;
await stateDemografi.update.submit();
toast.success('Data berhasil diperbarui');
router.push('/admin/ekonomi/demografi-pekerjaan');
} catch (error) {
console.error('Error updating data:', error);
toast.error('Gagal memperbarui data');
}
};
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack size={20} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: '100%', md: '50%' }} p={'md'}>
<Stack>
<Title order={3}>Edit Demografi Pekerjaan</Title>
<TextInput
label="Pekerjaan"
placeholder="masukkan pekerjaan"
value={stateDemografi.update.form.pekerjaan}
onChange={(val) => {
stateDemografi.update.form.pekerjaan = val.currentTarget.value;
}}
/>
<TextInput
label="Jumlah Pekerja Laki - Laki"
type="number"
placeholder="masukkan jumlah pekerja laki - laki"
value={stateDemografi.update.form.lakiLaki}
onChange={(val) => {
stateDemografi.update.form.lakiLaki = Number(val.currentTarget.value);
}}
/>
<TextInput
label="Jumlah Pekerja Perempuan"
type="number"
placeholder="masukkan jumlah pekerja perempuan"
value={stateDemografi.update.form.perempuan}
onChange={(val) => {
stateDemografi.update.form.perempuan = Number(val.currentTarget.value);
}}
/>
<Button
mt={10}
bg={colors['blue-button']}
onClick={handleSubmit}
>
Simpan Perubahan
</Button>
</Stack>
</Paper>
</Box>
)
}
export default EditDemografiPekerjaan;

View File

@@ -1,41 +1,90 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { Box, Button, Group, Paper, Stack, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import demografiPekerjaan from '../../../_state/ekonomi/demografi-pekerjaan';
function CreateDemografiPekerjaan() {
const router = useRouter();
const stateDemografi = useProxy(demografiPekerjaan);
const [chartData, setChartData] = useState<any[]>([]);
const router = useRouter()
const resetForm = () => {
stateDemografi.create.form = {
pekerjaan: "",
lakiLaki: 0,
perempuan: 0,
}
}
const handleSubmit = async () => {
const id = await stateDemografi.create.create();
if (id) {
const idStr = String(id);
await stateDemografi.findUnique.load(idStr);
if (stateDemografi.findUnique.data) {
setChartData([stateDemografi.findUnique.data]);
}
}
resetForm();
router.push("/admin/ekonomi/demografi-pekerjaan");
}
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25}/>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack size={20} />
</Button>
</Box>
<Paper w={{base: '100%', md: '50%'}} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Demografi Pekerjaan</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Pekerjaan</Text>}
placeholder='Masukkan pekerjaan'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Jumlah Pekerja Laki - Laki</Text>}
placeholder='Masukkan jumlah pekerja laki - laki'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Jumlah Pekerja Perempuan</Text>}
placeholder='Masukkan jumlah pekerja perempuan'
/>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
<Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Title order={4}>Tambah Demografi Pekerjaan</Title>
<Stack gap={"xs"}>
<TextInput
label="Pekerjaan"
type="text"
value={stateDemografi.create.form.pekerjaan}
placeholder="Masukkan pekerjaan"
onChange={(val) => {
stateDemografi.create.form.pekerjaan = val.currentTarget.value;
}}
/>
<TextInput
label="Jumlah Pekerja Laki - Laki"
type="number"
value={stateDemografi.create.form.lakiLaki}
placeholder="Masukkan jumlah pekerja laki - laki"
onChange={(val) => {
stateDemografi.create.form.lakiLaki = Number(val.currentTarget.value);
}}
/>
<TextInput
label="Jumlah Pekerja Perempuan"
type="number"
value={stateDemografi.create.form.perempuan}
placeholder="Masukkan jumlah pekerja perempuan"
onChange={(val) => {
stateDemografi.create.form.perempuan = Number(val.currentTarget.value);
}}
/>
<Group>
<Button
bg={colors['blue-button']}
mt={10}
onClick={handleSubmit}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
</Box>
);
}

View File

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

View File

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

View File

@@ -1,26 +1,85 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { Box, Button, Paper, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { useMediaQuery, useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { Bar, BarChart, Legend, Tooltip, XAxis, YAxis } from 'recharts';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import { useRouter } from 'next/navigation';
import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus';
import demografiPekerjaan from '../../_state/ekonomi/demografi-pekerjaan';
function DemografiPekerjaan() {
const [search, setSearch] = useState('');
return (
<Box>
<HeaderSearch
title='Demografi Pekerjaan'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListDemografiPekerjaan/>
<ListDemografiPekerjaan search={search} />
</Box>
);
}
function ListDemografiPekerjaan() {
function ListDemografiPekerjaan({ search }: { search: string }) {
type DemografiPekerjaan = {
id: string;
pekerjaan: string;
lakiLaki: number;
perempuan: number;
}
const router = useRouter();
const stateDemografi = useProxy(demografiPekerjaan)
const [chartData, setChartData] = useState<DemografiPekerjaan[]>([]);
const [mounted, setMounted] = useState(false); // untuk memastikan DOM sudah ready
const isTablet = useMediaQuery('(max-width: 1024px)')
const isMobile = useMediaQuery('(max-width: 768px)')
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const handleDelete = () => {
if (selectedId) {
stateDemografi.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
stateDemografi.findMany.load()
}
}
useShallowEffect(() => {
setMounted(true)
stateDemografi.findMany.load()
}, [])
useEffect(() => {
setMounted(true);
if (stateDemografi.findMany.data) {
setChartData(stateDemografi.findMany.data.map((item) => ({
id: item.id,
pekerjaan: item.pekerjaan,
lakiLaki: Number(item.lakiLaki),
perempuan: Number(item.perempuan),
})));
}
}, [stateDemografi.findMany.data]);
const filteredData = (stateDemografi.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.pekerjaan.toLowerCase().includes(keyword) ||
item.lakiLaki.toString().toLowerCase().includes(keyword) ||
item.perempuan.toString().toLowerCase().includes(keyword)
);
});
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
@@ -34,23 +93,71 @@ function ListDemografiPekerjaan() {
<TableTh>Pekerjaan</TableTh>
<TableTh>Jumlah Pekerja Laki - Laki</TableTh>
<TableTh>Jumlah Pekerja Perempuan</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>Karyawan</TableTd>
<TableTd>200</TableTd>
<TableTd>100</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/ekonomi/demografi-pekerjaan/detail')}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.pekerjaan}</TableTd>
<TableTd>{item.lakiLaki}</TableTd>
<TableTd>{item.perempuan}</TableTd>
<TableTd>
<Button color='green' onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<Button
color='red'
disabled={stateDemografi.delete.loading}
onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconTrash size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Table>
</Paper>
{/* Chart */}
{!mounted && !chartData ? (
<Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}>
<Paper bg={colors['white-1']} p={'md'}>
<Title pb={10} order={3}>Data Kelahiran & Kematian</Title>
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text>
</Paper>
</Box>
) : (
<Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}>
<Paper bg={colors['white-1']} p={'md'}>
<Title pb={10} order={4}>Data Kelahiran & Kematian</Title>
{mounted && chartData.length > 0 && (
<BarChart width={isMobile ? 450 : isTablet ? 500 : 550} height={350} data={chartData} >
<XAxis dataKey="pekerjaan" />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="lakiLaki" fill="#f03e3e" name="Laki - Laki" />
<Bar dataKey="perempuan" fill="#ff922b" name="Perempuan" />
</BarChart>
)}
</Paper>
</Box>
)}
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleDelete}
text='Apakah anda yakin ingin menghapus demografi pekerjaan ini?'
/>
</Box>
);
}

View File

@@ -0,0 +1,80 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import jumlahPendudukMiskin from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-penduduk-miskin';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
function EditJumlahPendudukMiskin() {
const router = useRouter()
const params = useParams() as { id: string }
const stateJPM = useProxy(jumlahPendudukMiskin)
const id = params.id
// Load data saat komponen mount
useEffect(() => {
if (id) {
stateJPM.findUnique.load(id).then(() => {
const data = stateJPM.findUnique.data
if (data) {
stateJPM.update.form = {
year: data.year || 0,
totalPoorPopulation: data.totalPoorPopulation || 0,
}
}
})
}
}, [id])
const handleSubmit = async () => {
// Set the ID before submitting
stateJPM.update.id = id;
await stateJPM.update.submit();
router.push('/admin/ekonomi/jumlah-penduduk-miskin-2024-2025')
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack size={20} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: '100%', md: '50%' }} p={'md'}>
<Stack>
<Title order={3}>Edit Jumlah Penduduk Miskin</Title>
<TextInput
label="Tahun"
placeholder="masukkan tahun"
value={stateJPM.update.form.year}
onChange={(val) => {
stateJPM.update.form.year = Number(val.currentTarget.value);
}}
/>
<TextInput
label="Jumlah Penduduk Miskin"
type="number"
placeholder="masukkan jumlah penduduk miskin"
value={stateJPM.update.form.totalPoorPopulation}
onChange={(val) => {
stateJPM.update.form.totalPoorPopulation = Number(val.currentTarget.value);
}}
/>
<Button
mt={10}
bg={colors['blue-button']}
onClick={handleSubmit}
>
Simpan
</Button>
</Stack>
</Paper>
</Box>
)
}
export default EditJumlahPendudukMiskin;

View File

@@ -1,37 +1,81 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import grafikkepuasan from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/grafikKepuasan';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { Box, Button, Group, Paper, Stack, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import jumlahPendudukMiskin from '../../../_state/ekonomi/jumlah-penduduk-miskin';
function CreateJumlahPendudukMiskin() {
const router = useRouter();
const stateJPM = useProxy(jumlahPendudukMiskin);
const [chartData, setChartData] = useState<any[]>([]);
const router = useRouter()
const resetForm = () => {
stateJPM.create.form = {
year: 0,
totalPoorPopulation: 0,
}
}
const handleSubmit = async () => {
const id = await stateJPM.create.create();
if (id) {
const idStr = String(id);
await stateJPM.findUnique.load(idStr);
if (stateJPM.findUnique.data) {
setChartData([stateJPM.findUnique.data]);
}
}
resetForm();
router.push("/admin/ekonomi/jumlah-penduduk-miskin-2024-2025");
}
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25}/>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack size={20} />
</Button>
</Box>
<Paper w={{base: '100%', md: '50%'}} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Jumlah Penduduk Miskin</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Tahun</Text>}
placeholder='Masukkan tahun'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Jumlah Penduduk Miskin</Text>}
placeholder='Masukkan jumlah penduduk miskin'
/>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
<Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Title order={4}>Tambah Grafik Hasil Kepuasan Masyarakat</Title>
<Stack gap={"xs"}>
<TextInput
label="Tahun"
type="number"
value={stateJPM.create.form.year}
placeholder="Masukkan tahun"
onChange={(val) => {
stateJPM.create.form.year = Number(val.currentTarget.value);
}}
/>
<TextInput
label="Jumlah Penduduk Miskin"
type="number"
value={stateJPM.create.form.totalPoorPopulation}
placeholder="Masukkan jumlah penduduk miskin"
onChange={(val) => {
stateJPM.create.form.totalPoorPopulation = Number(val.currentTarget.value);
}}
/>
<Group>
<Button
bg={colors['blue-button']}
mt={10}
onClick={handleSubmit}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
</Box>
);
}

View File

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

View File

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

View File

@@ -1,54 +1,167 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useMediaQuery, useShallowEffect } from '@mantine/hooks';
import { useProxy } from 'valtio/utils';
import jumlahPendudukMiskin from '../../_state/ekonomi/jumlah-penduduk-miskin';
import { Bar, BarChart, Legend, XAxis, YAxis, Tooltip } from 'recharts';
import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus';
function JumlahPendudukMiskin() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Jumlah Penduduk Miskin'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListJumlahPendudukMiskin/>
<ListJumlahPendudukMiskin search={search} />
</Box>
);
}
function ListJumlahPendudukMiskin() {
function ListJumlahPendudukMiskin({ search }: { search: string }) {
type JPMGrafik = {
id: string;
year: number;
totalPoorPopulation: number;
}
const stateJPM = useProxy(jumlahPendudukMiskin);
const [chartData, setChartData] = useState<JPMGrafik[]>([]);
const [mounted, setMounted] = useState(false); // untuk memastikan DOM sudah ready
const isTablet = useMediaQuery('(max-width: 1024px)')
const isMobile = useMediaQuery('(max-width: 768px)')
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter();
const handleDelete = () => {
if (selectedId) {
stateJPM.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
stateJPM.findMany.load()
}
}
useShallowEffect(() => {
setMounted(true)
stateJPM.findMany.load()
}, [])
useEffect(() => {
setMounted(true);
if (stateJPM.findMany.data) {
setChartData(stateJPM.findMany.data.map((item) => ({
id: item.id,
year: Number(item.year),
totalPoorPopulation: Number(item.totalPoorPopulation),
})));
}
}, [stateJPM.findMany.data]);
const filteredData = (stateJPM.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.year.toString().toLowerCase().includes(keyword) ||
item.totalPoorPopulation.toString().toLowerCase().includes(keyword)
);
});
if (!stateJPM.findMany.data) {
return (
<Stack>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Jumlah Penduduk Miskin'
href='/admin/ekonomi/jumlah-penduduk-miskin-2024-2025/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Tahun</TableTh>
<TableTh>Jumlah Penduduk Miskin</TableTh>
<TableTh>Detail</TableTh>
<Stack gap={'xs'}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Jumlah Penduduk Miskin'
href='/admin/ekonomi/jumlah-penduduk-miskin-2024-2025/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Tahun</TableTh>
<TableTh>Jumlah Penduduk Miskin</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>2024</TableTd>
<TableTd>100</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/ekonomi/jumlah-penduduk-miskin-2024-2025/detail')}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
</TableTbody>
</Table>
</Paper>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.year}</TableTd>
<TableTd>{item.totalPoorPopulation}</TableTd>
<TableTd>
<Button color='green' onClick={() => router.push(`/admin/ekonomi/jumlah-penduduk-miskin-2024-2025/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<Button
color='red'
disabled={stateJPM.delete.loading}
onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconTrash size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>
{/* Chart */}
{!mounted && !chartData ? (
<Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}>
<Paper bg={colors['white-1']} p={'md'}>
<Title pb={10} order={3}>Grafik Jumlah Penduduk Miskin</Title>
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text>
</Paper>
</Box>
) : (
<Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}>
<Paper bg={colors['white-1']} p={'md'}>
<Title pb={10} order={4}>Grafik Jumlah Penduduk Miskin</Title>
{mounted && chartData.length > 0 && (
<BarChart width={isMobile ? 450 : isTablet ? 500 : 550} height={350} data={chartData} >
<XAxis dataKey="year" />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="totalPoorPopulation" fill={colors['blue-button']} name="Jumlah Penduduk Miskin" />
</BarChart>
)}
</Paper>
</Box>
)}
</Stack>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleDelete}
text='Apakah anda yakin ingin menghapus grafik jumlah penduduk miskin ini?'
/>
</Box>
);
}

View File

@@ -0,0 +1,62 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
function LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter()
const pathname = usePathname()
const tabs = [
{
label: "Pengangguran Berdasarkan Usia",
value: "pengangguranberdasarkanusia",
href: "/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia"
},
{
label: "Pengangguran Berdasarkan Pendidikan",
value: "pengangguranberdasarkanpendidikan",
href: "/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan"
},
];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
const handleTabChange = (value: string | null) => {
const tab = tabs.find(t => t.value === value)
if (tab) {
router.push(tab.href)
}
setActiveTab(value)
}
useEffect(() => {
const match = tabs.find(tab => tab.href === pathname)
if (match) {
setActiveTab(match.value)
}
}, [pathname])
return (
<Stack>
<Title order={3}>Jumlah Penduduk Usia Kerja yang Menganggur</Title>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
{tabs.map((e, i) => (
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
))}
</TabsList>
{tabs.map((e, i) => (
<TabsPanel key={i} value={e.value}>
{/* Konten dummy, bisa diganti tergantung routing */}
<></>
</TabsPanel>
))}
</Tabs>
{children}
</Stack>
);
}
export default LayoutTabs;

View File

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

View File

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

View File

@@ -0,0 +1,109 @@
'use client'
import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur';
/* eslint-disable react-hooks/exhaustive-deps */
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
function EditGrafikBerdasarkanPendidikan() {
const router = useRouter()
const params = useParams() as { id: string }
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanPendidikan)
const id = params.id
useEffect(() => {
if(id){
stategrafik.findUnique.load(id).then(() => {
const data = stategrafik.findUnique.data
if(data){
stategrafik.update.form = {
SD: data.SD || '',
SMP: data.SMP || '',
SMA: data.SMA || '',
D3: data.D3 || '',
S1: data.S1 || '',
}
}
})
}
}, [id])
const handleSubmit = async () => {
stategrafik.update.id = id;
await stategrafik.update.submit();
router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan')
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack size={20} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: '100%', md: '50%' }} p={'md'}>
<Stack>
<Title order={3}>Edit Grafik Pengangguran Berdasarkan Pendidikan</Title>
<TextInput
label="SD"
type='number'
placeholder="masukkan jumlah"
value={stategrafik.update.form.SD}
onChange={(val) => {
stategrafik.update.form.SD = val.currentTarget.value;
}}
/>
<TextInput
label="SMP"
type="number"
placeholder="masukkan jumlah"
value={stategrafik.update.form.SMP}
onChange={(val) => {
stategrafik.update.form.SMP = val.currentTarget.value;
}}
/>
<TextInput
label="SMA"
type="number"
placeholder="masukkan jumlah"
value={stategrafik.update.form.SMA}
onChange={(val) => {
stategrafik.update.form.SMA = val.currentTarget.value;
}}
/>
<TextInput
label="D3"
type="number"
placeholder="masukkan jumlah"
value={stategrafik.update.form.D3}
onChange={(val) => {
stategrafik.update.form.D3 = val.currentTarget.value;
}}
/>
<TextInput
label="S1"
type="number"
placeholder="masukkan jumlah"
value={stategrafik.update.form.S1}
onChange={(val) => {
stategrafik.update.form.S1 = val.currentTarget.value;
}}
/>
<Button
mt={10}
bg={colors['blue-button']}
onClick={handleSubmit}
>
Submit
</Button>
</Stack>
</Paper>
</Box>
);
}
export default EditGrafikBerdasarkanPendidikan;

View File

@@ -1,54 +1,111 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */
import React from 'react';
import { useRouter } from 'next/navigation';
import grafikBerdasarkanJenisKelamin from '@/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanJenisKelamin';
import { useProxy } from 'valtio/utils';
import { useState } from 'react';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Title, TextInput } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur';
function CreatePengangguranBerdasarkanPendidikan() {
function CreateGrafikBerdasarkanPendidikan() {
const router = useRouter();
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanPendidikan)
const [donutData, setDonutData] = useState<any[]>([]);
const resetForm = () => {
stategrafik.create.form = {
...stategrafik.create.form,
SD: "",
SMP: "",
SMA: "",
D3: "",
S1: "",
}
}
const handleSubmit = async () => {
const id = await stategrafik.create.create();
if (id) {
const idStr = String(id);
await stategrafik.findUnique.load(idStr);
if (stategrafik.findUnique.data) {
setDonutData([stategrafik.findUnique.data]);
}
}
resetForm();
router.push("/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan")
}
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25}/>
</Button>
</Box>
<Paper w={{base: '100%', md: '50%'}} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Pengangguran Berdasarkan Pendidikan</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Pendidikan SD</Text>}
placeholder='Masukkan pendidikan sd'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Pendidikan SMP</Text>}
placeholder='Masukkan pendidikan smp'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Pendidikan SMA</Text>}
placeholder='Masukkan pendidikan sma'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Pendidikan S1</Text>}
placeholder='Masukkan pendidikan s1'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Pendidikan S2</Text>}
placeholder='Masukkan pendidikan s2'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Pendidikan S3</Text>}
placeholder='Masukkan pendidikan s3'
/>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack size={20} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: '100%', md: '50%' }} p={'md'}>
<Stack>
<Title order={3}>Create Grafik Pengangguran Berdasarkan Pendidikan</Title>
<TextInput
label="SD"
type='number'
placeholder="masukkan jumlah"
value={stategrafik.create.form.SD}
onChange={(val) => {
stategrafik.create.form.SD = val.currentTarget.value;
}}
/>
<TextInput
label="SMP"
type="number"
placeholder="masukkan jumlah"
value={stategrafik.create.form.SMP}
onChange={(val) => {
stategrafik.create.form.SMP = val.currentTarget.value;
}}
/>
<TextInput
label="SMA"
type="number"
placeholder="masukkan jumlah"
value={stategrafik.create.form.SMA}
onChange={(val) => {
stategrafik.create.form.SMA = val.currentTarget.value;
}}
/>
<TextInput
label="D3"
type="number"
placeholder="masukkan jumlah"
value={stategrafik.create.form.D3}
onChange={(val) => {
stategrafik.create.form.D3 = val.currentTarget.value;
}}
/>
<TextInput
label="S1"
type="number"
placeholder="masukkan jumlah"
value={stategrafik.create.form.S1}
onChange={(val) => {
stategrafik.create.form.S1 = val.currentTarget.value;
}}
/>
<Button
mt={10}
bg={colors['blue-button']}
onClick={handleSubmit}
>
Submit
</Button>
</Stack>
</Paper>
</Box>
);
}
export default CreatePengangguranBerdasarkanPendidikan;
export default CreateGrafikBerdasarkanPendidikan;

View File

@@ -1,74 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Stack, Text } from '@mantine/core';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailPengangguranBerdasarkanPendidikan() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Pengangguran Berdasarkan Pendidikan</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"}>Pendidikan SD</Text>
<Text>50</Text>
</Box>
<Box>
<Text fw={"bold"}>Pendidikan SMP</Text>
<Text>60</Text>
</Box>
<Box>
<Text fw={"bold"}>Pendidikan SMA</Text>
<Text>80</Text>
</Box>
<Box>
<Text fw={"bold"}>Pendidikan S1</Text>
<Text>40</Text>
</Box>
<Box>
<Text fw={"bold"}>Pendidikan S2</Text>
<Text>20</Text>
</Box>
<Box>
<Text fw={"bold"}>Pendidikan S3</Text>
<Text>10</Text>
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red">
<IconX size={20} />
</Button>
<Button onClick={() => router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/edit')} color="green">
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Hapus
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus potensi ini?"
/> */}
</Box>
);
}
export default DetailPengangguranBerdasarkanPendidikan;

View File

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

View File

@@ -1,49 +1,215 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Title } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { Box, Button, Flex, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { Cell, Pie, PieChart } from 'recharts';
import { useProxy } from 'valtio/utils';
import JudulListTab from '../../../_com/jusulListTab';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import grafikNganggur from '../../../_state/ekonomi/usia-kerja-nganggur';
function PengangguranBerdasarkanPendidikan() {
const router = useRouter();
function GrafikBerdasarkanPendidikan() {
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<JudulListTab
title='Pengangguran Berdasarkan Pendidikan'
href='/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/create'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
/>
<Title order={4}>List Pengangguran Berdasarkan Pendidikan</Title>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Pendidikan SD</TableTh>
<TableTh>Pendidikan SMP</TableTh>
<TableTh>Pendidikan SMA</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>80</TableTd>
<TableTd>40</TableTd>
<TableTd>20</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/detail')}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
</TableTbody>
</Table>
</Stack>
</Paper>
<Box>
<Stack gap={"xs"}>
<Title order={3}>Grafik Pengangguran Berdasarkan Pendidikan</Title>
<ListGrafikBerdasarkanPendidikan />
</Stack>
</Box>
);
}
export default PengangguranBerdasarkanPendidikan;
function ListGrafikBerdasarkanPendidikan() {
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanPendidikan)
const [donutData, setDonutData] = useState<any[]>([]);
const [mounted, setMounted] = useState(false);
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter();
const [search, setSearch] = useState("");
const handleDelete = async () => {
if (selectedId) {
await stategrafik.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
stategrafik.findMany.load()
}
}
useShallowEffect(() => {
setMounted(true);
stategrafik.findMany.load()
}, []);
useEffect(() => {
if (stategrafik.findMany.data) {
const SD = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.SD || 0), 0);
const SMP = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.SMP || 0), 0);
const SMA = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.SMA || 0), 0);
const D3 = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.D3 || 0), 0);
const S1 = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.S1 || 0), 0);
setDonutData([
{ name: 'SD', value: SD, color: colors['blue-button'], key: 'SD' },
{ name: 'SMP', value: SMP, color: '#10A85AFF', key: 'SMP' },
{ name: 'SMA', value: SMA, color: '#C07B13FF', key: 'SMA' },
{ name: 'D3', value: D3, color: '#1094A8FF', key: 'D3' },
{ name: 'S1', value: S1, color: '#A83610FF', key: 'S1' },
]);
}
}, [stategrafik.findMany.data])
const filteredData = (stategrafik.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.SD.toString().toLowerCase().includes(keyword) ||
item.SMP.toString().toLowerCase().includes(keyword) ||
item.SMA.toString().toLowerCase().includes(keyword) ||
item.D3.toString().toLowerCase().includes(keyword) ||
item.S1.toString().toLowerCase().includes(keyword)
);
});
if (!stategrafik.findMany.data) {
return (
<Box>
<Skeleton h={500} />
</Box>
)
}
return (
<Box>
<Stack gap={"xs"}>
<Paper bg={colors['white-1']} p={"md"}>
<JudulListTab
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
title='List Grafik Pengangguran Berdasarkan Pendidikan'
href='/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/create'
placeholder='pencarian'
searchIcon={<IconSearch size={16} />}
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>SD</TableTh>
<TableTh>SMP</TableTh>
<TableTh>SMA</TableTh>
<TableTh>D3</TableTh>
<TableTh>S1</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length === 0 ? (
<TableTr>
<TableTd colSpan={6}>
<Text ta='center' c='dimmed'>Belum ada data grafik responden</Text>
</TableTd>
</TableTr>
) : (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.SD}</TableTd>
<TableTd>{item.SMP}</TableTd>
<TableTd>{item.SMA}</TableTd>
<TableTd>{item.D3}</TableTd>
<TableTd>{item.S1}</TableTd>
<TableTd>
<Button color='green' onClick={() => router.push(`/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<Button
color='red'
disabled={stategrafik.delete.loading}
onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconTrash size={20} />
</Button>
</TableTd>
</TableTr>
))
)}
</TableTbody>
</Table>
</Paper>
{/* Chart */}
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<Title pb={10} order={3}>Grafik Pengangguran Berdasarkan Pendidikan</Title>
{mounted && donutData.length > 0 ? (<Box style={{ width: '100%', height: 'auto', minHeight: 200 }}>
<PieChart
width={800} height={300}
data={donutData}
>
<Pie
dataKey="value"
nameKey="name"
data={donutData}
cx={500}
cy={150}
innerRadius={60}
outerRadius={115}
label={true}
>
{donutData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
</PieChart>
<Flex gap={"md"} align={"center"}>
<Box bg={colors['blue-button']} w={20} h={20} />
<Text>SD : {donutData.find((entry) => entry.name === 'SD')?.value}</Text>
</Flex>
<Flex gap={"md"} align={"center"}>
<Box bg={'#10A85AFF'} w={20} h={20} />
<Text>SMP : {donutData.find((entry) => entry.name === 'SMP')?.value}</Text>
</Flex>
<Flex gap={"md"} align={"center"}>
<Box bg={'#C07B13FF'} w={20} h={20} />
<Text>SMA : {donutData.find((entry) => entry.name === 'SMA')?.value}</Text>
</Flex>
<Flex gap={"md"} align={"center"}>
<Box bg={'#1094A8FF'} w={20} h={20} />
<Text>D3 : {donutData.find((entry) => entry.name === 'D3')?.value}</Text>
</Flex>
<Flex gap={"md"} align={"center"}>
<Box bg={'#A83610FF'} w={20} h={20} />
<Text>S1 : {donutData.find((entry) => entry.name === 'S1')?.value}</Text>
</Flex>
</Box>
) : (
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text>
)}
</Stack>
</Paper>
</Box>
</Stack>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleDelete}
text='Apakah anda yakin ingin menghapus grafik pengangguran berdasarkan pendidikan ini?'
/>
</Box>
);
}
export default GrafikBerdasarkanPendidikan;

View File

@@ -0,0 +1,99 @@
'use client'
import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur';
/* eslint-disable react-hooks/exhaustive-deps */
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() {
const router = useRouter()
const params = useParams() as { id: string }
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanUsiaKerjaNganggur)
const id = params.id
useEffect(() => {
if(id){
stategrafik.findUnique.load(id).then(() => {
const data = stategrafik.findUnique.data
if(data){
stategrafik.update.form = {
usia18_25: data.usia18_25 || '',
usia26_35: data.usia26_35 || '',
usia36_45: data.usia36_45 || '',
usia46_keatas: data.usia46_keatas || '',
}
}
})
}
}, [id])
const handleSubmit = async () => {
stategrafik.update.id = id;
await stategrafik.update.submit();
router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia')
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack size={20} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: '100%', md: '50%' }} p={'md'}>
<Stack>
<Title order={3}>Edit Grafik Pengangguran Berdasarkan Usia Kerja</Title>
<TextInput
label="Usia 18 - 25"
type='number'
placeholder="masukkan jumlah"
value={stategrafik.update.form.usia18_25}
onChange={(val) => {
stategrafik.update.form.usia18_25 = val.currentTarget.value;
}}
/>
<TextInput
label="Usia 26 - 35"
type="number"
placeholder="masukkan jumlah"
value={stategrafik.update.form.usia26_35}
onChange={(val) => {
stategrafik.update.form.usia26_35 = val.currentTarget.value;
}}
/>
<TextInput
label="Usia 36 - 45"
type="number"
placeholder="masukkan jumlah"
value={stategrafik.update.form.usia36_45}
onChange={(val) => {
stategrafik.update.form.usia36_45 = val.currentTarget.value;
}}
/>
<TextInput
label="Usia 46 +"
type="number"
placeholder="masukkan jumlah"
value={stategrafik.update.form.usia46_keatas}
onChange={(val) => {
stategrafik.update.form.usia46_keatas = val.currentTarget.value;
}}
/>
<Button
mt={10}
bg={colors['blue-button']}
onClick={handleSubmit}
>
Submit
</Button>
</Stack>
</Paper>
</Box>
);
}
export default EditGrafikBerdasarkanUsiaKerjaYangMenganggur;

View File

@@ -1,45 +1,101 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */
import React from 'react';
import { useRouter } from 'next/navigation';
import grafikBerdasarkanJenisKelamin from '@/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanJenisKelamin';
import { useProxy } from 'valtio/utils';
import { useState } from 'react';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Title, TextInput } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur';
function CreatePengangguranBerdasarkanUsia() {
function CreateGrafikBerdasarkanUsiaKerjaYangMenganggur() {
const router = useRouter();
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanUsiaKerjaNganggur)
const [donutData, setDonutData] = useState<any[]>([]);
const resetForm = () => {
stategrafik.create.form = {
...stategrafik.create.form,
usia18_25: "",
usia26_35: "",
usia36_45: "",
usia46_keatas: "",
}
}
const handleSubmit = async () => {
const id = await stategrafik.create.create();
if (id) {
const idStr = String(id);
await stategrafik.findUnique.load(idStr);
if (stategrafik.findUnique.data) {
setDonutData([stategrafik.findUnique.data]);
}
}
resetForm();
router.push("/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia")
}
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25}/>
</Button>
</Box>
<Paper w={{base: '100%', md: '50%'}} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Pengangguran Berdasarkan Usia</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Usia 18 - 25</Text>}
placeholder='Masukkan usia'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Usia 26 - 35</Text>}
placeholder='Masukkan usia'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Usia 36 - 45</Text>}
placeholder='Masukkan usia'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Usia 46 +</Text>}
placeholder='Masukkan usia'
/>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack size={20} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: '100%', md: '50%' }} p={'md'}>
<Stack>
<Title order={3}>Create Grafik Pengangguran Berdasarkan Usia Kerja</Title>
<TextInput
label="Usia 18 - 25"
type='number'
placeholder="masukkan jumlah"
value={stategrafik.create.form.usia18_25}
onChange={(val) => {
stategrafik.create.form.usia18_25 = val.currentTarget.value;
}}
/>
<TextInput
label="Usia 26 - 35"
type="number"
placeholder="masukkan jumlah"
value={stategrafik.create.form.usia26_35}
onChange={(val) => {
stategrafik.create.form.usia26_35 = val.currentTarget.value;
}}
/>
<TextInput
label="Usia 36 - 45"
type="number"
placeholder="masukkan jumlah"
value={stategrafik.create.form.usia36_45}
onChange={(val) => {
stategrafik.create.form.usia36_45 = val.currentTarget.value;
}}
/>
<TextInput
label="Usia 46 +"
type="number"
placeholder="masukkan jumlah"
value={stategrafik.create.form.usia46_keatas}
onChange={(val) => {
stategrafik.create.form.usia46_keatas = val.currentTarget.value;
}}
/>
<Button
mt={10}
bg={colors['blue-button']}
onClick={handleSubmit}
>
Submit
</Button>
</Stack>
</Paper>
</Box>
);
}
export default CreatePengangguranBerdasarkanUsia;
export default CreateGrafikBerdasarkanUsiaKerjaYangMenganggur;

View File

@@ -1,70 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Stack, Text } from '@mantine/core';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailPengangguranBerdasarkanUsia() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Pengangguran Berdasarkan Usia</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"}>Usia 18 - 25</Text>
<Text>80</Text>
</Box>
<Box>
<Text fw={"bold"}>Usia 26 - 35</Text>
<Text>40</Text>
</Box>
<Box>
<Text fw={"bold"}>Usia 36 - 45</Text>
<Text>20</Text>
</Box>
<Box>
<Text fw={"bold"}>Usia 46 +</Text>
<Text>10</Text>
</Box>
<Box>
<Text fw={"bold"}>Total</Text>
<Text>150</Text>
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red">
<IconX size={20} />
</Button>
<Button onClick={() => router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/edit')} color="green">
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Hapus
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus potensi ini?"
/> */}
</Box>
);
}
export default DetailPengangguranBerdasarkanUsia;

View File

@@ -1,47 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
function EditPengangguranBerdasarkanUsia() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25}/>
</Button>
</Box>
<Paper w={{base: '100%', md: '50%'}} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Pengangguran Berdasarkan Usia</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Usia 18 - 25</Text>}
placeholder='Masukkan usia'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Usia 26 - 35</Text>}
placeholder='Masukkan usia'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Usia 36 - 45</Text>}
placeholder='Masukkan usia'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Usia 46 +</Text>}
placeholder='Masukkan usia'
/>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditPengangguranBerdasarkanUsia;

View File

@@ -1,49 +1,206 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Title } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { Box, Button, Flex, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { Cell, Pie, PieChart } from 'recharts';
import { useProxy } from 'valtio/utils';
import JudulListTab from '../../../_com/jusulListTab';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import grafikNganggur from '../../../_state/ekonomi/usia-kerja-nganggur';
function PengangguranBerdasarkanUsia() {
const router = useRouter();
function GrafikBerdasarkanUsiaKerjaYangMenganggur() {
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<JudulListTab
title='Pengangguran Berdasarkan Usia'
href='/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
/>
<Title order={4}>List Pengangguran Berdasarkan Usia</Title>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Usia 18 - 25</TableTh>
<TableTh>Usia 26 - 35</TableTh>
<TableTh>Usia 36 - 45</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>80</TableTd>
<TableTd>40</TableTd>
<TableTd>20</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/detail')}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
</TableTbody>
</Table>
</Stack>
</Paper>
<Box>
<Stack gap={"xs"}>
<Title order={3}>Grafik Pengangguran Berdasarkan Usia Kerja</Title>
<ListGrafikBerdasarkanUsiaKerjaYangMenganggur />
</Stack>
</Box>
);
}
export default PengangguranBerdasarkanUsia;
function ListGrafikBerdasarkanUsiaKerjaYangMenganggur() {
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanUsiaKerjaNganggur)
const [donutData, setDonutData] = useState<any[]>([]);
const [mounted, setMounted] = useState(false);
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter();
const [search, setSearch] = useState("");
const handleDelete = async () => {
if (selectedId) {
await stategrafik.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
stategrafik.findMany.load()
}
}
useShallowEffect(() => {
setMounted(true);
stategrafik.findMany.load()
}, []);
useEffect(() => {
if (stategrafik.findMany.data) {
const totalUsia18_25 = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.usia18_25 || 0), 0);
const totalUsia26_35 = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.usia26_35 || 0), 0);
const totalUsia36_45 = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.usia36_45 || 0), 0);
const totalUsia46_keatas = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.usia46_keatas || 0), 0);
setDonutData([
{ name: 'usia18_25', value: totalUsia18_25, color: colors['blue-button'], key: 'usia18_25' },
{ name: 'usia26_35', value: totalUsia26_35, color: '#10A85AFF', key: 'usia26_35' },
{ name: 'usia36_45', value: totalUsia36_45, color: '#C07B13FF', key: 'usia36_45' },
{ name: 'usia46_keatas', value: totalUsia46_keatas, color: '#1094A8FF', key: 'usia46_keatas' },
]);
}
}, [stategrafik.findMany.data])
const filteredData = (stategrafik.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.usia18_25.toString().toLowerCase().includes(keyword) ||
item.usia26_35.toString().toLowerCase().includes(keyword) ||
item.usia36_45.toString().toLowerCase().includes(keyword) ||
item.usia46_keatas.toString().toLowerCase().includes(keyword)
);
});
if (!stategrafik.findMany.data) {
return (
<Box>
<Skeleton h={500} />
</Box>
)
}
return (
<Box>
<Stack gap={"xs"}>
<Paper bg={colors['white-1']} p={"md"}>
<JudulListTab
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
title='List Pengangguran Berdasarkan Usia Kerja'
href='/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create'
placeholder='pencarian'
searchIcon={<IconSearch size={16} />}
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Usia 18-25</TableTh>
<TableTh>Usia 26-35</TableTh>
<TableTh>Usia 36-45</TableTh>
<TableTh>Usia 46 +</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length === 0 ? (
<TableTr>
<TableTd colSpan={6}>
<Text ta='center' c='dimmed'>Belum ada data grafik responden</Text>
</TableTd>
</TableTr>
) : (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.usia18_25}</TableTd>
<TableTd>{item.usia26_35}</TableTd>
<TableTd>{item.usia36_45}</TableTd>
<TableTd>{item.usia46_keatas}</TableTd>
<TableTd>
<Button color='green' onClick={() => router.push(`/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<Button
color='red'
disabled={stategrafik.delete.loading}
onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconTrash size={20} />
</Button>
</TableTd>
</TableTr>
))
)}
</TableTbody>
</Table>
</Paper>
{/* Chart */}
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<Title pb={10} order={3}>Grafik Pengangguran Berdasarkan Usia Kerja</Title>
{mounted && donutData.length > 0 ? (<Box style={{ width: '100%', height: 'auto', minHeight: 200 }}>
<PieChart
width={800} height={300}
data={donutData}
>
<Pie
dataKey="value"
nameKey="name"
data={donutData}
cx={500}
cy={150}
innerRadius={60}
outerRadius={115}
label={true}
>
{donutData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
</PieChart>
<Flex gap={"md"} align={"center"}>
<Box bg={colors['blue-button']} w={20} h={20} />
<Text>Usia 18-25 : {donutData.find((entry) => entry.name === 'usia18_25')?.value}</Text>
</Flex>
<Flex gap={"md"} align={"center"}>
<Box bg={'#10A85AFF'} w={20} h={20} />
<Text>Usia 26-35 : {donutData.find((entry) => entry.name === 'usia26_35')?.value}</Text>
</Flex>
<Flex gap={"md"} align={"center"}>
<Box bg={'#C07B13FF'} w={20} h={20} />
<Text>Usia 36-45 : {donutData.find((entry) => entry.name === 'usia36_45')?.value}</Text>
</Flex>
<Flex gap={"md"} align={"center"}>
<Box bg={'#1094A8FF'} w={20} h={20} />
<Text>Usia 46 + : {donutData.find((entry) => entry.name === 'usia46_keatas')?.value}</Text>
</Flex>
</Box>
) : (
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text>
)}
</Stack>
</Paper>
</Box>
</Stack>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleDelete}
text='Apakah anda yakin ingin menghapus grafik pengangguran berdasarkan usia kerja ini?'
/>
</Box>
);
}
export default GrafikBerdasarkanUsiaKerjaYangMenganggur;

View File

@@ -0,0 +1,155 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import lowonganKerjaState from '@/app/admin/(dashboard)/_state/ekonomi/lowongan-kerja';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditLowonganKerja() {
const lowonganState = useProxy(lowonganKerjaState)
const router = useRouter();
const params = useParams();
const [formData, setFormData] = useState({
posisi: lowonganKerjaState.update.form.posisi,
namaPerusahaan: lowonganKerjaState.update.form.namaPerusahaan,
lokasi: lowonganKerjaState.update.form.lokasi,
tipePekerjaan: lowonganKerjaState.update.form.tipePekerjaan,
gaji: lowonganKerjaState.update.form.gaji,
deskripsi: lowonganKerjaState.update.form.deskripsi,
kualifikasi: lowonganKerjaState.update.form.kualifikasi,
})
useEffect(() => {
const loadLowongan = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await lowonganState.update.load(id); // akses langsung, bukan dari proxy
if (data) {
setFormData({
posisi: data.posisi || '',
namaPerusahaan: data.namaPerusahaan || '',
lokasi: data.lokasi || '',
tipePekerjaan: data.tipePekerjaan || '',
gaji: data.gaji || '',
deskripsi: data.deskripsi || '',
kualifikasi: data.kualifikasi || '',
});
}
} catch (error) {
console.error("Error loading lowongan kerja:", error);
toast.error("Gagal memuat data lowongan kerja");
}
};
loadLowongan();
}, [params?.id])
const handleSubmit = async () => {
try {
lowonganKerjaState.update.form = {
...lowonganKerjaState.update.form,
posisi: formData.posisi,
namaPerusahaan: formData.namaPerusahaan,
lokasi: formData.lokasi,
tipePekerjaan: formData.tipePekerjaan,
gaji: formData.gaji,
deskripsi: formData.deskripsi,
kualifikasi: formData.kualifikasi,
}
await lowonganState.update.update()
toast.success("Lowongan kerja berhasil diperbarui!");
router.push("/admin/ekonomi/lowongan-kerja-lokal");
} catch (error) {
console.error("Error updating lowongan kerja:", error);
toast.error("Terjadi kesalahan saat memperbarui lowongan kerja");
}
}
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Lowongan Kerja Lokal</Title>
<TextInput
value={formData.posisi}
onChange={(val) => {
formData.posisi = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Posisi</Text>}
placeholder='Masukkan posisi'
/>
<TextInput
value={formData.namaPerusahaan}
onChange={(val) => {
formData.namaPerusahaan = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Perusahaan</Text>}
placeholder='Masukkan nama perusahaan'
/>
<TextInput
value={formData.lokasi}
onChange={(val) => {
formData.lokasi = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Lokasi</Text>}
placeholder='Masukkan lokasi'
/>
<TextInput
value={formData.tipePekerjaan}
onChange={(val) => {
formData.tipePekerjaan = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Tipe Pekerjaan</Text>}
placeholder='Masukkan tipe pekerjaan'
/>
<TextInput
value={formData.gaji}
onChange={(val) => {
formData.gaji = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Gaji selama 1 bulan</Text>}
placeholder='Masukkan gaji'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Lowongan Kerja</Text>
<EditEditor
value={formData.deskripsi}
onChange={(val) => {
formData.deskripsi = val;
}}
/>
</Box>
<Box>
<Text fw={"bold"} fz={"sm"}>Kualifikasi Lowongan Kerja</Text>
<EditEditor
value={formData.kualifikasi}
onChange={(val) => {
formData.kualifikasi = val;
}}
/>
</Box>
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditLowonganKerja;

View File

@@ -0,0 +1,128 @@
'use client'
import { useProxy } from 'valtio/utils';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import colors from '@/con/colors';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import lowonganKerjaState from '../../../_state/ekonomi/lowongan-kerja';
function DetailLowonganKerjaLokal() {
const lowonganState = useProxy(lowonganKerjaState)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter()
useShallowEffect(() => {
lowonganState.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
lowonganState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/ekonomi/lowongan-kerja-lokal")
}
}
if (!lowonganState.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={40} />
</Stack>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Lowongan Kerja Lokal</Text>
{lowonganState.findUnique.data ? (
<Paper key={lowonganState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Bekerja Sebagai</Text>
<Text fz={"lg"}>{lowonganState.findUnique.data?.posisi}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Nama Usaha</Text>
<Text fz={"lg"}>{lowonganState.findUnique.data?.namaPerusahaan}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Lokasi</Text>
<Text fz={"lg"}>{lowonganState.findUnique.data?.lokasi}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Tipe Pekerjaan</Text>
<Text fz={"lg"}>{lowonganState.findUnique.data?.tipePekerjaan}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Gaji</Text>
<Text fz={"lg"}>{lowonganState.findUnique.data?.gaji}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: lowonganState.findUnique.data?.deskripsi }} />
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Kualifikasi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: lowonganState.findUnique.data?.kualifikasi }} />
</Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (lowonganState.findUnique.data) {
setSelectedId(lowonganState.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={lowonganState.delete.loading || !lowonganState.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (lowonganState.findUnique.data) {
router.push(`/admin/ekonomi/lowongan-kerja-lokal/${lowonganState.findUnique.data.id}/edit`);
}
}}
disabled={!lowonganState.findUnique.data}
color={"green"}
>
<IconEdit size={20} />
</Button>
</Flex>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus lowongan kerja ini?'
/>
</Box>
);
}
export default DetailLowonganKerjaLokal;

View File

@@ -3,11 +3,33 @@ import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { KeamananEditor } from '../../../keamanan/_com/keamananEditor';
import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor';
import lowonganKerjaState from '../../../_state/ekonomi/lowongan-kerja';
function CreateLowonganKerja() {
const lowonganState = useProxy(lowonganKerjaState)
const router = useRouter();
const resetForm = () => {
lowonganState.create.form = {
posisi: "",
namaPerusahaan: "",
lokasi: "",
tipePekerjaan: "",
gaji: "",
deskripsi: "",
kualifikasi: "",
}
}
const handleSubmit = async () => {
await lowonganState.create.create()
resetForm()
router.push("/admin/ekonomi/lowongan-kerja-lokal")
}
return (
<Box>
<Box mb={10}>
@@ -20,33 +42,65 @@ function CreateLowonganKerja() {
<Stack gap={"xs"}>
<Title order={4}>Create Lowongan Kerja Lokal</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Pekerjaan</Text>}
placeholder='Masukkan pekerjaan'
value={lowonganState.create.form.posisi}
onChange={(val) => {
lowonganState.create.form.posisi = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Posisi</Text>}
placeholder='Masukkan posisi'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Usaha</Text>}
placeholder='Masukkan nama usaha'
value={lowonganState.create.form.namaPerusahaan}
onChange={(val) => {
lowonganState.create.form.namaPerusahaan = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Perusahaan</Text>}
placeholder='Masukkan nama perusahaan'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Alamat Usaha</Text>}
placeholder='Masukkan alamat usaha'
value={lowonganState.create.form.lokasi}
onChange={(val) => {
lowonganState.create.form.lokasi = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Lokasi</Text>}
placeholder='Masukkan lokasi'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nomor Telepon</Text>}
placeholder='Masukkan nomor telepon'
value={lowonganState.create.form.tipePekerjaan}
onChange={(val) => {
lowonganState.create.form.tipePekerjaan = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Tipe Pekerjaan</Text>}
placeholder='Masukkan tipe pekerjaan'
/>
<TextInput
value={lowonganState.create.form.gaji}
onChange={(val) => {
lowonganState.create.form.gaji = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Gaji selama 1 bulan</Text>}
placeholder='Masukkan gaji'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Lowongan Kerja</Text>
<KeamananEditor
showSubmit={false}
<CreateEditor
value={lowonganState.create.form.deskripsi}
onChange={(val) => {
lowonganState.create.form.deskripsi = val;
}}
/>
</Box>
<Box>
<Text fw={"bold"} fz={"sm"}>Kualifikasi Lowongan Kerja</Text>
<CreateEditor
value={lowonganState.create.form.kualifikasi}
onChange={(val) => {
lowonganState.create.form.kualifikasi = val;
}}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>

View File

@@ -1,78 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Stack, Text } from '@mantine/core';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailLowonganKerjaLokal() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Lowongan Kerja Lokal</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"}>Bekerja Sebagai</Text>
<Text>Karyawan</Text>
</Box>
<Box>
<Text fw={"bold"}>Nama Usaha</Text>
<Text>BIBD</Text>
</Box>
<Box>
<Text fw={"bold"}>Alamat Usaha</Text>
<Text>Jalan In Aja</Text>
</Box>
<Box>
<Text fw={"bold"}>Nomor Telepon</Text>
<Text>0896232831883</Text>
</Box>
<Box>
<Text fw={"bold"}>Waktu Kerja</Text>
<Text>Full Time</Text>
</Box>
<Box>
<Text fw={"bold"}>Gaji selama 1 bulan</Text>
<Text>Rp. 3.000.000</Text>
</Box>
<Box>
<Text fw={"bold"}>Deskripsi Lowongan Kerja</Text>
<Text> Pekerjaan dengan gaji Rp. 3.000.000</Text>
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red">
<IconX size={20} />
</Button>
<Button onClick={() => router.push('/admin/ekonomi/lowongan-kerja-lokal/edit')} color="green">
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Hapus
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus potensi ini?"
/> */}
</Box>
);
}
export default DetailLowonganKerjaLokal;

View File

@@ -1,57 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { KeamananEditor } from '../../../keamanan/_com/keamananEditor';
function EditLowonganKerja() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25}/>
</Button>
</Box>
<Paper w={{base: '100%', md: '50%'}} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Lowongan Kerja Lokal</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Pekerjaan</Text>}
placeholder='Masukkan pekerjaan'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Usaha</Text>}
placeholder='Masukkan nama usaha'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Alamat Usaha</Text>}
placeholder='Masukkan alamat usaha'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nomor Telepon</Text>}
placeholder='Masukkan nomor telepon'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Gaji selama 1 bulan</Text>}
placeholder='Masukkan gaji'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Lowongan Kerja</Text>
<KeamananEditor
showSubmit={false}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditLowonganKerja;

View File

@@ -1,26 +1,55 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import { useRouter } from 'next/navigation';
import lowonganKerjaState from '../../_state/ekonomi/lowongan-kerja';
import { useProxy } from 'valtio/utils';
import { useShallowEffect } from '@mantine/hooks';
import { useState } from 'react';
function LowonganKerjaLokal() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Lowongan Kerja Lokal'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListLowonganKerjaLokal/>
<ListLowonganKerjaLokal search={search} />
</Box>
);
}
function ListLowonganKerjaLokal() {
function ListLowonganKerjaLokal({ search }: { search: string }) {
const lowonganState = useProxy(lowonganKerjaState)
const router = useRouter();
useShallowEffect(() => {
lowonganState.findMany.load();
}, [])
const filteredData = (lowonganState.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.posisi.toLowerCase().includes(keyword) ||
item.namaPerusahaan.toLowerCase().includes(keyword) ||
item.lokasi.toLowerCase().includes(keyword)
);
});
if (!lowonganState.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
@@ -38,16 +67,18 @@ function ListLowonganKerjaLokal() {
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>Karyawan</TableTd>
<TableTd>BIBD</TableTd>
<TableTd>Jalan In Aja</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/ekonomi/lowongan-kerja-lokal/detail')}>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.posisi}</TableTd>
<TableTd>{item.namaPerusahaan}</TableTd>
<TableTd>{item.lokasi}</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/ekonomi/lowongan-kerja-lokal/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>

View File

@@ -0,0 +1,62 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
function LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter()
const pathname = usePathname()
const tabs = [
{
label: "Produk Pasar Desa",
value: "produkpasardesa",
href: "/admin/ekonomi/pasar-desa/produk-pasar-desa"
},
{
label: "Kategori Produk",
value: "kategoriproduk",
href: "/admin/ekonomi/pasar-desa/kategori-produk"
},
];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
const handleTabChange = (value: string | null) => {
const tab = tabs.find(t => t.value === value)
if (tab) {
router.push(tab.href)
}
setActiveTab(value)
}
useEffect(() => {
const match = tabs.find(tab => tab.href === pathname)
if (match) {
setActiveTab(match.value)
}
}, [pathname])
return (
<Stack>
<Title order={3}>Pasar Desa</Title>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
{tabs.map((e, i) => (
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
))}
</TabsList>
{tabs.map((e, i) => (
<TabsPanel key={i} value={e.value}>
{/* Konten dummy, bisa diganti tergantung routing */}
<></>
</TabsPanel>
))}
</Tabs>
{children}
</Stack>
);
}
export default LayoutTabs;

View File

@@ -1,56 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { KeamananEditor } from '../../../keamanan/_com/keamananEditor';
function CreatePasarDesa() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25}/>
</Button>
</Box>
<Paper w={{base: '100%', md: '50%'}} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Pasar Desa</Title>
<Box>
<Text fw={"bold"} fz={"sm"}>Masukkan Image</Text>
<IconImageInPicture size={50} />
</Box>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Produk</Text>}
placeholder='Masukkan nama produk'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Harga Produk</Text>}
placeholder='Masukkan harga produk'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Rating Produk</Text>}
placeholder='Masukkan rating produk'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Alamat Usaha</Text>}
placeholder='Masukkan alamat usaha'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Produk</Text>
<KeamananEditor
showSubmit={false}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreatePasarDesa;

View File

@@ -1,74 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Flex, Text, Image } from '@mantine/core';
import { IconArrowBack, IconX, IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import React from 'react';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailPasarDesa() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Pasar Desa</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Produk</Text>
<Text fz={"lg"}>Test Judul</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Harga Produk</Text>
<Text fz={"lg"}>Rp. 20.000</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Rating Produk</Text>
<Text fz={"lg"}>5</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Alamat Usaha</Text>
<Text fz={"lg"}>Jalan In Aja</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
<Image src={"/"} alt="gambar" />
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text>
<Text fz={"lg"} >Test Konten</Text>
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red">
<IconX size={20} />
</Button>
<Button onClick={() => router.push('/admin/ekonomi/pasar-desa/edit')} color="green">
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Hapus
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus potensi ini?"
/> */}
</Box>
);
}
export default DetailPasarDesa;

View File

@@ -1,56 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { KeamananEditor } from '../../../keamanan/_com/keamananEditor';
function EditPasarDesa() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25}/>
</Button>
</Box>
<Paper w={{base: '100%', md: '50%'}} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Pasar Desa</Title>
<Box>
<Text fw={"bold"} fz={"sm"}>Masukkan Image</Text>
<IconImageInPicture size={50} />
</Box>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Produk</Text>}
placeholder='Masukkan nama produk'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Harga Produk</Text>}
placeholder='Masukkan harga produk'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Rating Produk</Text>}
placeholder='Masukkan rating produk'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Alamat Usaha</Text>}
placeholder='Masukkan alamat usaha'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Produk</Text>
<KeamananEditor
showSubmit={false}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditPasarDesa;

View File

@@ -0,0 +1,98 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import React, { useEffect, useState } from 'react';
import { useParams, useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import pasarDesaState from '../../../../_state/ekonomi/pasar-desa/pasar-desa';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Title, TextInput, Group, Text } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { toast } from 'react-toastify';
function EditKategoriProduk() {
const router = useRouter();
const params = useParams();
const id = params?.id as string;
const statePasar = useProxy(pasarDesaState.kategoriProduk);
const [formData, setFormData] = useState({
nama: "",
});
useEffect(() => {
const loadKategoriProduk = async () => {
if (!id) return;
try {
const data = await statePasar.edit.load(id);
if (data) {
// pastikan id-nya masuk ke state edit
statePasar.edit.id = id;
setFormData({
nama: data.nama || '',
});
}
} catch (error) {
console.error("Error loading kategori produk:", error);
toast.error("Gagal memuat data kategori produk");
}
};
loadKategoriProduk();
}, [id]);
const handleSubmit = async () => {
try {
if (!formData.nama.trim()) {
toast.error('Nama kategori produk tidak boleh kosong');
return;
}
statePasar.edit.form = {
nama: formData.nama.trim(),
};
// Safety check tambahan: pastikan ID tidak kosong
if (!statePasar.edit.id) {
statePasar.edit.id = id; // fallback
}
const success = await statePasar.edit.update();
if (success) {
router.push("/admin/ekonomi/pasar-desa/kategori-produk");
}
} catch (error) {
console.error("Error updating kategori produk:", error);
// toast akan ditampilkan dari fungsi update
}
};
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Kategori Produk</Title>
<TextInput
value={formData.nama}
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Produk</Text>}
placeholder='Masukkan nama kategori produk'
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditKategoriProduk;

View File

@@ -0,0 +1,61 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import React, { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import pasarDesaState from '../../../../_state/ekonomi/pasar-desa/pasar-desa';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Title, TextInput, Group, Text } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
function CreateKategoriProduk() {
const router = useRouter();
const statePasar = useProxy(pasarDesaState.kategoriProduk)
useEffect(() => {
statePasar.findMany.load();
}, []);
const resetForm = () => {
statePasar.create.form = {
nama: "",
};
}
const handleSubmit = async () => {
await statePasar.create.create();
resetForm();
router.push("/admin/ekonomi/pasar-desa/kategori-produk")
}
return (
<Box>
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Kategori Produk</Title>
<TextInput
value={statePasar.create.form.nama}
onChange={(val) => {
statePasar.create.form.nama = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Produk</Text>}
placeholder='Masukkan nama kategori produk'
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
</Box>
);
}
export default CreateKategoriProduk;

View File

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

View File

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

View File

@@ -1,60 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import { useRouter } from 'next/navigation';
function PasarDesa() {
return (
<Box>
<HeaderSearch
title='Pasar Desa'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
/>
<ListPasarDesa/>
</Box>
);
}
function ListPasarDesa() {
const router = useRouter();
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Pasar Desa'
href='/admin/ekonomi/pasar-desa/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Produk</TableTh>
<TableTh>Harga Produk</TableTh>
<TableTh>Rating Produk</TableTh>
<TableTh>Alamat Usaha</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>Produk 1</TableTd>
<TableTd>Harga Rp. 20.000</TableTd>
<TableTd>Rating 5</TableTd>
<TableTd>Jalan In Aja</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/ekonomi/pasar-desa/detail')}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
</TableTbody>
</Table>
</Paper>
</Box>
);
}
export default PasarDesa;

View File

@@ -0,0 +1,226 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import pasarDesaState from '@/app/admin/(dashboard)/_state/ekonomi/pasar-desa/pasar-desa';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, MultiSelect, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditPasarDesa() {
const pasarState = useProxy(pasarDesaState)
const router = useRouter();
const params = useParams();
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({
nama: pasarState.pasarDesa.edit.form.nama || "",
harga: pasarState.pasarDesa.edit.form.harga || 0,
alamatUsaha: pasarState.pasarDesa.edit.form.alamatUsaha || "",
imageId: pasarState.pasarDesa.edit.form.imageId || "",
rating: pasarState.pasarDesa.edit.form.rating || 0,
kategoriId: pasarState.pasarDesa.edit.form.kategoriId || [],
})
useEffect(() => {
pasarState.kategoriProduk.findMany.load();
const loadPasarDesa = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await pasarState.pasarDesa.edit.load(id);
if (data) {
setFormData({
nama: data.nama || "",
harga: data.harga || 0,
alamatUsaha: data.alamatUsaha || "",
imageId: data.imageId || "",
rating: data.rating || 0,
// Use the IDs from KategoriToPasar relationship
kategoriId: data.KategoriToPasar?.map((k: any) => k.kategoriId) || [],
});
// Tampilkan preview gambar
if (data.image?.link) {
setPreviewImage(data.image.link);
}
}
} catch (error) {
console.error("Error loading pasar desa:", error);
toast.error(
error instanceof Error ? error.message : "Gagal mengambil data pasar desa"
);
}
}
loadPasarDesa();
}, [params?.id]);
const handleSubmit = async () => {
try {
pasarState.pasarDesa.edit.form = {
...pasarState.pasarDesa.edit.form,
nama: formData.nama,
harga: formData.harga,
alamatUsaha: formData.alamatUsaha,
imageId: formData.imageId,
rating: formData.rating,
kategoriId: formData.kategoriId,
}
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
// Update imageId in global state
pasarState.pasarDesa.edit.form.imageId = uploaded.id;
}
await pasarState.pasarDesa.edit.update();
toast.success("Tips Keamanan berhasil diperbarui!");
router.push("/admin/ekonomi/pasar-desa/produk-pasar-desa");
} catch (error) {
console.error("Error updating pasar desa:", error);
toast.error("Terjadi kesalahan saat memperbarui pasar desa");
}
};
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Pasar Desa</Title>
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box>
<TextInput
value={formData.nama}
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Nama Produk</Text>}
placeholder='Masukkan nama produk'
/>
<TextInput
value={formData.harga}
onChange={(e) => setFormData({ ...formData, harga: Number(e.target.value) })}
label={<Text fw={"bold"} fz={"sm"}>Harga Produk</Text>}
placeholder='Masukkan harga produk'
/>
<TextInput
type="number"
min={0}
max={5}
step={0.1} // bisa pakai 0.1 biar support desimal
value={formData.rating}
onChange={(e) => setFormData({ ...formData, rating: Number(e.target.value) })}
label={<Text fw={"bold"} fz={"sm"}>Rating Produk</Text>}
placeholder='Masukkan rating produk (0-5)'
/>
<TextInput
value={formData.alamatUsaha}
onChange={(e) => setFormData({ ...formData, alamatUsaha: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Alamat Usaha</Text>}
placeholder='Masukkan alamat usaha'
/>
<TextInput
value={formData.alamatUsaha}
onChange={(e) => setFormData({ ...formData, alamatUsaha: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Alamat Usaha</Text>}
placeholder='Masukkan alamat usaha'
/>
<MultiSelect
value={formData.kategoriId}
onChange={(val) => setFormData({ ...formData, kategoriId: val })}
label={<Text fw={"bold"} fz={"sm"}>Kategori Produk</Text>}
placeholder='Pilih kategori produk'
data={
pasarState.kategoriProduk.findMany.data?.map((v) => ({
value: v.id, // Make sure this is using the ID
label: v.nama
})) || []
}
clearable
searchable
required
error={!formData.kategoriId.length ? "Pilih minimal satu kategori" : undefined}
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditPasarDesa;

View File

@@ -0,0 +1,131 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Flex, Text, Image, Skeleton } from '@mantine/core';
import { IconArrowBack, IconX, IconEdit } from '@tabler/icons-react';
import { useRouter, useParams } from 'next/navigation';
import React, { useState } from 'react';
import { useProxy } from 'valtio/utils';
import pasarDesaState from '../../../../_state/ekonomi/pasar-desa/pasar-desa';
import { useShallowEffect } from '@mantine/hooks';
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
function DetailPasarDesa() {
const statePasar = useProxy(pasarDesaState)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter();
useShallowEffect(() => {
statePasar.pasarDesa.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
statePasar.pasarDesa.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/ekonomi/pasar-desa/produk-pasar-desa")
}
}
if (!statePasar.pasarDesa.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Pasar Desa</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Produk</Text>
<Text fz={"lg"}>{statePasar.pasarDesa.findUnique.data?.nama}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Harga Produk</Text>
<Text fz={"lg"}>Rp.{statePasar.pasarDesa.findUnique.data?.harga}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Rating Produk</Text>
<Text fz={"lg"}>{statePasar.pasarDesa.findUnique.data?.rating}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Alamat Usaha</Text>
<Text fz={"lg"}>{statePasar.pasarDesa.findUnique.data?.alamatUsaha}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
<Image src={statePasar.pasarDesa.findUnique.data?.image?.link} alt="gambar" />
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Kategori</Text>
<Stack gap={4}>
{statePasar.pasarDesa.findUnique.data?.KategoriToPasar &&
statePasar.pasarDesa.findUnique.data.KategoriToPasar.length > 0 ? (
statePasar.pasarDesa.findUnique.data.KategoriToPasar.map((kategori) => (
<Text fz={"lg"} key={kategori.id}>
{kategori.kategori.nama}
</Text>
))
) : (
<Text fz={"lg"} c="dimmed">
Tidak ada kategori
</Text>
)}
</Stack>
</Box>
<Box>
<Flex gap={"xs"}>
<Button
onClick={() => {
if (statePasar.pasarDesa.findUnique.data) {
setSelectedId(statePasar.pasarDesa.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={!statePasar.pasarDesa.findUnique.data}
color="red">
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (statePasar.pasarDesa.findUnique.data) {
router.push(`/admin/ekonomi/pasar-desa/produk-pasar-desa/${statePasar.pasarDesa.findUnique.data.id}/edit`);
}
}}
disabled={!statePasar.pasarDesa.findUnique.data}
color="green">
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus produk ini?"
/>
</Box>
);
}
export default DetailPasarDesa;

View File

@@ -0,0 +1,192 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, MultiSelect, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import pasarDesaState from '../../../../_state/ekonomi/pasar-desa/pasar-desa';
function CreatePasarDesa() {
const router = useRouter();
const statePasar = useProxy(pasarDesaState)
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
useEffect(() => {
statePasar.kategoriProduk.findMany.load();
}, []);
const resetForm = () => {
statePasar.pasarDesa.create.form = {
nama: "",
harga: 0,
alamatUsaha: "",
imageId: "",
rating: 0,
kategoriId: [],
};
setPreviewImage(null);
setFile(null);
};
const handleSubmit = async () => {
if (!file) {
return toast.warn("Pilih file gambar terlebih dahulu");
}
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
})
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal mengupload file");
}
statePasar.pasarDesa.create.form.imageId = uploaded.id;
await statePasar.pasarDesa.create.create();
resetForm();
router.push("/admin/ekonomi/pasar-desa/produk-pasar-desa")
}
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Pasar Desa</Title>
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box>
<TextInput
value={statePasar.pasarDesa.create.form.nama}
onChange={(val) => {
statePasar.pasarDesa.create.form.nama = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Produk</Text>}
placeholder='Masukkan nama produk'
/>
<TextInput
value={statePasar.pasarDesa.create.form.harga}
onChange={(val) => {
statePasar.pasarDesa.create.form.harga = Number(val.target.value);
}}
label={<Text fw={"bold"} fz={"sm"}>Harga Produk</Text>}
placeholder='Masukkan harga produk'
/>
<TextInput
type="number"
min={0}
max={5}
step={0.1} // bisa pakai 0.1 biar support desimal
value={statePasar.pasarDesa.create.form.rating}
onChange={(val) => {
const value = Number(val.target.value);
// Validasi manual juga boleh (jaga-jaga)
if (value >= 0 && value <= 5) {
statePasar.pasarDesa.create.form.rating = value;
}
}}
label={<Text fw={"bold"} fz={"sm"}>Rating Produk</Text>}
placeholder='Masukkan rating produk (0-5)'
/>
<TextInput
value={statePasar.pasarDesa.create.form.alamatUsaha}
onChange={(val) => {
statePasar.pasarDesa.create.form.alamatUsaha = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Alamat Usaha</Text>}
placeholder='Masukkan alamat usaha'
/>
<MultiSelect
value={statePasar.pasarDesa.create.form.kategoriId}
onChange={(val) => {
statePasar.pasarDesa.create.form.kategoriId = val;
}}
label={<Text fw={"bold"} fz={"sm"}>Kategori Produk</Text>}
placeholder='Pilih kategori produk'
data={
statePasar.kategoriProduk.findMany.data?.map((v) => ({
value: v.id,
label: v.nama
})) || []
}
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreatePasarDesa;

View File

@@ -0,0 +1,93 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useShallowEffect } from '@mantine/hooks';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import pasarDesaState from '../../../_state/ekonomi/pasar-desa/pasar-desa';
import { useState } from 'react';
function PasarDesa() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Produk Pasar Desa'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListPasarDesa search={search} />
</Box>
);
}
function ListPasarDesa({ search }: { search: string }) {
const statePasar = useProxy(pasarDesaState.pasarDesa)
const router = useRouter();
useShallowEffect(() => {
statePasar.findMany.load()
}, [])
const filteredData = (statePasar.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.nama.toLowerCase().includes(keyword) ||
item.harga.toString().toLowerCase().includes(keyword) ||
item.rating.toString().toLowerCase().includes(keyword) ||
item.alamatUsaha.toLowerCase().includes(keyword)
);
});
if (!statePasar.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Produk Pasar Desa'
href='/admin/ekonomi/pasar-desa/produk-pasar-desa/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Produk</TableTh>
<TableTh>Harga Produk</TableTh>
<TableTh>Rating Produk</TableTh>
<TableTh>Alamat Usaha</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.nama}</TableTd>
<TableTd>Rp.{item.harga}</TableTd>
<TableTd>{item.rating}</TableTd>
<TableTd>{item.alamatUsaha}</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/ekonomi/pasar-desa/produk-pasar-desa/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>
</Box>
);
}
export default PasarDesa;

View File

@@ -0,0 +1,126 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import programKemiskinanState from '@/app/admin/(dashboard)/_state/ekonomi/program-kemiskinan';
import colors from '@/con/colors';
import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter, useParams } from 'next/navigation';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
function EditProgramKemiskinan() {
const router = useRouter();
const params = useParams() as { id: string };
const stateProgram = useProxy(programKemiskinanState);
const id = params.id;
useEffect(() => {
if (id) {
stateProgram.findUnique.load(id).then(() => {
const data = stateProgram.findUnique.data;
if (data) {
stateProgram.update.form = {
nama: data.nama || '',
deskripsi: data.deskripsi || '',
ikonUrl: data.ikonUrl || '',
statistik: {
tahun: data.statistik?.tahun?.toString() || '',
jumlah: data.statistik?.jumlah?.toString() || '',
},
};
}
});
}
}, [id]);
const handleSubmit = async () => {
stateProgram.update.id = id;
await stateProgram.update.update();
router.push('/admin/ekonomi/program-kemiskinan');
};
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant="subtle" color="blue">
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p="md">
<Stack gap="xs">
<Title order={4}>Edit Program Kemiskinan</Title>
<TextInput
value={stateProgram.update.form.nama}
onChange={(e) => {
stateProgram.update.form.nama = e.target.value;
}}
label={<Text fw="bold" fz="md">Judul Program</Text>}
placeholder="Masukkan judul program"
/>
<Box>
<Text fw="bold" fz="md">Deskripsi</Text>
<EditEditor
value={stateProgram.update.form.deskripsi}
onChange={(val) => {
stateProgram.update.form.deskripsi = val;
}}
/>
</Box>
<TextInput
value={stateProgram.update.form.ikonUrl}
onChange={(e) => {
stateProgram.update.form.ikonUrl = e.target.value;
}}
label={<Text fw="bold" fz="md">Ikon URL</Text>}
placeholder="Masukkan ikon url"
/>
<Text fw="bold" fz="md">Statistik Jumlah Masyarakat Miskin</Text>
<TextInput
type="number"
value={stateProgram.update.form.statistik.jumlah}
onChange={(e) => {
stateProgram.update.form.statistik.jumlah = e.target.value;
}}
label={<Text fw="bold" fz="md">Jumlah Masyarakat Miskin</Text>}
placeholder="Masukkan jumlah masyarakat miskin"
/>
<TextInput
type="number"
value={stateProgram.update.form.statistik.tahun}
onChange={(e) => {
stateProgram.update.form.statistik.tahun = e.target.value;
}}
label={<Text fw="bold" fz="md">Tahun</Text>}
placeholder="Masukkan tahun"
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditProgramKemiskinan;

View File

@@ -0,0 +1,115 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import programKemiskinanState from '../../../_state/ekonomi/program-kemiskinan';
function DetailProgramKemiskinan() {
const programState = useProxy(programKemiskinanState)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter();
const params = useParams()
useShallowEffect(() => {
programState.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
programState.delete.delete(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/ekonomi/program-kemiskinan")
}
}
if (!programState.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={40} />
</Stack>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Program Kemiskinan</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"}>Judul Program</Text>
<Text fz={"md"}>{programState.findUnique.data?.nama}</Text>
</Box>
<Box>
<Text fw={"bold"}>Deskripsi Singkat</Text>
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: programState.findUnique.data?.deskripsi }}></Text>
</Box>
<Box>
<Text fw={"bold"}>Ikon URL</Text>
<Text fz={"md"}>{programState.findUnique.data?.ikonUrl}</Text>
</Box>
<Text fw={"bold"}>Statistik Jumlah Masyarakat Miskin</Text>
<Box>
<Text fw={"bold"}>Jumlah Masyarakat Miskin</Text>
<Text fz={"md"}>{programState.findUnique.data?.statistik?.jumlah}</Text>
</Box>
<Box>
<Text fw={"bold"}>Tahun</Text>
<Text fz={"md"}>{programState.findUnique.data?.statistik?.tahun}</Text>
</Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (programState.findUnique.data) {
setSelectedId(programState.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={programState.delete.loading || !programState.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (programState.findUnique.data) {
router.push(`/admin/ekonomi/program-kemiskinan/${programState.findUnique.data.id}/edit`);
}
}}
disabled={!programState.findUnique.data}
color={"green"}
>
<IconEdit size={20} />
</Button>
</Flex>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus program kemiskinan ini?"
/>
</Box>
);
}
export default DetailProgramKemiskinan;

View File

@@ -1,45 +1,107 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import programKemiskinanState from '../../../_state/ekonomi/program-kemiskinan';
import CreateEditor from '../../../_com/createEditor';
import { useState } from 'react';
function CreateProgramKemiskinan() {
const programState = useProxy(programKemiskinanState)
const router = useRouter();
const [lineChart, setLineChart] = useState<any[]>([]);
const resetForm = () => {
programState.create.form = {
nama: "",
deskripsi: "",
ikonUrl: "",
statistik: {
tahun: "",
jumlah: "",
}
}
}
const handleSubmit = async () => {
const id = await programState.create.create();
if (id) {
const idStr = String(id);
await programState.findUnique.load(idStr);
if (programState.findUnique.data) {
setLineChart([programState.findUnique.data]);
}
}
resetForm()
router.push("/admin/ekonomi/program-kemiskinan")
}
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25}/>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{base: '100%', md: '50%'}} bg={colors['white-1']} p={'md'}>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Program Kemiskinan</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Judul Program</Text>}
placeholder='Masukkan judul program'
value={programState.create.form.nama}
onChange={(val) => {
programState.create.form.nama = val.target.value;
}}
label={<Text fw={"bold"} fz={"md"}>Judul Program</Text>}
placeholder='Masukkan judul program'
/>
<Box>
<Text fw={"bold"} fz={"md"}>Deskripsi</Text>
<CreateEditor
value={programState.create.form.deskripsi}
onChange={(val) => {
programState.create.form.deskripsi = val;
}}
/>
</Box>
<TextInput
value={programState.create.form.ikonUrl}
onChange={(val) => {
programState.create.form.ikonUrl = val.target.value;
}}
label={<Text fw={"bold"} fz={"md"}>Ikon URL</Text>}
placeholder='Masukkan ikon url'
/>
<Text fw={"bold"} fz={"md"}>Statistik Jumlah Masyarakat Miskin</Text>
<TextInput
type='number'
value={programState.create.form.statistik.jumlah}
onChange={(val) => {
programState.create.form.statistik.jumlah = val.target.value;
}}
label={<Text fw={"bold"} fz={"md"}>Jumlah Masyarakat Miskin</Text>}
placeholder='Masukkan jumlah masyarakat miskin'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Deskripsi Singkat</Text>}
placeholder='Masukkan deskripsi'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Jumlah Masyarakat Miskin</Text>}
placeholder='Masukkan jumlah masyarakat miskin'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Deskripsi</Text>}
placeholder='Masukkan deskripsi'
type='number'
value={programState.create.form.statistik.tahun}
onChange={(val) => {
programState.create.form.statistik.tahun = val.target.value;
}}
label={<Text fw={"bold"} fz={"md"}>Tahun</Text>}
placeholder='Masukkan tahun'
/>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Stack>
</Paper>
</Box>
</Box>
);
}

View File

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

View File

@@ -1,46 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
function EditProgramKemiskinan() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25}/>
</Button>
</Box>
<Paper w={{base: '100%', md: '50%'}} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Program Kemiskinan</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Judul Program</Text>}
placeholder='Masukkan judul program'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Deskripsi Singkat</Text>}
placeholder='Masukkan deskripsi'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Jumlah Masyarakat Miskin</Text>}
placeholder='Masukkan jumlah masyarakat miskin'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Deskripsi</Text>}
placeholder='Masukkan deskripsi'
/>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditProgramKemiskinan;

View File

@@ -1,26 +1,75 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import programKemiskinanState from '../../_state/ekonomi/program-kemiskinan';
import { useShallowEffect } from '@mantine/hooks';
import { useEffect, useState } from 'react';
import { CartesianGrid, Legend, Line, LineChart, Tooltip, XAxis, YAxis } from 'recharts';
function ProgramKemiskinan() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Program Kemiskinan'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListProgramKemiskinan/>
<ListProgramKemiskinan search={search}/>
</Box>
);
}
function ListProgramKemiskinan() {
function ListProgramKemiskinan({ search }: { search: string }) {
const programState = useProxy(programKemiskinanState)
const router = useRouter();
const [lineChart, setLineChart] = useState<any[]>([]);
const [mounted, setMounted] = useState(false);
useShallowEffect(() => {
setMounted(true)
programState.findMany.load()
}, [])
useEffect(() => {
if (programState.findMany.data) {
const chartData = programState.findMany.data
.filter(item => item.statistik)
.map(item => ({
tahun: item.statistik?.tahun,
jumlah: Number(item.statistik?.jumlah)
}))
.sort((a, b) => (a.tahun || 0) - (b.tahun || 0)); // opsional, urutkan tahun
setLineChart(chartData);
}
}, [programState.findMany.data])
const filteredData = (programState.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.nama.toLowerCase().includes(keyword) ||
item.deskripsi.toLowerCase().includes(keyword) ||
item.statistik?.tahun.toString().includes(keyword)
);
});
if (!programState.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
@@ -35,22 +84,66 @@ function ListProgramKemiskinan() {
<TableTh>Deskripsi Singkat</TableTh>
<TableTh>Jumlah Masyarakat Miskin</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>Program A</TableTd>
<TableTd>Deskripsi Program A</TableTd>
<TableTd>100</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/ekonomi/program-kemiskinan/detail')}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.nama}</TableTd>
<TableTd>
<Text fz={'sm'} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd>
<TableTd>{item.statistik?.jumlah}</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/ekonomi/program-kemiskinan/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Table>
</Paper>
{/* Chart */}
<Box>
<Paper bg={colors['white-1']} p={'md'} >
<Stack>
<Box >
<Title pb={10} order={3}>Grafik Berdasarkan Responden</Title>
{mounted && lineChart.length > 0 ? (<Box style={{ width: '100%', height: 'auto', }}>
<Box w={"100%"} style={{overflowX: 'auto'}}>
<LineChart
width={820}
height={300}
data={lineChart}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="tahun" />
<YAxis />
<Tooltip
formatter={(value: any, name: string) => [`${value} orang`, name]}
labelFormatter={(label: any) => `Tahun: ${label}`}
/>
<Legend />
<Line
type="monotone"
dataKey="jumlah"
name="Jumlah per Tahun"
stroke={colors['blue-button']}
/>
</LineChart>
</Box>
</Box>
) : (
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text>
)}
</Box>
</Stack>
</Paper>
</Box>
</Box>
);
}

View File

@@ -0,0 +1,89 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import grafikSektorUnggulan from '@/app/admin/(dashboard)/_state/ekonomi/sektor-unggulan-desa';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
function EditSektorUnggulanDesa() {
const router = useRouter()
const params = useParams() as { id: string }
const stateGrafik = useProxy(grafikSektorUnggulan)
const id = params.id
// Load data saat komponen mount
useEffect(() => {
if (id) {
stateGrafik.findUnique.load(id).then(() => {
const data = stateGrafik.findUnique.data
if (data) {
stateGrafik.update.form = {
name: data.name || '',
description: data.description || '',
value: data.value || 0,
}
}
})
}
}, [id])
const handleSubmit = async () => {
// Set the ID before submitting
stateGrafik.update.id = id;
await stateGrafik.update.submit();
router.push('/admin/ekonomi/sektor-unggulan-desa')
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack size={20} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: '100%', md: '50%' }} p={'md'}>
<Stack>
<Title order={3}>Edit Sektor Unggulan Desa</Title>
<TextInput
label="Nama Sektor Unggulan"
placeholder="masukkan nama sektor unggulan"
value={stateGrafik.update.form.name}
onChange={(val) => {
stateGrafik.update.form.name = val.currentTarget.value;
}}
/>
<TextInput
label="Deskripsi Sektor Unggulan"
placeholder="masukkan deskripsi sektor unggulan"
value={stateGrafik.update.form.description}
onChange={(val) => {
stateGrafik.update.form.description = val.currentTarget.value;
}}
/>
<TextInput
label="Jumlah"
type="number"
placeholder="masukkan jumlah"
value={stateGrafik.update.form.value}
onChange={(val) => {
stateGrafik.update.form.value = Number(val.currentTarget.value);
}}
/>
<Button
mt={10}
bg={colors['blue-button']}
onClick={handleSubmit}
>
Simpan
</Button>
</Stack>
</Paper>
</Box>
)
}
export default EditSektorUnggulanDesa;

View File

@@ -0,0 +1,105 @@
'use client'
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import grafikSektorUnggulan from '../../../_state/ekonomi/sektor-unggulan-desa';
function DetailSektorUnggulanDesa() {
const stateGrafik = useProxy(grafikSektorUnggulan)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter();
useShallowEffect(() => {
stateGrafik.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
stateGrafik.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/ekonomi/sektor-unggulan-desa")
}
}
if (!stateGrafik.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Sektor Unggulan Desa</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Sektor Unggulan</Text>
<Text fz={"lg"}>{stateGrafik.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi Sektor Unggulan</Text>
<Text fz={"lg"}>{stateGrafik.findUnique.data?.description}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Jumlah Sektor Unggulan</Text>
<Text fz={"lg"}>{stateGrafik.findUnique.data?.value}</Text>
</Box>
<Box>
<Flex gap={"xs"}>
<Button
onClick={() => {
if (stateGrafik.findUnique.data) {
setSelectedId(stateGrafik.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={!stateGrafik.findUnique.data}
color="red">
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (stateGrafik.findUnique.data) {
router.push(`/admin/ekonomi/sektor-unggulan-desa/${stateGrafik.findUnique.data.id}/edit`);
}
}}
disabled={!stateGrafik.findUnique.data}
color="green">
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus sektor unggulan ini?"
/>
</Box>
);
}
export default DetailSektorUnggulanDesa;

View File

@@ -1,43 +1,89 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { Box, Button, Group, Paper, Stack, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { KeamananEditor } from '../../../keamanan/_com/keamananEditor';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import grafikSektorUnggulan from '../../../_state/ekonomi/sektor-unggulan-desa';
function CreateSektorUnggulanDesa() {
const router = useRouter();
const stateGrafik= useProxy(grafikSektorUnggulan);
const [chartData, setChartData] = useState<any[]>([]);
const router = useRouter()
const resetForm = () => {
stateGrafik.create.form = {
name: "",
description: "",
value: 0,
}
}
const handleSubmit = async () => {
const id = await stateGrafik.create.create();
if (id) {
const idStr = String(id);
await stateGrafik.findUnique.load(idStr);
if (stateGrafik.findUnique.data) {
setChartData([stateGrafik.findUnique.data]);
}
}
resetForm();
router.push("/admin/ekonomi/sektor-unggulan-desa");
}
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack size={20} />
</Button>
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Sektor Unggulan Desa</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Sektor Unggulan</Text>}
placeholder='Masukkan nama sektor unggulan'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Sektor Unggulan</Text>
<KeamananEditor
showSubmit={false}
<Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Title order={4}>Tambah Grafik Hasil Kepuasan Masyarakat</Title>
<Stack gap={"xs"}>
<TextInput
label="Nama Sektor Unggulan"
type="text"
value={stateGrafik.create.form.name}
placeholder="Masukkan nama sektor unggulan"
onChange={(val) => {
stateGrafik.create.form.name = val.currentTarget.value;
}}
/>
</Box>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Jumlah Sektor Unggulan</Text>}
placeholder='Masukkan jumlah sektor unggulan'
/>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
<TextInput
label="Deskripsi Sektor Unggulan"
type="text"
value={stateGrafik.create.form.description}
placeholder="Masukkan deskripsi sektor unggulan"
onChange={(val) => {
stateGrafik.create.form.description = val.currentTarget.value;
}}
/>
<TextInput
label="Jumlah"
type="number"
value={stateGrafik.create.form.value}
placeholder="Masukkan jumlah"
onChange={(val) => {
stateGrafik.create.form.value = Number(val.currentTarget.value);
}}
/>
<Group>
<Button
bg={colors['blue-button']}
mt={10}
onClick={handleSubmit}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
</Box>
);
}

View File

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

View File

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

View File

@@ -1,26 +1,65 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { Box, Button, Paper, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import grafikSektorUnggulan from '../../_state/ekonomi/sektor-unggulan-desa';
import { useProxy } from 'valtio/utils';
import { useMediaQuery, useShallowEffect } from '@mantine/hooks';
import { Bar, BarChart, Legend, Tooltip, XAxis, YAxis } from 'recharts';
function SektorUnggulanDesa() {
const [search, setSearch] = useState('');
return (
<Box>
<HeaderSearch
title='Sektor Unggulan Desa'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListSektorUnggulanDesa/>
<ListSektorUnggulanDesa search={search} />
</Box>
);
}
function ListSektorUnggulanDesa() {
function ListSektorUnggulanDesa({ search }: { search: string }) {
const router = useRouter();
const state = useProxy(grafikSektorUnggulan);
const [chartData, setChartData] = useState<{id: string; name: string; description: string | null; value: number | null}[]>([]);
const [mounted, setMounted] = useState(false); // untuk memastikan DOM sudah ready
const isTablet = useMediaQuery('(max-width: 1024px)')
const isMobile = useMediaQuery('(max-width: 768px)')
const filteredData = (state.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
(item.value?.toString() || '').toLowerCase().includes(keyword)
);
});
useShallowEffect(() => {
setMounted(true)
state.findMany.load()
}, [])
useEffect(() => {
setMounted(true);
if (state.findMany.data) {
setChartData(state.findMany.data.map((item) => ({
id: item.id,
name: item.name,
description: item.description,
value: Number(item.value),
})));
}
}, [state.findMany.data]);
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
@@ -34,21 +73,48 @@ function ListSektorUnggulanDesa() {
<TableTh>Nama Sektor Unggulan</TableTh>
<TableTh>Deskripsi Sektor Unggulan</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>Sektor 1</TableTd>
<TableTd>Deskripsi Sektor 1</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/ekonomi/sektor-unggulan-desa/detail')}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>{item.description}</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/ekonomi/sektor-unggulan-desa/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Table>
</Paper>
{/* Chart */}
{!mounted && !chartData ? (
<Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}>
<Paper bg={colors['white-1']} p={'md'}>
<Title pb={10} order={3}>Grafik Hasil Kepuasan Masyarakat</Title>
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text>
</Paper>
</Box>
) : (
<Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}>
<Paper bg={colors['white-1']} p={'md'}>
<Title pb={10} order={4}>Grafik Sektor Unggulan Desa</Title>
{mounted && chartData.length > 0 && (
<BarChart width={isMobile ? 450 : isTablet ? 500 : 550} height={350} data={chartData} >
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="value" fill={colors['blue-button']} name="Jumlah" />
</BarChart>
)}
</Paper>
</Box>
)}
</Box>
);
}

View File

@@ -0,0 +1,67 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
function LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter()
const pathname = usePathname()
const tabs = [
{
label: "Posisi Organisasi",
value: "posisiorganisasi",
href: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi"
},
{
label: "Pegawai",
value: "pegawai",
href: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai"
},
{
label: "Hubungan Organisasi",
value: "hubunganorganisasi",
href: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/hubungan-organisasi"
},
];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
const handleTabChange = (value: string | null) => {
const tab = tabs.find(t => t.value === value)
if (tab) {
router.push(tab.href)
}
setActiveTab(value)
}
useEffect(() => {
const match = tabs.find(tab => tab.href === pathname)
if (match) {
setActiveTab(match.value)
}
}, [pathname])
return (
<Stack>
<Title order={3}>Struktur Organisasi & SK Pengurus BUMDesa</Title>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
{tabs.map((e, i) => (
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
))}
</TabsList>
{tabs.map((e, i) => (
<TabsPanel key={i} value={e.value}>
{/* Konten dummy, bisa diganti tergantung routing */}
<></>
</TabsPanel>
))}
</Tabs>
{children}
</Stack>
);
}
export default LayoutTabs;

View File

@@ -0,0 +1,94 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client';
import { useEffect, useState } from 'react';
import { useParams, useRouter } from 'next/navigation';
import { Box, Button, Paper, Select, Stack, TextInput, Title } from '@mantine/core';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import strukturorganisasiState from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
export default function EditHubunganOrganisasi() {
const router = useRouter();
const { id } = useParams<{ id: string }>();
const state = useProxy(strukturorganisasiState.hubunganOrganisasi);
const pegawaiList = strukturorganisasiState.pegawai.findMany.data;
const [form, setForm] = useState({
atasanId: '',
bawahanId: '',
tipe: '',
});
useEffect(() => {
strukturorganisasiState.pegawai.findMany.load();
if (id) {
state.edit.load(id).then(data => {
if (data) {
setForm({
atasanId: data.atasanId,
bawahanId: data.bawahanId,
tipe: data.tipe || '',
});
}
});
}
}, [id]);
const handleSubmit = async () => {
if (!form.atasanId || !form.bawahanId) {
toast.warn("Atasan dan bawahan harus diisi");
return;
}
state.edit.id = id;
state.edit.form = form;
const result = await state.edit.update();
if (result) {
toast.success("Data berhasil diperbarui");
router.push('/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/hubungan-organisasi');
}
};
return (
<Box>
<Paper p="md" w={{ base: '100%', md: '50%' }}>
<Stack>
<Title order={3}>Edit Hubungan Organisasi</Title>
<Select
label="Atasan"
placeholder="Pilih atasan"
searchable
data={pegawaiList?.map(p => ({
value: p.id,
label: p.namaLengkap,
})) || []}
value={form.atasanId}
onChange={(val) => setForm({ ...form, atasanId: val || '' })}
/>
<Select
label="Bawahan"
placeholder="Pilih bawahan"
searchable
data={pegawaiList?.map(p => ({
value: p.id,
label: p.namaLengkap,
})) || []}
value={form.bawahanId}
onChange={(val) => setForm({ ...form, bawahanId: val || '' })}
/>
<TextInput
label="Tipe"
placeholder="Contoh: langsung_melapor"
value={form.tipe}
onChange={(e) => setForm({ ...form, tipe: e.currentTarget.value })}
/>
<Button onClick={handleSubmit} color="blue">Simpan</Button>
</Stack>
</Paper>
</Box>
);
}

View File

@@ -0,0 +1,78 @@
'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { Box, Button, Paper, Select, Stack, TextInput, Title } from '@mantine/core';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import strukturorganisasiState from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
export default function CreateHubunganOrganisasi() {
const router = useRouter();
const state = useProxy(strukturorganisasiState.hubunganOrganisasi);
const pegawaiList = strukturorganisasiState.pegawai.findMany.data;
const [form, setForm] = useState({
atasanId: '',
bawahanId: '',
tipe: '',
});
useEffect(() => {
strukturorganisasiState.pegawai.findMany.load();
}, []);
const handleSubmit = async () => {
if (!form.atasanId || !form.bawahanId) {
toast.warn("Atasan dan bawahan harus diisi");
return;
}
state.create.form = form;
const result = await state.create.create();
if (result) {
toast.success("Hubungan Organisasi berhasil ditambahkan");
router.push('/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/hubungan-organisasi');
}
};
return (
<Box>
<Paper p="md" w={{ base: '100%', md: '50%' }}>
<Stack>
<Title order={3}>Create Hubungan Organisasi</Title>
<Select
label="Atasan"
placeholder="Pilih atasan"
searchable
data={pegawaiList?.map(p => ({
value: p.id,
label: p.namaLengkap,
})) || []}
value={form.atasanId}
onChange={(val) => setForm({ ...form, atasanId: val || '' })}
/>
<Select
label="Bawahan"
placeholder="Pilih bawahan"
searchable
data={pegawaiList?.map(p => ({
value: p.id,
label: p.namaLengkap,
})) || []}
value={form.bawahanId}
onChange={(val) => setForm({ ...form, bawahanId: val || '' })}
/>
<TextInput
label="Tipe"
placeholder="Contoh: langsung_melapor"
value={form.tipe}
onChange={(e) => setForm({ ...form, tipe: e.currentTarget.value })}
/>
<Button onClick={handleSubmit} color="blue">Simpan</Button>
</Stack>
</Paper>
</Box>
);
}

View File

@@ -0,0 +1,130 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import strukturorganisasiState from '../../../_state/ekonomi/struktur-organisasi/struktur-organisasi';
import { useState } from 'react';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function HubunganOrganisasi() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Hubungan Organisasi'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListHubunganOrganisasi search={search} />
</Box>
);
}
function ListHubunganOrganisasi({ search }: { search: string }) {
const stateOrganisasi = useProxy(strukturorganisasiState.hubunganOrganisasi);
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const router = useRouter();
useShallowEffect(() => {
stateOrganisasi.findMany.load();
}, []);
const handleHapus = () => {
if (selectedId) {
stateOrganisasi.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
}
};
const filteredData = (stateOrganisasi.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.atasan?.namaLengkap?.toLowerCase().includes(keyword) ||
item.bawahan?.namaLengkap?.toLowerCase().includes(keyword) ||
item.tipe?.toLowerCase().includes(keyword)
);
});
if (!stateOrganisasi.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
);
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Hubungan Organisasi'
href='/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/hubungan-organisasi/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Atasan</TableTh>
<TableTh>Bawahan</TableTh>
<TableTh>Tipe</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Hapus</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData
.sort((a, b) =>
a.atasan?.namaLengkap.localeCompare(b.atasan?.namaLengkap)
)
.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.atasan?.namaLengkap}</TableTd>
<TableTd>{item.bawahan?.namaLengkap}</TableTd>
<TableTd>{item.tipe}</TableTd>
<TableTd>
<Button
bg="green"
onClick={() => router.push(`/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/hubungan-organisasi/${item.id}`)}
>
<IconEdit size={25} />
</Button>
</TableTd>
<TableTd>
<Button
color="red"
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus hubungan organisasi ini?'
/>
</Box>
);
}
export default HubunganOrganisasi;

View File

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

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