Compare commits

...

35 Commits

Author SHA1 Message Date
7439eb7687 API & UI Menu Lingkungan, Submenu Penglolaan Sampah 2025-07-17 17:09:28 +08:00
49a1084099 API & UI Menu Inovasi, SubMenu Layanan Online Desa 2025-07-17 15:31:10 +08:00
cde6c91cd4 API & UI Menu Inovasi, Submenu Layanan Online Desa: Tab 1-4 2025-07-17 11:44:53 +08:00
55433128a9 API & UI Menu Inovasi & Submenu Layan Online Desa 2 tabs 2025-07-16 00:23:15 +08:00
e8ad74d118 Merge branch 'nico/push-stg' into nico/15-jul-25 2025-07-15 11:51:13 +08:00
99c1fd1004 UI & API Menu Inovasi, SubMenu : Kolaborasi Inovasi & Info Teknologi 2025-07-15 11:48:56 +08:00
03c0523194 UI & API Menu Inovasi, SubMenu Program Kreatif Desa 2025-07-14 17:35:38 +08:00
ae328f40a0 UI & API Menu Inovasi, SubMenu Program Kreatif Desa 2025-07-14 17:14:02 +08:00
6e109ffe00 FIX UI Admin Menu PPID 2025-07-14 14:33:08 +08:00
c4aea568e9 UI & API Smart DIgital Village 2025-07-14 10:24:59 +08:00
c823462a47 Merge pull request #39 from bipproduction/nico/4-jul-25
Nico/4 jul 25:
Fix UI & API Admin Menu Ekonomi Pasar Desa
2025-07-04 11:20:15 +08:00
32a75bcb01 Merge pull request #38 from bipproduction/nico/1-jul-25
UI & API Menu Keamanan baru 3 Menu : Keamanan Lingkungan, Polsek Terd
2025-07-01 11:19:03 +08:00
9f39eb41ab Merge pull request #36 from bipproduction/nico/30-jun-2025
Nico/30 jun 2025
2025-06-30 11:15:28 +08:00
81ea18cb07 Merge branch 'nico/push-stg' into nico/30-jun-2025 2025-06-30 11:15:06 +08:00
21085ce342 Merge branch 'staging' into nico/push-stg 2025-06-26 11:19:54 +08:00
88784f00f6 Merge pull request #34 from bipproduction/nico/25-jun-25
Nico/25 jun 25
2025-06-26 11:01:15 +08:00
456342851b Merge pull request #33 from bipproduction/nico/25-jun-25
Nico/25 jun 25
staging versi 0.1.3
2025-06-25 16:32:28 +08:00
fa922c7127 Merge pull request #31 from bipproduction/nico/push-stg
Nico/push stg
2025-06-18 16:38:45 +08:00
45acdba93f Merge pull request #30 from bipproduction/nico/18-jun-25
Nico/18 jun 25
2025-06-18 16:38:10 +08:00
cb8d561467 Merge pull request #29 from bipproduction/nico/push-stg
Nico/push stg 18 Juni 2025:
UI & API Menu PPID & Desa Clear
2025-06-18 15:35:49 +08:00
85a0cb6d56 Merge pull request #28 from bipproduction/nico/18-jun-25
Nico/18 jun 25:
UI & API Menu PPID & Desa Clear
2025-06-18 15:34:27 +08:00
4f2c565b2e Merge pull request #27 from bipproduction/nico/push-stg
Nico/push stg

Senin, 2 Juni 2025 : 
Yang Sudah Di Kerjakan
* Tampilan UI Admin di menu desa
Yang Lagi Dikerjakan:
* Tampilan UI Admin di menu kesehatan

Yang Akan Dikerjakan:
* API Create, edit dan delete pengumuman
* Tampilan UI Admin di menu keamanan
2025-06-02 22:30:15 +08:00
c4adc9bb22 Merge pull request #26 from bipproduction/nico/1-jun-25
Senin, 2 Juni 2025 : 
Yang Sudah Di Kerjakan
* Tampilan UI Admin di menu desa
Yang Lagi Dikerjakan:
* Tampilan UI Admin di menu kesehatan

Yang Akan Dikerjakan:
* API Create, edit dan delete pengumuman
* Tampilan UI Admin di menu keamanan
2025-06-02 22:28:58 +08:00
9d572f82c3 Merge pull request #25 from bipproduction/nico/push-stg
Jum'at, 30 May 2025 : 
Yang Sudah Di Kerjakan
- Tampilan UI Admin di menu inovasi
- API Create, edit dan delete potensi
- Tampilan UI Landing Page sudah sesuai di mobile

Yang Lagi Dikerjakan:
- Progress Tampilan UI Admin Di Menu lingkungan
- Progress API Create, edit dan delete potensi

Yang Akan Dikerjakan:
- API Create, edit dan delete pengumuman
- Tampilan UI Admin Di Menu Pendidikan
2025-05-30 21:16:15 +08:00
452692f314 Merge pull request #24 from bipproduction/nico/30-may-25
Jum'at, 30 May 2025 : 
Yang Sudah Di Kerjakan
- Tampilan UI Admin di menu inovasi
- API Create, edit dan delete potensi
- Tampilan UI Landing Page sudah sesuai di mobile

Yang Lagi Dikerjakan:
- Progress Tampilan UI Admin Di Menu lingkungan
- Progress API Create, edit dan delete potensi

Yang Akan Dikerjakan:
- API Create, edit dan delete pengumuman
- Tampilan UI Admin Di Menu Pendidikan
2025-05-30 21:15:27 +08:00
5010677bc8 Merge pull request #23 from bipproduction/nico/27-may-25-01
Selasa, 27 May 2025:
Yang Sudah Di Kerjakan
* Tampilan UI Admin di menu ekonomi
* API Create, edit dan delete berita

Yang Lagi Dikerjakan:
* Progress Tampilan UI Admin Di Menu Inovasi
* Progress API ProfilePPID

Yang Akan Dikerjakan:
* API Menu Lain
* Tampilan UI Admin Di Menu Lingkungan
* Tampilan UI Admin Di Menu Pendidikan
2025-05-27 11:24:24 +08:00
7af3fbff2d Merge pull request #22 from bipproduction/nico/26-may-25
Senin, 26 May 2025 :
Yang Sudah Di Kerjakan
* Tampilan UI Admin di menu ekonomi
* API Create, edit dan delete berita

Yang Akan Dikerjakan:
* API ProfilePPID
* Tampilan UI Admin Di Menu Inovasi
2025-05-26 17:16:07 +08:00
34ca736dda Merge pull request #21 from bipproduction/nico/26-may-25
Nico/26 may 25
Senin, 26 May 2025 : 
Yang Sudah Di Kerjakan
* Tampilan UI Admin di menu ekonomi
* API Create, dan delete berita

Yang Akan Dikerjakan:
* API Di Menu Desa : Edit Berita
* Tampilan UI Admin Di Menu Inovasi
2025-05-26 14:30:07 +08:00
3b61f54509 Merge pull request #20 from bipproduction/nico/8-may-25
Nico/8 may 25
2025-05-09 10:36:45 +08:00
fbe0c19d22 Merge pull request #19 from bipproduction/nico/2-may-25
Fix Eror Admin Persentase & Grafik
2025-05-02 16:11:38 +08:00
f8914ab78f Merge pull request #18 from bipproduction/nico/30-apr-25
Admin Dashboard Bagian Data Kesehatan
2025-04-30 16:42:45 +08:00
8f3ee2f831 Push STG 2025-04-30 16:32:29 +08:00
ddf0ca62c4 Merge pull request #17 from bipproduction/nico/28-apr-25
Dashboard Admin
2025-04-30 16:16:48 +08:00
f8cdd3abdd Merge pull request #13 from bipproduction/nico/24-mar-25
tamabah versi
2025-03-24 15:35:55 +08:00
64bc739496 Merge pull request #12 from bipproduction/nico/24-mar-25
Nico/24 mar 25
2025-03-24 15:33:27 +08:00
177 changed files with 10157 additions and 1780 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -1,6 +1,6 @@
{
"name": "desa-darmasaba",
"version": "0.1.3",
"version": "0.1.5",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
@@ -50,9 +50,10 @@
"embla-carousel-autoplay": "^8.5.2",
"embla-carousel-react": "^7.1.0",
"form-data": "^4.0.2",
"framer-motion": "^12.4.1",
"framer-motion": "^12.23.5",
"get-port": "^7.1.0",
"jotai": "^2.12.3",
"list": "^2.0.19",
"lodash": "^4.17.21",
"motion": "^12.4.1",
"nanoid": "^5.1.5",

View File

@@ -0,0 +1,245 @@
/*
Warnings:
- You are about to drop the column `belanjaId` on the `ApbDesa` table. All the data in the column will be lost.
- You are about to drop the column `pembiayaanId` on the `ApbDesa` table. All the data in the column will be lost.
- You are about to drop the column `pendapatanId` on the `ApbDesa` table. All the data in the column will be lost.
- You are about to drop the `KegiatanSubak` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `KlasifikasiBelanja` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `RincianBelanja` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `_ApbDesaToKegiatanSubak` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `_BelanjaToKlasifikasiBelanja` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `_KlasifikasiBelanjaToRincianBelanja` table. If the table is not empty, all the data it contains will be lost.
- Changed the type of `tanggal` on the `DaftarInformasiPublik` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.
- Added the required column `updatedAt` to the `Pembiayaan` table without a default value. This is not possible if the table is not empty.
*/
-- DropForeignKey
ALTER TABLE "ApbDesa" DROP CONSTRAINT "ApbDesa_belanjaId_fkey";
-- DropForeignKey
ALTER TABLE "ApbDesa" DROP CONSTRAINT "ApbDesa_pembiayaanId_fkey";
-- DropForeignKey
ALTER TABLE "ApbDesa" DROP CONSTRAINT "ApbDesa_pendapatanId_fkey";
-- DropForeignKey
ALTER TABLE "_ApbDesaToKegiatanSubak" DROP CONSTRAINT "_ApbDesaToKegiatanSubak_A_fkey";
-- DropForeignKey
ALTER TABLE "_ApbDesaToKegiatanSubak" DROP CONSTRAINT "_ApbDesaToKegiatanSubak_B_fkey";
-- DropForeignKey
ALTER TABLE "_BelanjaToKlasifikasiBelanja" DROP CONSTRAINT "_BelanjaToKlasifikasiBelanja_A_fkey";
-- DropForeignKey
ALTER TABLE "_BelanjaToKlasifikasiBelanja" DROP CONSTRAINT "_BelanjaToKlasifikasiBelanja_B_fkey";
-- DropForeignKey
ALTER TABLE "_KlasifikasiBelanjaToRincianBelanja" DROP CONSTRAINT "_KlasifikasiBelanjaToRincianBelanja_A_fkey";
-- DropForeignKey
ALTER TABLE "_KlasifikasiBelanjaToRincianBelanja" DROP CONSTRAINT "_KlasifikasiBelanjaToRincianBelanja_B_fkey";
-- AlterTable
ALTER TABLE "ApbDesa" DROP COLUMN "belanjaId",
DROP COLUMN "pembiayaanId",
DROP COLUMN "pendapatanId",
ADD COLUMN "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true;
-- AlterTable
ALTER TABLE "DaftarInformasiPublik" DROP COLUMN "tanggal",
ADD COLUMN "tanggal" DATE NOT NULL;
-- AlterTable
ALTER TABLE "Pembiayaan" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL;
-- DropTable
DROP TABLE "KegiatanSubak";
-- DropTable
DROP TABLE "KlasifikasiBelanja";
-- DropTable
DROP TABLE "RincianBelanja";
-- DropTable
DROP TABLE "_ApbDesaToKegiatanSubak";
-- DropTable
DROP TABLE "_BelanjaToKlasifikasiBelanja";
-- DropTable
DROP TABLE "_KlasifikasiBelanjaToRincianBelanja";
-- CreateTable
CREATE TABLE "DesaDigital" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"deskripsi" TEXT NOT NULL,
"imageId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"isActive" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "DesaDigital_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ProgramKreatif" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"deskripsi" TEXT NOT NULL,
"icon" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"isActive" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "ProgramKreatif_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "KolaborasiInovasi" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"tahun" INTEGER NOT NULL,
"slug" TEXT NOT NULL,
"deskripsi" TEXT NOT NULL,
"kolaborator" TEXT NOT NULL,
"imageId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"isActive" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "KolaborasiInovasi_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "InfoTekno" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"deskripsi" TEXT NOT NULL,
"imageId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"isActive" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "InfoTekno_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "AjukanIdeInovatif" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"alamat" TEXT NOT NULL,
"namaIde" TEXT NOT NULL,
"deskripsi" TEXT NOT NULL,
"masalah" TEXT NOT NULL,
"benefit" 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 "AjukanIdeInovatif_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "AdministrasiOnline" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"alamat" TEXT NOT NULL,
"nomorTelepon" TEXT NOT NULL,
"jenisLayananId" 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 "AdministrasiOnline_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "JenisLayanan" (
"id" TEXT NOT NULL,
"nama" TEXT NOT NULL,
"deskripsi" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"isActive" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "JenisLayanan_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "_ApbDesaPembiayaan" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL,
CONSTRAINT "_ApbDesaPembiayaan_AB_pkey" PRIMARY KEY ("A","B")
);
-- CreateTable
CREATE TABLE "_ApbDesaBelanja" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL,
CONSTRAINT "_ApbDesaBelanja_AB_pkey" PRIMARY KEY ("A","B")
);
-- CreateTable
CREATE TABLE "_ApbDesaPendapatan" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL,
CONSTRAINT "_ApbDesaPendapatan_AB_pkey" PRIMARY KEY ("A","B")
);
-- CreateIndex
CREATE INDEX "_ApbDesaPembiayaan_B_index" ON "_ApbDesaPembiayaan"("B");
-- CreateIndex
CREATE INDEX "_ApbDesaBelanja_B_index" ON "_ApbDesaBelanja"("B");
-- CreateIndex
CREATE INDEX "_ApbDesaPendapatan_B_index" ON "_ApbDesaPendapatan"("B");
-- AddForeignKey
ALTER TABLE "DesaDigital" ADD CONSTRAINT "DesaDigital_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "KolaborasiInovasi" ADD CONSTRAINT "KolaborasiInovasi_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "InfoTekno" ADD CONSTRAINT "InfoTekno_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "AdministrasiOnline" ADD CONSTRAINT "AdministrasiOnline_jenisLayananId_fkey" FOREIGN KEY ("jenisLayananId") REFERENCES "JenisLayanan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_ApbDesaPembiayaan" ADD CONSTRAINT "_ApbDesaPembiayaan_A_fkey" FOREIGN KEY ("A") REFERENCES "ApbDesa"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_ApbDesaPembiayaan" ADD CONSTRAINT "_ApbDesaPembiayaan_B_fkey" FOREIGN KEY ("B") REFERENCES "Pembiayaan"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_ApbDesaBelanja" ADD CONSTRAINT "_ApbDesaBelanja_A_fkey" FOREIGN KEY ("A") REFERENCES "ApbDesa"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_ApbDesaBelanja" ADD CONSTRAINT "_ApbDesaBelanja_B_fkey" FOREIGN KEY ("B") REFERENCES "Belanja"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_ApbDesaPendapatan" ADD CONSTRAINT "_ApbDesaPendapatan_A_fkey" FOREIGN KEY ("A") REFERENCES "ApbDesa"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_ApbDesaPendapatan" ADD CONSTRAINT "_ApbDesaPendapatan_B_fkey" FOREIGN KEY ("B") REFERENCES "Pendapatan"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -74,22 +74,18 @@ model FileStorage {
ProgramKesehatan ProgramKesehatan[]
PenangananDarurat PenangananDarurat[]
KontakDarurat KontakDarurat[]
InfoWabahPenyakit InfoWabahPenyakit[]
KeamananLingkungan KeamananLingkungan[]
MenuTipsKeamanan MenuTipsKeamanan[]
Pelapor Pelapor[]
PasarDesa PasarDesa[]
KontakDaruratKeamanan KontakDaruratKeamanan[]
KontakItem KontakItem[]
Pegawai Pegawai[]
InfoWabahPenyakit InfoWabahPenyakit[]
KeamananLingkungan KeamananLingkungan[]
MenuTipsKeamanan MenuTipsKeamanan[]
Pelapor Pelapor[]
PasarDesa PasarDesa[]
KontakDaruratKeamanan KontakDaruratKeamanan[]
KontakItem KontakItem[]
Pegawai Pegawai[]
DesaDigital DesaDigital[]
KolaborasiInovasi KolaborasiInovasi[]
InfoTekno InfoTekno[]
PengaduanMasyarakat PengaduanMasyarakat[]
}
//========================================= MENU PPID ========================================= //
@@ -150,7 +146,7 @@ model DaftarInformasiPublik {
id String @id @default(cuid())
jenisInformasi String
deskripsi String
tanggal String
tanggal DateTime @db.Date
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
@@ -1312,7 +1308,7 @@ model Belanja {
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
ApbDesa ApbDesa[] @relation("ApbDesaBelanja")
ApbDesa ApbDesa[] @relation("ApbDesaBelanja")
}
model Pembiayaan {
@@ -1323,5 +1319,141 @@ model Pembiayaan {
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
ApbDesa ApbDesa[] @relation("ApbDesaPembiayaan")
ApbDesa ApbDesa[] @relation("ApbDesaPembiayaan")
}
// ========================================= INOVASI ========================================= //
// ========================================= DESA DIGITAL / SMART VILLAGE ========================================= //
model DesaDigital {
id String @id @default(cuid())
name String
deskripsi String @db.Text
image FileStorage @relation(fields: [imageId], references: [id])
imageId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= PROGRAM KREATIF ========================================= //
model ProgramKreatif {
id String @id @default(cuid())
name String
slug String @db.Text //deskripsi singkat
deskripsi String @db.Text //deskripsi panjang
icon String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= KOLABORASI INOVASI ========================================= //
model KolaborasiInovasi {
id String @id @default(cuid())
name String
tahun Int
slug String @db.Text //deskripsi singkat
deskripsi String @db.Text //deskripsi panjang
kolaborator String
image FileStorage @relation(fields: [imageId], references: [id])
imageId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= INFO TEKHNOLOGI TEPAT GUNA ========================================= //
model InfoTekno {
id String @id @default(cuid())
name String
deskripsi String @db.Text
image FileStorage @relation(fields: [imageId], references: [id])
imageId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= AJUKAN IDE INOVATIF ========================================= //
model AjukanIdeInovatif {
id String @id @default(cuid())
name String
alamat String
namaIde String
deskripsi String @db.Text
masalah String @db.Text
benefit String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= LAYANAN ONLINE DESA ========================================= //
model AdministrasiOnline {
id String @id @default(cuid())
name String
alamat String
nomorTelepon String
jenisLayanan JenisLayanan @relation(fields: [jenisLayananId], references: [id])
jenisLayananId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model JenisLayanan {
id String @id @default(uuid())
nama String
deskripsi String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
AdministrasiOnline AdministrasiOnline[]
}
model PengaduanMasyarakat {
id String @id @default(cuid())
name String
email String
nomorTelepon String
nik String
judulPengaduan String
lokasiKejadian String
image FileStorage @relation(fields: [imageId], references: [id])
imageId String
deskripsiPengaduan String @db.Text
jenisPengaduan JenisPengaduan @relation(fields: [jenisPengaduanId], references: [id])
jenisPengaduanId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model JenisPengaduan {
id String @id @default(uuid())
nama String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
PengaduanMasyarakat PengaduanMasyarakat[]
}
// ========================================= PENGELOLAAN SAMPAH ========================================= //
model PengelolaanSampah {
id String @id @default(cuid())
name String
icon String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}

View File

@@ -19,7 +19,7 @@ const HeaderSearch = ({
onChange,
}: HeaderSearchProps) => {
return (
<Grid>
<Grid mb={10}>
<GridCol span={{ base: 12, md: 9 }}>
<Title order={3}>{title}</Title>
</GridCol>

View File

@@ -263,6 +263,59 @@ const berita = proxy({
berita.edit.form = { ...defaultForm };
},
},
findFirst: {
data: null as Prisma.BeritaGetPayload<{
include: {
image: true;
kategoriBerita: true;
};
}> | null,
loading: false,
async load() {
this.loading = true;
try {
const res = await ApiFetch.api.desa.berita["find-first"].get();
if (res.status === 200 && res.data?.success) {
// Add type assertion to ensure type safety
berita.findFirst.data = res.data.data as Prisma.BeritaGetPayload<{
include: {
image: true;
kategoriBerita: true;
};
}> | null;
}
} catch (err) {
console.error("Gagal fetch berita terbaru:", err);
} finally {
this.loading = false;
}
},
},
findRecent: {
data: [] as Prisma.BeritaGetPayload<{
include: {
image: true;
kategoriBerita: true;
};
}>[],
loading: false,
async load() {
try {
this.loading = true;
const res = await ApiFetch.api.desa.berita["find-recent"].get();
if (res.status === 200 && res.data?.success) {
this.data = res.data.data ?? [];
}
} catch (error) {
console.error("Gagal fetch berita recent:", error);
} finally {
this.loading = false;
}
},
}
});

View File

@@ -68,11 +68,11 @@ const pengumuman = proxy({
},
findMany: {
data: null as
| Prisma.PengumumanGetPayload<{
include: {
| Prisma.PengumumanGetPayload<{
include: {
CategoryPengumuman: true;
}
}>[]
};
}>[]
| null,
async load() {
const res = await ApiFetch.api.desa.pengumuman["find-many"].get();
@@ -82,30 +82,28 @@ const pengumuman = proxy({
}
},
},
// findUnique: {
// data: null as
// | Prisma.PengumumanGetPayload<{
// include: {
// CategoryPengumuman: true;
// }
// }>
// | null,
// async load(id: string) {
// try {
// const res = await fetch(`/api/desa/pengumuman/${id}`);
// if (res.ok) {
// const data = await res.json();
// pengumuman.findUnique.data = data.data ?? null;
// } else {
// console.error('Failed to fetch pengumuman:', res.statusText);
// pengumuman.findUnique.data = null;
// }
// } catch (error) {
// console.error('Error fetching pengumuman:', error);
// pengumuman.findUnique.data = null;
// }
// },
// },
findUnique: {
data: null as Prisma.PengumumanGetPayload<{
include: {
CategoryPengumuman: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/desa/pengumuman/${id}`);
if (res.ok) {
const data = await res.json();
pengumuman.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch pengumuman:", res.statusText);
pengumuman.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching pengumuman:", error);
pengumuman.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
@@ -237,6 +235,55 @@ const pengumuman = proxy({
}
},
},
findFirst: {
data: null as Prisma.PengumumanGetPayload<{
include: {
CategoryPengumuman: true;
};
}> | null,
loading: false,
async load() {
this.loading = true;
try {
const res = await ApiFetch.api.desa.pengumuman["find-first"].get();
if (res.status === 200 && res.data?.success) {
// Add type assertion to ensure type safety
pengumuman.findFirst.data = res.data
.data as Prisma.PengumumanGetPayload<{
include: {
CategoryPengumuman: true;
};
}> | null;
}
} catch (err) {
console.error("Gagal fetch pengumuman terbaru:", err);
} finally {
this.loading = false;
}
},
},
findRecent: {
data: [] as Prisma.PengumumanGetPayload<{
include: {
CategoryPengumuman: true;
};
}>[],
loading: false,
async load() {
try {
this.loading = true;
const res = await ApiFetch.api.desa.pengumuman["find-recent"].get();
if (res.status === 200 && res.data?.success) {
this.data = res.data.data ?? [];
}
} catch (error) {
console.error("Gagal fetch pengumuman recent:", error);
} finally {
this.loading = false;
}
},
},
});
const stateDesaPengumuman = proxy({

View File

@@ -275,8 +275,7 @@ const kategoriProduk = proxy({
data: null as Array<{
id: string;
nama: string;
}>
| null,
}> | null,
async load() {
const res = await ApiFetch.api.ekonomi.kategoriproduk["find-many"].get();
if (res.status === 200) {
@@ -335,125 +334,135 @@ const kategoriProduk = proxy({
}
},
},
edit: {
id: "",
form: { ...kategoriProdukDefaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
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}`);
}
try {
const response = await fetch(`/api/ekonomi/kategoriproduk/${id}`, {
method: "GET",
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",
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
body: JSON.stringify({
nama: kategoriProduk.edit.form.nama,
}),
}
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;
}
);
// Clone the response to avoid 'body already read' error
const responseClone = response.clone();
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');
}
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) {
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;
// 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");
}
}
},
reset() {
kategoriProduk.edit.id = "";
kategoriProduk.edit.form = { ...kategoriProdukDefaultForm };
},
} 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
kategoriProduk,
});
export default pasarDesaState;

View File

@@ -0,0 +1,123 @@
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
const templateForm = z.object({
name: z.string().min(1).max(50),
deskripsi: z.string().min(1).max(5000),
alamat: z.string().min(1).max(5000),
namaIde: z.string().min(1).max(5000),
masalah: z.string().min(1).max(5000),
benefit: z.string().min(1).max(5000),
});
const defaultForm = {
name: "",
deskripsi: "",
alamat: "",
namaIde: "",
masalah: "",
benefit: "",
};
const ajukanIdeInovatifState = proxy({
create: {
form: { ...defaultForm },
loading: false,
async create() {
const cek = templateForm.safeParse(ajukanIdeInovatifState.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
ajukanIdeInovatifState.create.loading = true;
const res = await ApiFetch.api.inovasi.ajukanideinovatif["create"].post(
ajukanIdeInovatifState.create.form
);
if (res.status === 200) {
ajukanIdeInovatifState.findMany.load();
return toast.success("Ajukan Ide Inovatif berhasil di kirim");
}
console.log(res);
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
} finally {
ajukanIdeInovatifState.create.loading = false;
}
},
},
findMany: {
data: null as
| Prisma.AjukanIdeInovatifGetPayload<{
omit: {
isActive: true;
};
}>[]
| null,
async load() {
const res = await ApiFetch.api.inovasi.ajukanideinovatif["find-many"].get();
if (res.status === 200) {
ajukanIdeInovatifState.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.AjukanIdeInovatifGetPayload<{
omit: {
isActive: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/inovasi/ajukanideinovatif/${id}`);
if (res.ok) {
const data = await res.json();
ajukanIdeInovatifState.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
ajukanIdeInovatifState.findUnique.data = null;
}
} catch (error) {
console.error("Error loading ajukan ide inovatif:", error);
ajukanIdeInovatifState.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
ajukanIdeInovatifState.delete.loading = true;
const response = await fetch(`/api/inovasi/ajukanideinovatif/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok) {
toast.success(result.message || "Ajukan Ide Inovatif berhasil dihapus");
await ajukanIdeInovatifState.findMany.load();
} else {
toast.error(result?.message || "Gagal menghapus ajukan ide inovatif");
}
} catch (error) {
console.log((error as Error).message);
toast.error("Terjadi kesalahan saat menghapus ajukan ide inovatif");
} finally {
ajukanIdeInovatifState.delete.loading = false;
}
},
},
});
export default ajukanIdeInovatifState;

View File

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

View File

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

View File

@@ -0,0 +1,234 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
const templateForm = z.object({
name: z.string().min(1, "Nama minimal 1 karakter"),
tahun: z.number().min(4, "Tahun minimal 4 karakter"),
slug: z.string().min(1, "Deskripsi singkat minimal 1 karakter"),
deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
kolaborator: z.string().min(1, "Kolaborator minimal 1 karakter"),
imageId: z.string().min(1, "Image ID minimal 1 karakter"),
})
const defaultForm = {
name: "",
tahun: 0,
slug: "",
deskripsi: "",
kolaborator: "",
imageId: "",
}
const kolaborasiInovasiState = proxy({
create: {
form: { ...defaultForm },
loading: false,
async create() {
const cek = templateForm.safeParse(kolaborasiInovasiState.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
kolaborasiInovasiState.create.loading = true;
const res = await ApiFetch.api.inovasi.kolaborasiinovasi["create"].post(
kolaborasiInovasiState.create.form
);
if (res.status === 200) {
kolaborasiInovasiState.findMany.load();
return toast.success("success create");
}
console.log(res);
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
} finally {
kolaborasiInovasiState.create.loading = false;
}
},
},
findMany: {
data: null as any[] | null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
load: async (page = 1, limit = 10) => {
// Change to arrow function
kolaborasiInovasiState.findMany.loading = true; // Use the full path to access the property
kolaborasiInovasiState.findMany.page = page;
try {
const res = await ApiFetch.api.inovasi.kolaborasiinovasi["find-many"].get({
query: { page, limit },
});
if (res.status === 200 && res.data?.success) {
kolaborasiInovasiState.findMany.data = res.data.data || [];
kolaborasiInovasiState.findMany.total = res.data.total || 0;
kolaborasiInovasiState.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error(
"Failed to load grafik berdasarkan jenis kelamin:",
res.data?.message
);
kolaborasiInovasiState.findMany.data = [];
kolaborasiInovasiState.findMany.total = 0;
kolaborasiInovasiState.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading grafik berdasarkan jenis kelamin:", error);
kolaborasiInovasiState.findMany.data = [];
kolaborasiInovasiState.findMany.total = 0;
kolaborasiInovasiState.findMany.totalPages = 1;
} finally {
kolaborasiInovasiState.findMany.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/inovasi/kolaborasiinovasi/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
name: data.name,
tahun: data.tahun,
slug: data.slug,
deskripsi: data.deskripsi,
kolaborator: data.kolaborator,
imageId: data.imageId,
};
return data;
} else {
throw new Error(result?.message || "Gagal mengambil data");
}
} catch (error) {
console.error("Error loading kolaborasi inovasi:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async submit() {
const id = this.id;
if (!id) {
toast.warn("ID tidak valid");
return null;
}
const cek = templateForm.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/inovasi/kolaborasiinovasi/${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 kolaborasiInovasiState.findMany.load();
return result.data;
} catch (error) {
console.error("Error update data:", error);
toast.error("Gagal update data kolaborasi inovasi");
} finally {
this.loading = false;
}
},
},
findUnique: {
data: null as Prisma.KolaborasiInovasiGetPayload<{
include: { image: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/inovasi/kolaborasiinovasi/${id}`);
if (res.ok) {
const data = await res.json();
kolaborasiInovasiState.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
kolaborasiInovasiState.findUnique.data = null;
}
} catch (error) {
console.error("Error loading kolaborasi inovasi:", error);
kolaborasiInovasiState.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
kolaborasiInovasiState.delete.loading = true;
const response = await fetch(`/api/inovasi/kolaborasiinovasi/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Kolaborasi inovasi berhasil dihapus");
await kolaborasiInovasiState.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus kolaborasi inovasi");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus kolaborasi inovasi");
} finally {
kolaborasiInovasiState.delete.loading = false;
}
},
},
});
export default kolaborasiInovasiState;

View File

@@ -0,0 +1,803 @@
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
// ========================================= ADMINISTRASI ONLINE ========================================= //
const templateAdministrasiOnlineForm = z.object({
name: z.string().min(1, "Nama minimal 1 karakter"),
alamat: z.string().min(1, "Alamat minimal 1 karakter"),
nomorTelepon: z.string().min(1, "Nomor telepon minimal 1 karakter"),
jenisLayananId: z.string().min(1, "Jenis layanan minimal 1 karakter"),
});
const defaultAdministrasiOnlineForm = {
name: "",
alamat: "",
nomorTelepon: "",
jenisLayananId: "",
};
const administrasiOnline = proxy({
create: {
form: { ...defaultAdministrasiOnlineForm },
loading: false,
async create() {
const cek = templateAdministrasiOnlineForm.safeParse(
administrasiOnline.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
administrasiOnline.create.loading = true;
const res =
await ApiFetch.api.inovasi.layananonlinedesa.administrasionline[
"create"
].post(administrasiOnline.create.form);
if (res.status === 200) {
administrasiOnline.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 {
administrasiOnline.create.loading = false;
}
},
},
findMany: {
data: null as Array<
Prisma.AdministrasiOnlineGetPayload<{
include: {
jenisLayanan: true;
};
}>
> | null,
page: 1,
totalPages: 1,
loading: false,
async load(page = 1, limit = 10) {
administrasiOnline.findMany.loading = true;
administrasiOnline.findMany.page = page;
try {
const res =
await ApiFetch.api.inovasi.layananonlinedesa.administrasionline[
"find-many"
].get({
query: {
page,
limit,
},
});
if (res.status === 200 && res.data?.success) {
administrasiOnline.findMany.data = res.data.data ?? [];
administrasiOnline.findMany.totalPages = res.data.totalPages ?? 1;
}
} catch (err) {
console.error("Gagal fetch administrasi online paginated:", err);
} finally {
administrasiOnline.findMany.loading = false;
}
},
},
findUnique: {
data: null as Prisma.AdministrasiOnlineGetPayload<{
include: {
jenisLayanan: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(
`/api/inovasi/layananonlinedesa/administrasionline/${id}`
);
if (res.ok) {
const data = await res.json();
administrasiOnline.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch administrasi online:", res.statusText);
administrasiOnline.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching administrasi online:", error);
administrasiOnline.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
administrasiOnline.delete.loading = true;
const response = await fetch(
`/api/inovasi/layananonlinedesa/administrasionline/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(
result.message || "Administrasi online berhasil dihapus"
);
await administrasiOnline.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus administrasi online");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus administrasi online");
} finally {
administrasiOnline.delete.loading = false;
}
},
},
});
// ========================================= JENIS LAYANAN ========================================= //
const templateJenisLayananForm = z.object({
nama: z.string().min(1, "Nama minimal 1 karakter"),
deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
});
const defaultJenisLayananForm = {
nama: "",
deskripsi: "",
};
const jenisLayanan = proxy({
create: {
form: { ...defaultJenisLayananForm },
loading: false,
async create() {
const cek = templateJenisLayananForm.safeParse(jenisLayanan.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
jenisLayanan.create.loading = true;
const res =
await ApiFetch.api.inovasi.layananonlinedesa.administrasionline.jenislayanan[
"create"
].post(jenisLayanan.create.form);
if (res.status === 200) {
jenisLayanan.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 {
jenisLayanan.create.loading = false;
}
},
},
findMany: {
data: null as Array<{
id: string;
nama: string;
deskripsi: string;
}> | null,
async load() {
const res =
await ApiFetch.api.inovasi.layananonlinedesa.administrasionline.jenislayanan[
"find-many"
].get();
if (res.status === 200) {
jenisLayanan.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.JenisLayananGetPayload<{
omit: { isActive: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(
`/api/inovasi/layananonlinedesa/administrasionline/jenislayanan/${id}`
);
if (res.ok) {
const data = await res.json();
jenisLayanan.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
jenisLayanan.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
jenisLayanan.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
jenisLayanan.delete.loading = true;
const response = await fetch(
`/api/inovasi/layananonlinedesa/administrasionline/jenislayanan/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Jenis layanan berhasil dihapus");
await jenisLayanan.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus jenis layanan");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus jenis layanan");
} finally {
jenisLayanan.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...defaultJenisLayananForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(
`/api/inovasi/layananonlinedesa/administrasionline/jenislayanan/${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,
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading jenis layanan:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateJenisLayananForm.safeParse(jenisLayanan.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
jenisLayanan.edit.loading = true;
const response = await fetch(
`/api/inovasi/layananonlinedesa/administrasionline/jenislayanan/${jenisLayanan.edit.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
nama: jenisLayanan.edit.form.nama,
deskripsi: jenisLayanan.edit.form.deskripsi,
}),
}
);
// 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 jenis layanan (${response.status})`
);
}
if (result.success) {
toast.success(
result.message || "Berhasil memperbarui jenis layanan"
);
await jenisLayanan.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal mengupdate jenis layanan");
}
} 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 jenis layanan:", error);
toast.error(
error instanceof Error
? error.message
: "Gagal mengupdate jenis layanan"
);
return false;
} finally {
jenisLayanan.edit.loading = false;
}
},
reset() {
jenisLayanan.edit.id = "";
jenisLayanan.edit.form = { ...defaultJenisLayananForm };
},
},
});
// ========================================= PENGADUAN MASYARAKAT ========================================= //
const templatePengaduanMasyarakatForm = z.object({
name: z.string().min(1, "Nama minimal 1 karakter"),
email: z.string().min(1, "Alamat minimal 1 karakter"),
nomorTelepon: z.string().min(1, "Nomor telepon minimal 1 karakter"),
nik: z.string().min(1, "NIK minimal 1 karakter"),
judulPengaduan: z.string().min(1, "Judul pengaduan minimal 1 karakter"),
lokasiKejadian: z.string().min(1, "Lokasi kejadian minimal 1 karakter"),
deskripsiPengaduan: z.string().min(1, "Deskripsi pengaduan minimal 1 karakter"),
jenisPengaduanId: z.string().min(1, "Jenis pengaduan minimal 1 karakter"),
imageId: z.string().min(1, "Image minimal 1 karakter"),
});
const defaultPengaduanMasyarakatForm = {
name: "",
email: "",
nomorTelepon: "",
nik: "",
judulPengaduan: "",
lokasiKejadian: "",
deskripsiPengaduan: "",
jenisPengaduanId: "",
imageId: "",
};
const pengaduanMasyarakat = proxy({
create: {
form: { ...defaultPengaduanMasyarakatForm },
loading: false,
async create() {
const cek = templatePengaduanMasyarakatForm.safeParse(
pengaduanMasyarakat.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
pengaduanMasyarakat.create.loading = true;
const res =
await ApiFetch.api.inovasi.layananonlinedesa.pengaduanmasyarakat[
"create"
].post(pengaduanMasyarakat.create.form);
if (res.status === 200) {
pengaduanMasyarakat.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 {
pengaduanMasyarakat.create.loading = false;
}
},
},
findMany: {
data: null as Array<
Prisma.PengaduanMasyarakatGetPayload<{
include: {
jenisPengaduan: true;
image: true;
};
}>
> | null,
page: 1,
totalPages: 1,
loading: false,
async load(page = 1, limit = 10) {
pengaduanMasyarakat.findMany.loading = true;
pengaduanMasyarakat.findMany.page = page;
try {
const res =
await ApiFetch.api.inovasi.layananonlinedesa.pengaduanmasyarakat[
"find-many"
].get({
query: {
page,
limit,
},
});
if (res.status === 200 && res.data?.success) {
pengaduanMasyarakat.findMany.data = res.data.data ?? [];
pengaduanMasyarakat.findMany.totalPages = res.data.totalPages ?? 1;
}
} catch (err) {
console.error("Gagal fetch pengaduan masyarakat paginated:", err);
} finally {
pengaduanMasyarakat.findMany.loading = false;
}
},
},
findUnique: {
data: null as Prisma.PengaduanMasyarakatGetPayload<{
include: {
jenisPengaduan: true;
image: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(
`/api/inovasi/layananonlinedesa/pengaduanmasyarakat/${id}`
);
if (res.ok) {
const data = await res.json();
pengaduanMasyarakat.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch pengaduan masyarakat:", res.statusText);
pengaduanMasyarakat.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching pengaduan masyarakat:", error);
pengaduanMasyarakat.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
pengaduanMasyarakat.delete.loading = true;
const response = await fetch(
`/api/inovasi/layananonlinedesa/pengaduanmasyarakat/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(
result.message || "Pengaduan masyarakat berhasil dihapus"
);
await pengaduanMasyarakat.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus pengaduan masyarakat");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus pengaduan masyarakat");
} finally {
pengaduanMasyarakat.delete.loading = false;
}
},
},
});
// ========================================= JENIS PENGADUAN ========================================= //
const templateJenisPengaduanForm = z.object({
nama: z.string().min(1, "Nama minimal 1 karakter"),
});
const defaultJenisPengaduanForm = {
nama: "",
};
const jenisPengaduan = proxy({
create: {
form: { ...defaultJenisPengaduanForm },
loading: false,
async create() {
const cek = templateJenisPengaduanForm.safeParse(jenisPengaduan.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
jenisPengaduan.create.loading = true;
const res =
await ApiFetch.api.inovasi.layananonlinedesa.pengaduanmasyarakat.jenispengaduan[
"create"
].post(jenisPengaduan.create.form);
if (res.status === 200) {
jenisPengaduan.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 {
jenisPengaduan.create.loading = false;
}
},
},
findMany: {
data: null as Array<{
id: string;
nama: string;
}> | null,
async load() {
const res =
await ApiFetch.api.inovasi.layananonlinedesa.pengaduanmasyarakat.jenispengaduan[
"find-many"
].get();
if (res.status === 200) {
jenisPengaduan.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.JenisPengaduanGetPayload<{
omit: { isActive: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(
`/api/inovasi/layananonlinedesa/pengaduanmasyarakat/jenispengaduan/${id}`
);
if (res.ok) {
const data = await res.json();
jenisPengaduan.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
jenisPengaduan.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
jenisPengaduan.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
jenisPengaduan.delete.loading = true;
const response = await fetch(
`/api/inovasi/layananonlinedesa/pengaduanmasyarakat/jenispengaduan/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Jenis pengduan berhasil dihapus");
await jenisPengaduan.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus jenis pengaduan");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus jenis pengaduan");
} finally {
jenisPengaduan.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...defaultJenisPengaduanForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(
`/api/inovasi/layananonlinedesa/pengaduanmasyarakat/jenispengaduan/${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 jenis pengaduan:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateJenisPengaduanForm.safeParse(jenisPengaduan.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
jenisPengaduan.edit.loading = true;
const response = await fetch(
`/api/inovasi/layananonlinedesa/pengaduanmasyarakat/jenispengaduan/${jenisPengaduan.edit.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
nama: jenisPengaduan.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 jenis pengaduan (${response.status})`
);
}
if (result.success) {
toast.success(
result.message || "Berhasil memperbarui jenis pengaduan"
);
await jenisPengaduan.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal mengupdate jenis pengaduan");
}
} 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 jenis pengaduan:", error);
toast.error(
error instanceof Error
? error.message
: "Gagal mengupdate jenis pengaduan"
);
return false;
} finally {
jenisPengaduan.edit.loading = false;
}
},
reset() {
jenisPengaduan.edit.id = "";
jenisPengaduan.edit.form = { ...defaultJenisPengaduanForm };
},
},
});
const layananonlineDesa = proxy({
administrasiOnline,
jenisLayanan,
pengaduanMasyarakat,
jenisPengaduan,
});
export default layananonlineDesa;

View File

@@ -0,0 +1,227 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
const templateForm = z.object({
name: z.string().min(1, "Nama minimal 1 karakter"),
deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
slug: z.string().min(1, "Deskripsi singkat minimal 1 karakter"),
icon: z.string().min(1, "Icon minimal 1 karakter"),
});
const defaultForm = {
name: "",
deskripsi: "",
slug: "",
icon: "",
};
const programKreatifState = proxy({
create: {
form: { ...defaultForm },
loading: false,
async create() {
const cek = templateForm.safeParse(programKreatifState.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
programKreatifState.create.loading = true;
const res = await ApiFetch.api.inovasi.programkreatif["create"].post(
programKreatifState.create.form
);
if (res.status === 200) {
programKreatifState.findMany.load();
return toast.success("success create");
}
console.log(res);
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
} finally {
programKreatifState.create.loading = false;
}
},
},
findMany: {
data: null as any[] | null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
load: async (page = 1, limit = 10) => {
// Change to arrow function
programKreatifState.findMany.loading = true; // Use the full path to access the property
programKreatifState.findMany.page = page;
try {
const res = await ApiFetch.api.inovasi.programkreatif["find-many"].get({
query: { page, limit },
});
if (res.status === 200 && res.data?.success) {
programKreatifState.findMany.data = res.data.data || [];
programKreatifState.findMany.total = res.data.total || 0;
programKreatifState.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error(
"Failed to load grafik berdasarkan jenis kelamin:",
res.data?.message
);
programKreatifState.findMany.data = [];
programKreatifState.findMany.total = 0;
programKreatifState.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading grafik berdasarkan jenis kelamin:", error);
programKreatifState.findMany.data = [];
programKreatifState.findMany.total = 0;
programKreatifState.findMany.totalPages = 1;
} finally {
programKreatifState.findMany.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/inovasi/programkreatif/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
name: data.name,
deskripsi: data.deskripsi,
slug: data.slug,
icon: data.icon,
};
return data;
} else {
throw new Error(result?.message || "Gagal mengambil data");
}
} catch (error) {
console.error("Error loading program kreatif:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async submit() {
const id = this.id;
if (!id) {
toast.warn("ID tidak valid");
return null;
}
const cek = templateForm.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/inovasi/programkreatif/${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 programKreatifState.findMany.load();
return result.data;
} catch (error) {
console.error("Error update data:", error);
toast.error("Gagal update data program kreatif");
} finally {
this.loading = false;
}
},
},
findUnique: {
data: null as Prisma.ProgramKreatifGetPayload<{
omit: { isActive: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/inovasi/programkreatif/${id}`);
if (res.ok) {
const data = await res.json();
programKreatifState.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
programKreatifState.findUnique.data = null;
}
} catch (error) {
console.error("Error loading program kreatif:", error);
programKreatifState.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
programKreatifState.delete.loading = true;
const response = await fetch(`/api/inovasi/programkreatif/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Program kreatif berhasil dihapus");
await programKreatifState.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus program kreatif");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus program kreatif");
} finally {
programKreatifState.delete.loading = false;
}
},
},
});
export default programKreatifState;

View File

@@ -81,7 +81,6 @@ const persentasekelahiran = proxy({
}
},
},
findUnique: {
data: null as Prisma.DataKematian_KelahiranGetPayload<{
omit: { isActive: true };
@@ -176,7 +175,6 @@ const persentasekelahiran = proxy({
},
}
);
const result = await response.json();
if (response.ok && result?.success) {

View File

@@ -0,0 +1,221 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
const templateForm = z.object({
name: z.string().min(1, "Nama minimal 1 karakter"),
icon: z.string().min(1, "Icon minimal 1 karakter"),
});
const defaultForm = {
name: "",
icon: "",
};
const pengelolaanSampahState = proxy({
create: {
form: { ...defaultForm },
loading: false,
async create() {
const cek = templateForm.safeParse(pengelolaanSampahState.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
pengelolaanSampahState.create.loading = true;
const res = await ApiFetch.api.lingkungan.pengelolaansampah["create"].post(
pengelolaanSampahState.create.form
);
if (res.status === 200) {
pengelolaanSampahState.findMany.load();
return toast.success("success create");
}
console.log(res);
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
} finally {
pengelolaanSampahState.create.loading = false;
}
},
},
findMany: {
data: null as any[] | null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
load: async (page = 1, limit = 10) => {
// Change to arrow function
pengelolaanSampahState.findMany.loading = true; // Use the full path to access the property
pengelolaanSampahState.findMany.page = page;
try {
const res = await ApiFetch.api.lingkungan.pengelolaansampah["find-many"].get({
query: { page, limit },
});
if (res.status === 200 && res.data?.success) {
pengelolaanSampahState.findMany.data = res.data.data || [];
pengelolaanSampahState.findMany.total = res.data.total || 0;
pengelolaanSampahState.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error(
"Failed to load pengelolaan sampah:",
res.data?.message
);
pengelolaanSampahState.findMany.data = [];
pengelolaanSampahState.findMany.total = 0;
pengelolaanSampahState.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading pengelolaan sampah:", error);
pengelolaanSampahState.findMany.data = [];
pengelolaanSampahState.findMany.total = 0;
pengelolaanSampahState.findMany.totalPages = 1;
} finally {
pengelolaanSampahState.findMany.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/lingkungan/pengelolaansampah/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
name: data.name,
icon: data.icon,
};
return data;
} else {
throw new Error(result?.message || "Gagal mengambil data");
}
} catch (error) {
console.error("Error loading pengelolaan sampah:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async submit() {
const id = this.id;
if (!id) {
toast.warn("ID tidak valid");
return null;
}
const cek = templateForm.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/lingkungan/pengelolaansampah/${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 pengelolaanSampahState.findMany.load();
return result.data;
} catch (error) {
console.error("Error update data:", error);
toast.error("Gagal update data pengelolaan sampah");
} finally {
this.loading = false;
}
},
},
findUnique: {
data: null as Prisma.ProgramKreatifGetPayload<{
omit: { isActive: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/lingkungan/pengelolaansampah/${id}`);
if (res.ok) {
const data = await res.json();
pengelolaanSampahState.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
pengelolaanSampahState.findUnique.data = null;
}
} catch (error) {
console.error("Error loading pengelolaan sampah:", error);
pengelolaanSampahState.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
pengelolaanSampahState.delete.loading = true;
const response = await fetch(`/api/lingkungan/pengelolaansampah/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "pengelolaan sampah berhasil dihapus");
await pengelolaanSampahState.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus pengelolaan sampah");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus pengelolaan sampah");
} finally {
pengelolaanSampahState.delete.loading = false;
}
},
},
});
export default pengelolaanSampahState;

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
@@ -16,17 +17,9 @@ const defaultForm = {
tanggal: "",
};
type DaftarInformasi = Prisma.DaftarInformasiPublikGetPayload<{
select: {
jenisInformasi: true;
deskripsi: true;
tanggal: true;
};
}>;
const daftarInformasiPublik = proxy({
create: {
form: {} as DaftarInformasi,
form: {...defaultForm},
loading: false,
async create() {
const cek = templateDaftarInformasi.safeParse(
@@ -56,15 +49,38 @@ const daftarInformasiPublik = proxy({
},
},
findMany: {
data: null as
| Prisma.DaftarInformasiPublikGetPayload<{ omit: { isActive: true } }>[]
| null,
async load() {
const res = await ApiFetch.api.ppid.daftarinformasipublik[
"find-many"
].get();
if (res.status === 200) {
daftarInformasiPublik.findMany.data = res.data?.data ?? [];
data: null as any[] | null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
load: async (page = 1, limit = 10) => { // Change to arrow function
daftarInformasiPublik.findMany.loading = true; // Use the full path to access the property
daftarInformasiPublik.findMany.page = page;
try {
const res = await ApiFetch.api.ppid.daftarinformasipublik[
"find-many"
].get({
query: { page, limit },
});
if (res.status === 200 && res.data?.success) {
daftarInformasiPublik.findMany.data = res.data.data || [];
daftarInformasiPublik.findMany.total = res.data.total || 0;
daftarInformasiPublik.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error("Failed to load daftar informasi publik:", res.data?.message);
daftarInformasiPublik.findMany.data = [];
daftarInformasiPublik.findMany.total = 0;
daftarInformasiPublik.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading daftar informasi publik:", error);
daftarInformasiPublik.findMany.data = [];
daftarInformasiPublik.findMany.total = 0;
daftarInformasiPublik.findMany.totalPages = 1;
} finally {
daftarInformasiPublik.findMany.loading = false;
}
},
},
@@ -186,7 +202,9 @@ const daftarInformasiPublik = proxy({
}
try {
daftarInformasiPublik.edit.loading = true;
const formattedTanggal = this.form.tanggal
? new Date(this.form.tanggal).toISOString()
: undefined;
const response = await fetch(
`/api/ppid/daftarinformasipublik/${this.id}`,
{
@@ -197,7 +215,7 @@ const daftarInformasiPublik = proxy({
body: JSON.stringify({
jenisInformasi: this.form.jenisInformasi,
deskripsi: this.form.deskripsi,
tanggal: this.form.tanggal,
tanggal: formattedTanggal,
}),
}
);

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
@@ -9,71 +10,75 @@ const templateGrafikJenisKelamin = z.object({
perempuan: z.string().min(1, "Data perempuan harus diisi"),
});
type GrafikJenisKelamin = Prisma.GrafikBerdasarkanJenisKelaminGetPayload<{
select: {
id: true;
laki: true;
perempuan: true;
};
}>;
const defaultForm: Omit<GrafikJenisKelamin, 'id'> & { id?: string } = {
const defaultForm = {
laki: "",
perempuan: "",
};
const grafikBerdasarkanJenisKelamin = proxy({
create: {
form: defaultForm,
form: {...defaultForm},
loading: false,
async create() {
const cek = templateGrafikJenisKelamin.safeParse(
grafikBerdasarkanJenisKelamin.create.form
);
async create(){
const cek = templateGrafikJenisKelamin.safeParse(grafikBerdasarkanJenisKelamin.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
const err = cek.error.issues.map((i) => i.message).join("\n");
toast.error(err);
return;
}
try {
grafikBerdasarkanJenisKelamin.create.loading = true;
const res = await ApiFetch.api.ppid.grafikberdasarkanjeniskelamin[
"create"
].post(grafikBerdasarkanJenisKelamin.create.form);
if (res.status === 200) {
const id = res.data?.data?.id;
if (id) {
toast.success("Success create");
grafikBerdasarkanJenisKelamin.create.form = {
laki: "",
perempuan: "",
};
grafikBerdasarkanJenisKelamin.findMany.load();
return id;
}
toast.success("Grafik berdasarkan jenis kelamin berhasil ditambahkan");
await grafikBerdasarkanJenisKelamin.findMany.load();
} else {
toast.error(res.data?.message ?? "Gagal tambah grafik berdasarkan jenis kelamin");
}
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
console.error("Gagal create:", error);
toast.error("Terjadi kesalahan saat menambahkan grafik berdasarkan jenis kelamin");
} finally {
grafikBerdasarkanJenisKelamin.create.loading = false;
}
},
},
findMany: {
data: null as
| Prisma.GrafikBerdasarkanJenisKelaminGetPayload<{
omit: { isActive: true };
}>[]
| null,
data: null as any[] | null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
async load() {
const res = await ApiFetch.api.ppid.grafikberdasarkanjeniskelamin[
"find-many"
].get();
if (res.status === 200) {
grafikBerdasarkanJenisKelamin.findMany.data = res.data?.data ?? [];
load: async (page = 1, limit = 10) => { // Change to arrow function
grafikBerdasarkanJenisKelamin.findMany.loading = true; // Use the full path to access the property
grafikBerdasarkanJenisKelamin.findMany.page = page;
try {
const res = await ApiFetch.api.ppid.grafikberdasarkanjeniskelamin[
"find-many"
].get({
query: { page, limit },
});
if (res.status === 200 && res.data?.success) {
grafikBerdasarkanJenisKelamin.findMany.data = res.data.data || [];
grafikBerdasarkanJenisKelamin.findMany.total = res.data.total || 0;
grafikBerdasarkanJenisKelamin.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error("Failed to load grafik berdasarkan jenis kelamin:", res.data?.message);
grafikBerdasarkanJenisKelamin.findMany.data = [];
grafikBerdasarkanJenisKelamin.findMany.total = 0;
grafikBerdasarkanJenisKelamin.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading grafik berdasarkan jenis kelamin:", error);
grafikBerdasarkanJenisKelamin.findMany.data = [];
grafikBerdasarkanJenisKelamin.findMany.total = 0;
grafikBerdasarkanJenisKelamin.findMany.totalPages = 1;
} finally {
grafikBerdasarkanJenisKelamin.findMany.loading = false;
}
},
},

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
@@ -11,17 +12,7 @@ const templateGrafikResponden = z.object({
tidakbaik: z.string().min(1, "Data tidak baik harus diisi"),
});
type GrafikResponden = Prisma.GrafikBerdasarkanRespondenGetPayload<{
select: {
id: true;
sangatbaik: true;
baik: true;
kurangbaik: true;
tidakbaik: true;
};
}>;
const defaultForm: Omit<GrafikResponden, 'id'> & { id?: string } = {
const defaultForm = {
sangatbaik: "",
baik: "",
kurangbaik: "",
@@ -30,7 +21,7 @@ const defaultForm: Omit<GrafikResponden, 'id'> & { id?: string } = {
const grafikBerdasarkanResponden = proxy({
create: {
form: defaultForm,
form: {...defaultForm},
loading: false,
async create() {
const cek = templateGrafikResponden.safeParse(
@@ -48,40 +39,52 @@ const grafikBerdasarkanResponden = proxy({
"create"
].post(grafikBerdasarkanResponden.create.form);
if (res.status === 200) {
const id = res.data?.data?.id;
if (id) {
toast.success("Success create");
grafikBerdasarkanResponden.create.form = {
sangatbaik: "",
baik: "",
kurangbaik: "",
tidakbaik: "",
};
grafikBerdasarkanResponden.findMany.load();
return id;
}
toast.success("Grafik berdasarkan responden berhasil ditambahkan");
await grafikBerdasarkanResponden.findMany.load();
} else {
toast.error(res.data?.message ?? "Gagal tambah grafik berdasarkan responden");
}
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
console.error("Gagal create:", error);
toast.error("Terjadi kesalahan saat menambahkan grafik berdasarkan responden");
} finally {
grafikBerdasarkanResponden.create.loading = false;
}
},
},
findMany: {
data: null as
| Prisma.GrafikBerdasarkanRespondenGetPayload<{
omit: { isActive: true };
}>[]
| null,
data: null as any[] | null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
async load() {
const res = await ApiFetch.api.ppid.grafikberdasarkanresponden[
"find-many"
].get();
if (res.status === 200) {
grafikBerdasarkanResponden.findMany.data = res.data?.data ?? [];
load: async (page = 1, limit = 10) => { // Change to arrow function
grafikBerdasarkanResponden.findMany.loading = true; // Use the full path to access the property
grafikBerdasarkanResponden.findMany.page = page;
try {
const res = await ApiFetch.api.ppid.grafikberdasarkanresponden[
"find-many"
].get({
query: { page, limit },
});
if (res.status === 200 && res.data?.success) {
grafikBerdasarkanResponden.findMany.data = res.data.data || [];
grafikBerdasarkanResponden.findMany.total = res.data.total || 0;
grafikBerdasarkanResponden.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error("Failed to load grafik berdasarkan responden:", res.data?.message);
grafikBerdasarkanResponden.findMany.data = [];
grafikBerdasarkanResponden.findMany.total = 0;
grafikBerdasarkanResponden.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading grafikBerdasarkanResponden:", error);
grafikBerdasarkanResponden.findMany.data = [];
grafikBerdasarkanResponden.findMany.total = 0;
grafikBerdasarkanResponden.findMany.totalPages = 1;
} finally {
grafikBerdasarkanResponden.findMany.loading = false;
}
},
},

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
@@ -11,17 +12,7 @@ const templateGrafikUmur = z.object({
lansia: z.string().min(1, "Data lansia harus diisi"),
});
type GrafikUmur = Prisma.GrafikBerdasarkanUmurGetPayload<{
select: {
id: true;
remaja: true;
dewasa: true;
orangtua: true;
lansia: true;
};
}>;
const defaultForm: Omit<GrafikUmur, "id"> & { id?: string } = {
const defaultForm = {
remaja: "",
dewasa: "",
orangtua: "",
@@ -30,7 +21,7 @@ const defaultForm: Omit<GrafikUmur, "id"> & { id?: string } = {
const grafikBerdasarkanUmur = proxy({
create: {
form: defaultForm,
form: {...defaultForm},
loading: false,
async create() {
const cek = templateGrafikUmur.safeParse(
@@ -70,18 +61,38 @@ const grafikBerdasarkanUmur = proxy({
},
},
findMany: {
data: null as
| Prisma.GrafikBerdasarkanUmurGetPayload<{
omit: { isActive: true };
}>[]
| null,
data: null as any[] | null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
async load() {
const res = await ApiFetch.api.ppid.grafikberdasarkanumur[
"find-many"
].get();
if (res.status === 200) {
grafikBerdasarkanUmur.findMany.data = res.data?.data ?? [];
load: async (page = 1, limit = 10) => { // Change to arrow function
grafikBerdasarkanUmur.findMany.loading = true; // Use the full path to access the property
grafikBerdasarkanUmur.findMany.page = page;
try {
const res = await ApiFetch.api.ppid.grafikberdasarkanumur[
"find-many"
].get({
query: { page, limit },
});
if (res.status === 200 && res.data?.success) {
grafikBerdasarkanUmur.findMany.data = res.data.data || [];
grafikBerdasarkanUmur.findMany.total = res.data.total || 0;
grafikBerdasarkanUmur.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error("Failed to load grafik berdasarkan umur:", res.data?.message);
grafikBerdasarkanUmur.findMany.data = [];
grafikBerdasarkanUmur.findMany.total = 0;
grafikBerdasarkanUmur.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading grafik berdasarkan umur:", error);
grafikBerdasarkanUmur.findMany.data = [];
grafikBerdasarkanUmur.findMany.total = 0;
grafikBerdasarkanUmur.findMany.totalPages = 1;
} finally {
grafikBerdasarkanUmur.findMany.loading = false;
}
},
},

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
@@ -9,22 +10,14 @@ const templateGrafikHasilKepuasanMasyarakat = z.object({
kepuasan: z.string().min(1, "Kepuasan harus diisi"),
});
type GrafikHasilKepuasanMasyarakat = Prisma.IndeksKepuasanMasyarakatGetPayload<{
select: {
id: true;
label: true;
kepuasan: true;
};
}>;
const defaultForm: Omit<GrafikHasilKepuasanMasyarakat, 'id'> & { id?: string } = {
const defaultForm = {
label: "",
kepuasan: "",
};
const grafikHasilKepuasanMasyarakat = proxy({
create: {
form: defaultForm,
form: {...defaultForm},
loading: false,
async create() {
const cek = templateGrafikHasilKepuasanMasyarakat.safeParse(
@@ -38,42 +31,52 @@ const grafikHasilKepuasanMasyarakat = proxy({
}
try {
grafikHasilKepuasanMasyarakat.create.loading = true;
const res = await ApiFetch.api.ppid.grafikhasilkepuasamanmasyarakat["create"].post(
grafikHasilKepuasanMasyarakat.create.form
);
const res = await ApiFetch.api.ppid.grafikhasilkepuasamanmasyarakat["create"].post(grafikHasilKepuasanMasyarakat.create.form);
if (res.status === 200) {
const id = res.data?.data?.id;
if (id) {
toast.success("Success create");
grafikHasilKepuasanMasyarakat.create.form = {
label: "",
kepuasan: "",
};
grafikHasilKepuasanMasyarakat.findMany.load();
return id;
}
toast.success("Grafik hasil kepuasan masyarakat berhasil ditambahkan");
await grafikHasilKepuasanMasyarakat.findMany.load();
} else {
toast.error(res.data?.message ?? "Gagal tambah grafik hasil kepuasan masyarakat");
}
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
console.error("Gagal create:", error);
toast.error("Terjadi kesalahan saat menambahkan grafik hasil kepuasan masyarakat");
} finally {
grafikHasilKepuasanMasyarakat.create.loading = false;
}
},
},
findMany: {
data: null as
| Prisma.IndeksKepuasanMasyarakatGetPayload<{
omit: { isActive: true };
}>[]
| null,
async load() {
const res = await ApiFetch.api.ppid.grafikhasilkepuasamanmasyarakat[
"find-many"
].get();
if (res.status === 200) {
grafikHasilKepuasanMasyarakat.findMany.data = res.data?.data ?? [];
data: null as any[] | null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
load: async (page = 1, limit = 10) => { // Change to arrow function
grafikHasilKepuasanMasyarakat.findMany.loading = true; // Use the full path to access the property
grafikHasilKepuasanMasyarakat.findMany.page = page;
try {
const res = await ApiFetch.api.ppid.grafikhasilkepuasamanmasyarakat["find-many"].get({
query: { page, limit },
});
if (res.status === 200 && res.data?.success) {
grafikHasilKepuasanMasyarakat.findMany.data = res.data.data || [];
grafikHasilKepuasanMasyarakat.findMany.total = res.data.total || 0;
grafikHasilKepuasanMasyarakat.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error("Failed to load grafik hasil kepuasan masyarakat:", res.data?.message);
grafikHasilKepuasanMasyarakat.findMany.data = [];
grafikHasilKepuasanMasyarakat.findMany.total = 0;
grafikHasilKepuasanMasyarakat.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading grafik hasil kepuasan masyarakat:", error);
grafikHasilKepuasanMasyarakat.findMany.data = [];
grafikHasilKepuasanMasyarakat.findMany.total = 0;
grafikHasilKepuasanMasyarakat.findMany.totalPages = 1;
} finally {
grafikHasilKepuasanMasyarakat.findMany.loading = false;
}
},
},

View File

@@ -18,8 +18,6 @@ import { useParams, useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { toast } from "react-toastify";
import { useProxy } from "valtio/utils";
import EditEditor from "@/app/admin/(dashboard)/_com/editEditor";
import colors from "@/con/colors";
import ApiFetch from "@/lib/api-fetch";

View File

@@ -1,6 +1,6 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Grid, GridCol, Image, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { Box, Button, Center, Grid, GridCol, Image, Pagination, 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';
@@ -40,8 +40,8 @@ function ListBerita({ search }: { search: string }) {
// Fetch pertama kali
useShallowEffect(() => {
load(page); // awal page = 1
}, []);
load(page, 10); // awal page = 1
}, [page]);
const filteredData = (data || []).filter((item) => {
const keyword = search.toLowerCase();
@@ -57,14 +57,6 @@ function ListBerita({ search }: { search: string }) {
return (
<Box py={10}>
<Pagination
value={page}
onChange={(newPage) => load(newPage)} // ini penting!
total={totalPages}
mt="md"
mb="md"
/>
<Paper bg={colors["white-1"]} p={"md"}>
<Stack>
<Grid>
@@ -128,6 +120,15 @@ function ListBerita({ search }: { search: string }) {
</Box>
</Stack>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)} // ini penting!
total={totalPages}
mt="md"
mb="md"
/>
</Center>
</Box>
);
}

View File

@@ -0,0 +1,108 @@
'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, 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 ajukanIdeInovatifState from '../../../_state/inovasi/ajukan-ide-inovatif';
function DetailAjukanIdeInofativDesa() {
const state = useProxy(ajukanIdeInovatifState)
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const router = useRouter()
const params = useParams()
useShallowEffect(() => {
state.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
state.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/inovasi/ajukan-ide-inovatif")
}
}
if (!state.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack>
<Flex justify="space-between" gap={"xs"}>
<Text fz={"xl"} fw={"bold"}>Detail Ajukan Ide Inovatif Desa</Text>
<Button
onClick={() => {
if (state.findUnique.data) {
setSelectedId(state.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={state.delete.loading || !state.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
</Flex>
{state.findUnique.data ? (
<Paper key={state.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Nama</Text>
<Text fz={"lg"}>{state.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Alamat</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: state.findUnique.data?.alamat }} />
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Nama Ide Inovatif</Text>
<Text fz={"lg"}>{state.findUnique.data?.namaIde}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: state.findUnique.data?.deskripsi }} />
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Masalah</Text>
<Text fz={"lg"}>{state.findUnique.data?.masalah}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Benefit</Text>
<Text fz={"lg"}>{state.findUnique.data?.benefit}</Text>
</Box>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus ajukan ide inovatif ini?'
/>
</Box>
);
}
export default DetailAjukanIdeInofativDesa;

View File

@@ -1,59 +1,91 @@
'use client'
import colors from '@/con/colors';
import { Box, Paper, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Title } from '@mantine/core';
import { IconSearch } from '@tabler/icons-react';
import React from 'react';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import ajukanIdeInovatifState from '../../_state/inovasi/ajukan-ide-inovatif';
function AjukanIdeInofativ() {
function AjukanIdeInovatif() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Ajukan Ide Inovatif'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListAjukanIdeInovatif />
<ListAjukanIdeInovatif search={search} />
</Box>
);
}
function ListAjukanIdeInovatif() {
function ListAjukanIdeInovatif({ search }: { search: string }) {
const state = useProxy(ajukanIdeInovatifState)
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) ||
item.alamat.toLowerCase().includes(keyword) ||
item.namaIde.toLowerCase().includes(keyword) ||
item.masalah.toLowerCase().includes(keyword) ||
item.benefit.toLowerCase().includes(keyword)
);
});
if (!state.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>List Ajukan Ide Inovatif</Title>
<Box>
<Table striped withRowBorders withColumnBorders withTableBorder>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Nama</TableTh>
<TableTh>Alamat</TableTh>
<TableTh>Nama Ide Inovatif</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Masalah yang ingin diatasi</TableTh>
<TableTh>Benefit</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>1</TableTd>
<TableTd>nama</TableTd>
<TableTd>alamat</TableTd>
<TableTd>ide inovatif</TableTd>
<TableTd>deskripsi</TableTd>
<TableTd>masalah</TableTd>
<TableTd>benefit</TableTd>
</TableTr>
</TableTbody>
</Table> </Box>
</Stack>
<Title mb={10} order={3}>List Ajukan Ide Inovatif</Title>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Alamat</TableTh>
<TableTh>Nama Ide Inovatif</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.alamat }} />
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.namaIde }} />
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/inovasi/ajukan-ide-inovatif/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>
</Box>
)
);
}
export default AjukanIdeInofativ;
export default AjukanIdeInovatif;

View File

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

View File

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

View File

@@ -1,48 +1,115 @@
'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 ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { KeamananEditor } from '../../../keamanan/_com/keamananEditor';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor';
import desaDigitalState from '../../../_state/inovasi/desa-digital';
function CreateDesaDigital() {
const router = useRouter();
const stateDesaDigital = useProxy(desaDigitalState)
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const router = useRouter()
const resetForm = () => {
stateDesaDigital.create.form = {
name: "",
deskripsi: "",
imageId: "",
}
setPreviewImage(null)
setFile(null)
}
const handleSubmit = async () => {
if (!file) {
return toast.error("Silahkan pilih file gambar terlebih dahulu")
}
try {
// Upload the image first
const uploadRes = await ApiFetch.api.fileStorage.create.post({
file: file,
name: file.name
})
const uploaded = uploadRes.data?.data
if (!uploaded?.id) {
return toast.error("Gagal upload gambar")
}
// Set the image ID in the form
stateDesaDigital.create.form.imageId = uploaded.id
// Submit the form
const success = await stateDesaDigital.create.create()
if (success) {
resetForm()
router.push("/admin/inovasi/desa-digital-smart-village")
}
} catch (error) {
console.error("Error in handleSubmit:", error)
toast.error("Terjadi kesalahan saat menyimpan data")
}
}
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 color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{base: '100%', md: '50%'}} bg={colors['white-1']} p={'md'}>
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={4}>Create Desa Digital Smart Village</Title>
<Title order={3}>Create Desa Digital Smart Village</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Inovasi</Text>}
placeholder='Masukkan nama inovasi'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Deskripsi Singkat Inovasi</Text>}
placeholder='Masukkan deskripsi singkat inovasi'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Image</Text>}
placeholder='Masukkan image'
value={stateDesaDigital.create.form.name}
onChange={(val) => {
stateDesaDigital.create.form.name = val.target.value;
}}
label={<Text fz={"sm"} fw={"bold"}>Nama Desa Digital Smart Village</Text>}
placeholder="masukkan nama desa digital smart village"
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Inovasi</Text>
<KeamananEditor
showSubmit={false}
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
<CreateEditor
value={stateDesaDigital.create.form.deskripsi}
onChange={(htmlContent) => {
stateDesaDigital.create.form.deskripsi = htmlContent;
}}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
<FileInput
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar Konten</Text>}
value={file}
onChange={async (e) => {
if (!e) return;
setFile(e);
const base64 = await e.arrayBuffer().then((buf) =>
"data:image/png;base64," + Buffer.from(buf).toString("base64")
);
setPreviewImage(base64);
}}
/>
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg={"gray"}>
<IconImageInPicture />
</Center>
)}
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
</Stack>
</Paper>
</Box>
</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, IconImageInPicture, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailDesaDigital() {
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 Desa Digital Smart Village</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"}>Nama Inovasi</Text>
<Text>Pelayanan Admin Digital</Text>
</Box>
<Box>
<Text fw={"bold"}>Deskripsi Singkat Inovasi</Text>
<Text>Deskripsi Singkat Inovasi</Text>
</Box>
<Box>
<Text fw={"bold"}>Image</Text>
<IconImageInPicture size={20} />
</Box>
<Box>
<Text fw={"bold"}>Deskripsi Inovasi</Text>
<Text>Deskripsi Inovasi</Text>
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red">
<IconX size={20} />
</Button>
<Button onClick={() => router.push('/admin/inovasi/desa-digital-smart-village/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 DetailDesaDigital;

View File

@@ -1,49 +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 EditDesaDigital() {
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 Desa Digital Smart Village</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Inovasi</Text>}
placeholder='Masukkan nama inovasi'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Deskripsi Singkat Inovasi</Text>}
placeholder='Masukkan deskripsi singkat inovasi'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Image</Text>}
placeholder='Masukkan image'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Inovasi</Text>
<KeamananEditor
showSubmit={false}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditDesaDigital;

View File

@@ -1,26 +1,53 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Image, Paper, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import { useRouter } from 'next/navigation';
import desaDigitalState from '../../_state/inovasi/desa-digital';
function DesaDigitalSmartVillage() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Desa Digital Smart Village'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListDesaDigitalSmartVillage/>
<ListDesaDigitalSmartVillage search={search} />
</Box>
);
}
function ListDesaDigitalSmartVillage() {
const router = useRouter();
function ListDesaDigitalSmartVillage({ search }: { search: string }) {
const state = useProxy(desaDigitalState)
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}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
@@ -33,25 +60,25 @@ function ListDesaDigitalSmartVillage() {
<TableTr>
<TableTh>Nama Inovasi</TableTh>
<TableTh>Deskripsi Singkat Inovasi</TableTh>
<TableTh>Image</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>Layanan Admin Digital</TableTd>
<TableTd>Deskripsi Singkat Inovasi</TableTd>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd>
<TableTd>
<Image src={"/"} alt=''/>
</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/inovasi/desa-digital-smart-village/detail')}>
<Button onClick={() => router.push(`/admin/inovasi/desa-digital-smart-village/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Table>
</Paper>
</Box>
);

View File

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

View File

@@ -0,0 +1,107 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import infoTeknoState from '../../../_state/inovasi/info-tekno';
function DetailInfoTeknologiTepatGuna() {
const stateInfoTekno = useProxy(infoTeknoState)
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const router = useRouter()
const params = useParams()
useShallowEffect(() => {
stateInfoTekno.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
stateInfoTekno.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/inovasi/info-teknologi-tepat-guna")
}
}
if (!stateInfoTekno.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Info Teknologi Tepat Guna</Text>
{stateInfoTekno.findUnique.data ? (
<Paper key={stateInfoTekno.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Judul</Text>
<Text fz={"lg"}>{stateInfoTekno.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: stateInfoTekno.findUnique.data?.deskripsi }} />
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
<Image w={{ base: 150, md: 150, lg: 150 }} src={stateInfoTekno.findUnique.data?.image?.link} alt="gambar" />
</Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (stateInfoTekno.findUnique.data) {
setSelectedId(stateInfoTekno.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={stateInfoTekno.delete.loading || !stateInfoTekno.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (stateInfoTekno.findUnique.data) {
router.push(`/admin/inovasi/info-teknologi-tepat-guna/${stateInfoTekno.findUnique.data.id}/edit`);
}
}}
disabled={!stateInfoTekno.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 info teknologi tepat guna ini?'
/>
</Box>
);
}
export default DetailInfoTeknologiTepatGuna;

View File

@@ -1,44 +1,115 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { KeamananEditor } from '../../../keamanan/_com/keamananEditor';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor';
import infoTeknoState from '../../../_state/inovasi/info-tekno';
function CreateInfoTeknologiTepatGuna() {
const router = useRouter();
const stateInfoTekno = useProxy(infoTeknoState)
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const router = useRouter()
const resetForm = () => {
stateInfoTekno.create.form = {
name: "",
deskripsi: "",
imageId: "",
}
setPreviewImage(null)
setFile(null)
}
const handleSubmit = async () => {
if (!file) {
return toast.error("Silahkan pilih file gambar terlebih dahulu")
}
try {
// Upload the image first
const uploadRes = await ApiFetch.api.fileStorage.create.post({
file: file,
name: file.name
})
const uploaded = uploadRes.data?.data
if (!uploaded?.id) {
return toast.error("Gagal upload gambar")
}
// Set the image ID in the form
stateInfoTekno.create.form.imageId = uploaded.id
// Submit the form
const success = await stateInfoTekno.create.create()
if (success) {
resetForm()
router.push("/admin/inovasi/info-teknologi-tepat-guna")
}
} catch (error) {
console.error("Error in handleSubmit:", error)
toast.error("Terjadi kesalahan saat menyimpan data")
}
}
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 color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{base: '100%', md: '50%'}} bg={colors['white-1']} p={'md'}>
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={4}>Create Info Teknologi Tepat Guna</Title>
<Box>
<Text fw={"bold"} fz={"sm"}>Masukkan Image</Text>
<IconImageInPicture size={50} />
</Box>
<Title order={3}>Create Info Teknologi Tepat Guna</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Info Teknologi Tepat Guna</Text>}
placeholder='Masukkan nama info teknologi tepat guna'
value={stateInfoTekno.create.form.name}
onChange={(val) => {
stateInfoTekno.create.form.name = val.target.value;
}}
label={<Text fz={"sm"} fw={"bold"}>Nama Info Teknologi Tepat Guna</Text>}
placeholder="masukkan nama info teknologi tepat guna"
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Info Teknologi Tepat Guna</Text>
<KeamananEditor
showSubmit={false}
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
<CreateEditor
value={stateInfoTekno.create.form.deskripsi}
onChange={(htmlContent) => {
stateInfoTekno.create.form.deskripsi = htmlContent;
}}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
<FileInput
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar Konten</Text>}
value={file}
onChange={async (e) => {
if (!e) return;
setFile(e);
const base64 = await e.arrayBuffer().then((buf) =>
"data:image/png;base64," + Buffer.from(buf).toString("base64")
);
setPreviewImage(base64);
}}
/>
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg={"gray"}>
<IconImageInPicture />
</Center>
)}
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
</Stack>
</Paper>
</Box>
</Box>
);
}

View File

@@ -1,62 +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 DetailInfoTeknologiTepatGuna() {
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 Info Teknologi Tepat Guna</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Info Teknologi Tepat Guna</Text>
<Text fz={"lg"}>Test Judul</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 Deskripsi</Text>
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red">
<IconX size={20} />
</Button>
<Button onClick={() => router.push('/admin/inovasi/info-teknologi-tepat-guna/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 DetailInfoTeknologiTepatGuna;

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, IconImageInPicture } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { KeamananEditor } from '../../../keamanan/_com/keamananEditor';
function EditInfoTeknologiTepatGuna() {
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 Info Teknologi Tepat Guna</Title>
<Box>
<Text fw={"bold"} fz={"sm"}>Masukkan Image</Text>
<IconImageInPicture size={50} />
</Box>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Info Teknologi Tepat Guna</Text>}
placeholder='Masukkan nama info teknologi tepat guna'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Info Teknologi Tepat Guna</Text>
<KeamananEditor
showSubmit={false}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditInfoTeknologiTepatGuna;

View File

@@ -1,26 +1,53 @@
'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 } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import { useRouter } from 'next/navigation';
import infoTeknoState from '../../_state/inovasi/info-tekno';
function InfoTeknologiTepatGuna() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Info Teknologi Tepat Guna'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListInfoTeknologiTepatGuna/>
<ListInfoTeknologiTepatGuna search={search} />
</Box>
);
}
function ListInfoTeknologiTepatGuna() {
const router = useRouter();
function ListInfoTeknologiTepatGuna({ search }: { search: string }) {
const state = useProxy(infoTeknoState)
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}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
@@ -32,24 +59,26 @@ function ListInfoTeknologiTepatGuna() {
<TableThead>
<TableTr>
<TableTh>Nama Info Teknologi Tepat Guna</TableTh>
<TableTh>Image</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Deskripsi Singkat Info Teknologi Tepat Guna</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>Info Teknologi Tepat Guna 1</TableTd>
<TableTd>Image</TableTd>
<TableTd>Deskripsi</TableTd>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/inovasi/info-teknologi-tepat-guna/detail')}>
<Button onClick={() => router.push(`/admin/inovasi/info-teknologi-tepat-guna/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Table>
</Paper>
</Box>
);

View File

@@ -0,0 +1,186 @@
/* eslint-disable react-hooks/exhaustive-deps */
"use client";
import EditEditor from "@/app/admin/(dashboard)/_com/editEditor";
import colors from "@/con/colors";
import ApiFetch from "@/lib/api-fetch";
import {
Box,
Button,
Center,
FileInput,
Image,
Paper,
Stack,
Text,
TextInput,
Title
} from "@mantine/core";
import { IconArrowBack, IconImageInPicture } from "@tabler/icons-react";
import { useParams, useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { toast } from "react-toastify";
import { useProxy } from "valtio/utils";
import kolaborasiInovasiState from "@/app/admin/(dashboard)/_state/inovasi/kolaborasi-inovasi";
function EditKolaborasiInovasi() {
const kolaborasiState = useProxy(kolaborasiInovasiState);
const router = useRouter();
const params = useParams();
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({
name: kolaborasiState.update.form.name || '',
deskripsi: kolaborasiState.update.form.deskripsi || '',
tahun: kolaborasiState.update.form.tahun || '',
slug: kolaborasiState.update.form.slug || '',
kolaborator: kolaborasiState.update.form.kolaborator || '',
imageId: kolaborasiState.update.form.imageId || ''
});
// Load berita by id saat pertama kali
useEffect(() => {
const loadKolaborasi = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await kolaborasiState.update.load(id); // akses langsung, bukan dari proxy
if (data) {
setFormData({
name: data.name || '',
deskripsi: data.deskripsi || '',
tahun: data.tahun || '',
slug: data.slug || '',
kolaborator: data.kolaborator || '',
imageId: data.imageId || '',
});
if (data.image) {
if (data?.image?.link) {
setPreviewImage(data.image.link);
}
}
}
} catch (error) {
console.error("Error loading berita:", error);
toast.error("Gagal memuat data berita");
}
};
loadKolaborasi();
}, [params?.id]);
const handleSubmit = async () => {
try {
// Update global state with form data
kolaborasiState.update.form = {
...kolaborasiState.update.form,
name: formData.name,
deskripsi: formData.deskripsi,
tahun: Number(formData.tahun),
slug: formData.slug,
kolaborator: formData.kolaborator,
imageId: formData.imageId // Keep existing imageId if not changed
};
// Jika ada file baru, upload
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
// Update imageId in global state
kolaborasiState.update.form.imageId = uploaded.id;
}
await kolaborasiState.update.submit();
toast.success("Berita berhasil diperbarui!");
router.push("/admin/inovasi/kolaborasi-inovasi");
} catch (error) {
console.error("Error updating berita:", error);
toast.error("Terjadi kesalahan saat memperbarui berita");
}
};
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors["blue-button"]} size={30} />
</Button>
</Box>
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Edit Kolaborasi Inovasi</Title>
<TextInput
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Nama</Text>}
placeholder="masukkan nama"
/>
<TextInput
value={formData.slug}
onChange={(e) => setFormData({ ...formData, slug: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Deskripsi Singkat</Text>}
placeholder="masukkan deskripsi singkat"
/>
<TextInput
value={formData.tahun}
onChange={(e) => setFormData({ ...formData, tahun: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Tahun</Text>}
placeholder="masukkan tahun"
/>
<TextInput
value={formData.kolaborator}
onChange={(e) => setFormData({ ...formData, kolaborator: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Kolaborator</Text>}
placeholder="masukkan kolaborator"
/>
<FileInput
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar Baru (Opsional)</Text>}
value={file}
onChange={async (e) => {
if (!e) return;
setFile(e);
const base64 = await e.arrayBuffer().then((buf) =>
"data:image/png;base64," + Buffer.from(buf).toString("base64")
);
setPreviewImage(base64);
}}
/>
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg={"gray"}>
<IconImageInPicture />
</Center>
)}
<Box>
<Text fz={"sm"} fw={"bold"}>Konten</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }));
kolaborasiState.update.form.deskripsi = htmlContent;
}}
/>
</Box>
<Button onClick={handleSubmit}>Simpan</Button>
</Stack>
</Paper>
</Box>
);
}
export default EditKolaborasiInovasi;

View File

@@ -0,0 +1,122 @@
'use client'
import { useProxy } from 'valtio/utils';
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import colors from '@/con/colors';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import kolaborasiInovasiState from '../../../_state/inovasi/kolaborasi-inovasi';
function DetailKolaborasiInovasi() {
const kolaborasiState = useProxy(kolaborasiInovasiState)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter()
useShallowEffect(() => {
kolaborasiState.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
kolaborasiState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/inovasi/kolaborasi-inovasi")
}
}
if (!kolaborasiState.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 Kolaborasi Inovasi</Text>
{kolaborasiState.findUnique.data ? (
<Paper key={kolaborasiState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Nama Kolaborasi Inovasi</Text>
<Text fz={"lg"}>{kolaborasiState.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Tahun</Text>
<Text fz={"lg"}>{kolaborasiState.findUnique.data?.tahun}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi Singkat</Text>
<Text fz={"lg"} >{kolaborasiState.findUnique.data?.slug}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: kolaborasiState.findUnique.data?.deskripsi }} />
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
<Image w={{ base: 150, md: 150, lg: 150 }} src={kolaborasiState.findUnique.data?.image?.link} alt="gambar" />
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Kolaborator</Text>
<Text fz={"lg"}>{kolaborasiState.findUnique.data?.kolaborator}</Text>
</Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (kolaborasiState.findUnique.data) {
setSelectedId(kolaborasiState.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={kolaborasiState.delete.loading || !kolaborasiState.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (kolaborasiState.findUnique.data) {
router.push(`/admin/inovasi/kolaborasi-inovasi/${kolaborasiState.findUnique.data.id}/edit`);
}
}}
disabled={!kolaborasiState.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 kolaborasi inovasi ini?'
/>
</Box>
);
}
export default DetailKolaborasiInovasi;

View File

@@ -1,50 +1,155 @@
'use client'
import { KeamananEditor } from '@/app/admin/(dashboard)/keamanan/_com/keamananEditor';
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 { Box, Button, Center, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor';
import kolaborasiInovasiState from '../../../_state/inovasi/kolaborasi-inovasi';
import { useState } from 'react';
import { toast } from 'react-toastify';
import ApiFetch from '@/lib/api-fetch';
import { Dropzone } from '@mantine/dropzone';
function CreateKolaborasiInovasi() {
function CreateProgramKreatifDesa() {
const stateCreate = useProxy(kolaborasiInovasiState)
const router = useRouter();
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const resetForm = () => {
stateCreate.create.form = {
name: "",
tahun: 0,
slug: "",
deskripsi: "",
kolaborator: "",
imageId: "",
}
setPreviewImage(null);
setFile(null);
}
const handleSubmit = async () => {
if (!file) {
return toast.warn("Pilih file gambar terlebih dahulu");
}
// Upload gambar dulu
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");
}
// Simpan ID gambar ke form
stateCreate.create.form.imageId = uploaded.id;
// Submit data berita
await stateCreate.create.create();
// Reset form setelah submit
resetForm();
router.push("/admin/inovasi/kolaborasi-inovasi")
}
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 Kolaborasi Inovasi</Title>
<Title order={3}>Create Kolaborasi Inovasi</Title>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Nama Kolaborasi Inovasi</Text>}
placeholder="masukkan nama kolaborasi inovasi"
onChange={(val) => stateCreate.create.form.name = val.target.value}
/>
<TextInput
type='number'
label={<Text fz={"sm"} fw={"bold"}>Tahun</Text>}
placeholder="masukkan tahun"
onChange={(val) => stateCreate.create.form.tahun = parseInt(val.target.value)}
/>
<TextInput
onChange={(e) => stateCreate.create.form.slug = e.currentTarget.value}
label={<Text fw={"bold"} fz={"sm"}>Deskripsi Singkat Kolaborasi Inovasi</Text>}
placeholder='Masukkan deskripsi singkat kolaborasi inovasi'
/>
<TextInput
onChange={(e) => stateCreate.create.form.kolaborator = e.currentTarget.value}
label={<Text fw={"bold"} fz={"sm"}>Kolaborator</Text>}
placeholder='Masukkan kolaborator'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Masukkan Image</Text>
<IconImageInPicture size={50} />
<Stack gap={"xs"}>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box>
<Dropzone
onDrop={(files) => {
const newImages = files.map((file) => ({
file,
preview: URL.createObjectURL(file),
label: '',
}));
setFile(newImages[0].file);
setPreviewImage(newImages[0].preview); // ← ini yang kurang
}}
>
<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 images here or click to select files
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Attach as many files as you like, each file should not exceed 5mb
</Text>
</div>
</Group>
</Dropzone>
</Box>
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg={"gray"}>
<IconImageInPicture />
</Center>
)}
</Stack>
</Box>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Kolaborasi Inovasi</Text>}
placeholder='Masukkan nama kolaborasi inovasi'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Deskripsi Singkat Kolaborasi Inovasi</Text>}
placeholder='Masukkan deskripsi singkat kolaborasi inovasi'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Kolaborasi Inovasi</Text>
<KeamananEditor
showSubmit={false}
<CreateEditor
value={stateCreate.create.form.deskripsi}
onChange={(htmlContent) => stateCreate.create.form.deskripsi = htmlContent}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Stack>
</Paper>
</Box>
</Box>
);
}
export default CreateKolaborasiInovasi;
export default CreateProgramKreatifDesa;

View File

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

View File

@@ -1,49 +0,0 @@
'use client'
import { KeamananEditor } from '@/app/admin/(dashboard)/keamanan/_com/keamananEditor';
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';
function EditKolaborasiInovasi() {
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 Kolaborasi Inovasi</Title>
<Box>
<Text fw={"bold"} fz={"sm"}>Masukkan Image</Text>
<IconImageInPicture size={50} />
</Box>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Kolaborasi Inovasi</Text>}
placeholder='Masukkan nama kolaborasi inovasi'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Deskripsi Singkat Kolaborasi Inovasi</Text>}
placeholder='Masukkan deskripsi singkat kolaborasi inovasi'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Kolaborasi Inovasi</Text>
<KeamananEditor
showSubmit={false}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditKolaborasiInovasi;

View File

@@ -1,26 +1,84 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } 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 kolaborasiInovasiState from '../../_state/inovasi/kolaborasi-inovasi';
import { useProxy } from 'valtio/utils';
function KolaborasiInovasi() {
const [search, setSearch] = useState('');
return (
<Box>
<HeaderSearch
title='Kolaborasi Inovasi'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListKolaborasiInovasi/>
<ListKolaborasiInovasi search={search} />
</Box>
);
}
function ListKolaborasiInovasi() {
function ListKolaborasiInovasi({ search }: { search: string }) {
const listState = useProxy(kolaborasiInovasiState)
const { data, loading, page, totalPages, load } = listState.findMany
const router = useRouter();
useEffect(() => {
load(page, 10)
}, [page])
const filteredData = (data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.deskripsi.toLowerCase().includes(keyword) ||
item.slug.toLowerCase().includes(keyword) ||
item.kolaborator.toLowerCase().includes(keyword)
);
});
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={650} />
</Stack>
);
}
if (data.length === 0) {
return (
<Box py={10}>
<Paper p="md" >
<Stack>
<JudulList
title='List Kolaborasi Inovasi'
href='/admin/inovasi/kolaborasi-inovasi/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh style={{ width: '2%', textAlign: 'center' }}>No</TableTh>
<TableTh style={{ width: '15%' }}>Nama Kolaborasi Inovasi</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Tahun</TableTh>
<TableTh style={{ width: '20%' }}>Deskripsi Singkat</TableTh>
<TableTh style={{ width: '5%', textAlign: 'center' }}>Detail</TableTh>
</TableTr>
</TableThead>
</Table>
<Text ta="center">Tidak ada data kolaborasi inovasi yang tersedia</Text>
</Stack>
</Paper>
</Box >
);
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
@@ -31,26 +89,43 @@ function ListKolaborasiInovasi() {
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Kolaborasi Inovasi</TableTh>
<TableTh>Image</TableTh>
<TableTh>Deskripsi Singkat</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
<TableTh style={{ width: '1%', textAlign: 'center' }}>No</TableTh>
<TableTh style={{ width: '15%' }}>Nama Kolaborasi Inovasi</TableTh>
<TableTh style={{ width: '5%', textAlign: 'center' }}>Tahun</TableTh>
<TableTh style={{ width: '20%' }}>Deskripsi Singkat</TableTh>
<TableTh style={{ width: '5%', textAlign: 'center' }}>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>Kolaborasi Inovasi 1</TableTd>
<TableTd>Image</TableTd>
<TableTd>Deskripsi Singkat</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/inovasi/kolaborasi-inovasi/detail')}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
{filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd style={{ width: '1%', textAlign: 'center' }}>{index + 1}</TableTd>
<TableTd style={{ width: '15%' }}>{item.name}</TableTd>
<TableTd style={{ width: '5%', textAlign: 'center' }}>{item.tahun}</TableTd>
<TableTd style={{ width: '20%' }}>{item.slug}</TableTd>
<TableTd style={{ width: '5%', textAlign: 'center' }}>
<Button onClick={() => router.push(`/admin/inovasi/kolaborasi-inovasi/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Table>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
}}
total={totalPages}
mt="md"
mb="md"
/>
</Center>
</Box>
);
}

View File

@@ -0,0 +1,78 @@
/* 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 LayoutTabsLayananOnlineDesa({ children }: { children: React.ReactNode }) {
const router = useRouter()
const pathname = usePathname()
const tabs = [
{
label: "Administrasi Online",
value: "administrasionline",
href: "/admin/inovasi/layanan-online-desa/administrasi-online"
},
{
label: "Jenis Layanan",
value: "jenislayanan",
href: "/admin/inovasi/layanan-online-desa/jenis-layanan"
},
{
label: "Pengaduan Masyarakat",
value: "pengaduanmasyarakat",
href: "/admin/inovasi/layanan-online-desa/pengaduan-masyarakat"
},
{
label: "Jenis Pengaduan",
value: "jenispengaduan",
href: "/admin/inovasi/layanan-online-desa/jenis-pengaduan"
},
{
label: "Informasi Desa",
value: "informasidesa",
href: "/admin/inovasi/layanan-online-desa/informasi-desa"
}
];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
const handleTabChange = (value: string | null) => {
const tab = tabs.find(t => t.value === value)
if (tab) {
router.push(tab.href)
}
setActiveTab(value)
}
useEffect(() => {
const match = tabs.find(tab => tab.href === pathname)
if (match) {
setActiveTab(match.value)
}
}, [pathname])
return (
<Stack>
<Title order={3}>Layanan Online 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 LayoutTabsLayananOnlineDesa;

View File

@@ -0,0 +1,104 @@
'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, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa';
import colors from '@/con/colors';
function DetailAdministrasiOnline() {
const beritaState = useProxy(layananonlineDesa)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter()
useShallowEffect(() => {
beritaState.administrasiOnline.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
beritaState.administrasiOnline.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/inovasi/layanan-online-desa/administrasi-online")
}
}
if (!beritaState.administrasiOnline.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>
<Flex gap={"xs"} justify={"space-between"} mt={10}>
<Text fz={"xl"} fw={"bold"}>Detail Administrasi Online</Text>
<Button
onClick={() => {
if (beritaState.administrasiOnline.findUnique.data) {
setSelectedId(beritaState.administrasiOnline.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={beritaState.administrasiOnline.delete.loading || !beritaState.administrasiOnline.findUnique.data}
color={"red"}
>
<IconTrash size={20} />
</Button>
</Flex>
{beritaState.administrasiOnline.findUnique.data ? (
<Paper key={beritaState.administrasiOnline.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Nama</Text>
<Text fz={"lg"}>{beritaState.administrasiOnline.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Alamat</Text>
<Text fz={"lg"}>{beritaState.administrasiOnline.findUnique.data?.alamat}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Nomor Telepon</Text>
<Text fz={"lg"} >{beritaState.administrasiOnline.findUnique.data?.nomorTelepon}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Jenis Layanan</Text>
<Text fz={"lg"} >{beritaState.administrasiOnline.findUnique.data?.jenisLayanan?.nama}</Text>
</Box>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus administrasi online ini?'
/>
</Box>
);
}
export default DetailAdministrasiOnline;

View File

@@ -0,0 +1,99 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Title } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useShallowEffect } from '@mantine/hooks';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import layananonlineDesa from '../../../_state/inovasi/layanan-online-desa';
function AdministrasiOnline() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Administrasi Online'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListAdministrasiOnline search={search} />
</Box>
);
}
function ListAdministrasiOnline({ search }: { search: string }) {
const listState = useProxy(layananonlineDesa.administrasiOnline)
const router = useRouter();
const {
data,
page,
totalPages,
loading,
load,
} = listState.findMany;
useShallowEffect(() => {
load(page, 10);
}, [page]);
const filteredData = (data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.alamat.toLowerCase().includes(keyword) ||
item.nomorTelepon.toLowerCase().includes(keyword)
);
});
if (loading || !data) {
return <Skeleton h={500} />;
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Title order={3} mb={10}>List Administrasi Online</Title>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Layanan</TableTh>
<TableTh>Alamat</TableTh>
<TableTh>Nomor Telepon</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody style={{ overflowX: "auto" }}>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>{item.alamat}</TableTd>
<TableTd>{item.nomorTelepon}</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin//inovasi/layanan-online-desa/administrasi-online/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)} // ini penting!
total={totalPages}
mt="md"
mb="md"
/>
</Center>
</Box>
);
}
export default AdministrasiOnline;

View File

@@ -1,44 +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 CreateLayananOnlineDesa() {
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 Layanan Online Desa</Title>
<Box>
<Text fw={"bold"} fz={"sm"}>Masukkan Image</Text>
<IconImageInPicture size={50} />
</Box>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Layanan Online Desa</Text>}
placeholder='Masukkan nama LayananOnlineDesa'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Layanan Online Desa</Text>
<KeamananEditor
showSubmit={false}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreateLayananOnlineDesa;

View File

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

View File

@@ -1,42 +0,0 @@
'use client'
import colors from "@/con/colors";
import { Box, Button, Paper, Stack, Title, TextInput, Group, Text } from "@mantine/core";
import { IconArrowBack, IconImageInPicture } from "@tabler/icons-react";
import { useRouter } from "next/navigation";
import { KeamananEditor } from "../../../keamanan/_com/keamananEditor";
export default function EditLayananOnlineDesa() {
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 Layanan Online Desa</Title>
<Box>
<Text fw={"bold"} fz={"sm"}>Masukkan Image</Text>
<IconImageInPicture size={50} />
</Box>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Layanan Online Desa</Text>}
placeholder='Masukkan nama LayananOnlineDesa'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Layanan Online Desa</Text>
<KeamananEditor
showSubmit={false}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}

View File

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

View File

@@ -0,0 +1,103 @@
'use client'
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa';
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
function DetailJenisLayanan() {
const state = useProxy(layananonlineDesa.jenisLayanan)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter()
useShallowEffect(() => {
state.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
state.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/inovasi/layanan-online-desa/jenis-layanan")
}
}
if (!state.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Jenis Layanan</Text>
{state.findUnique.data ? (
<Paper key={state.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Nama</Text>
<Text fz={"lg"}>{state.findUnique.data?.nama}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
<Text fz={"lg"}dangerouslySetInnerHTML={{ __html: state.findUnique.data?.deskripsi }}></Text>
</Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (state.findUnique.data) {
setSelectedId(state.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={state.delete.loading || !state.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (state.findUnique.data) {
router.push(`/admin/inovasi/layanan-online-desa/jenis-layanan/${state.findUnique.data.id}/edit`);
}
}}
disabled={!state.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 jenis layanan ini?'
/>
</Box>
);
}
export default DetailJenisLayanan;

View File

@@ -0,0 +1,70 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
function CreateJenisLayanan() {
const router = useRouter();
const statePasar = useProxy(layananonlineDesa.jenisLayanan)
useEffect(() => {
statePasar.findMany.load();
}, []);
const resetForm = () => {
statePasar.create.form = {
nama: "",
deskripsi: "",
};
}
const handleSubmit = async () => {
await statePasar.create.create();
resetForm();
router.push("/admin/inovasi/layanan-online-desa/jenis-layanan")
}
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 Jenis Layanan</Title>
<TextInput
value={statePasar.create.form.nama}
onChange={(val) => {
statePasar.create.form.nama = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Jenis Layanan</Text>}
placeholder='Masukkan nama jenis layanan'
/>
<TextInput
value={statePasar.create.form.deskripsi}
onChange={(val) => {
statePasar.create.form.deskripsi = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Deskripsi</Text>}
placeholder='Masukkan deskripsi'
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
</Box>
);
}
export default CreateJenisLayanan;

View File

@@ -0,0 +1,88 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import layananonlineDesa from '../../../_state/inovasi/layanan-online-desa';
function JenisLayanan() {
const [search, setSearch] = useState("")
return (
<Box>
<HeaderSearch
title='Jenis Layanan'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListJenisLayanan search={search} />
</Box>
);
}
function ListJenisLayanan({ search }: { search: string }) {
const stateList = useProxy(layananonlineDesa.jenisLayanan)
const router = useRouter()
useShallowEffect(() => {
stateList.findMany.load()
}, [])
const filteredData = (stateList.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.nama.toLowerCase().includes(keyword)
);
});
if (!stateList.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Jenis Layanan'
href='/admin/inovasi/layanan-online-desa/jenis-layanan/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Jenis Layanan</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.nama}</TableTd>
<TableTd>{item.deskripsi}</TableTd>
<TableTd>
<Button color="green" onClick={() => router.push(`/admin/inovasi/layanan-online-desa/jenis-layanan/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>
</Box>
);
}
export default JenisLayanan;

View File

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

View File

@@ -0,0 +1,61 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
function CreateJenisPengaduan() {
const router = useRouter();
const state = useProxy(layananonlineDesa.jenisPengaduan)
useEffect(() => {
state.findMany.load();
}, []);
const resetForm = () => {
state.create.form = {
nama: "",
};
}
const handleSubmit = async () => {
await state.create.create();
resetForm();
router.push("/admin/inovasi/layanan-online-desa/jenis-pengaduan")
}
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 Jenis Pengaduan</Title>
<TextInput
value={state.create.form.nama}
onChange={(val) => {
state.create.form.nama = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Jenis Pengaduan</Text>}
placeholder='Masukkan nama jenis pengaduan'
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
</Box>
);
}
export default CreateJenisPengaduan;

View File

@@ -0,0 +1,125 @@
'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 { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import layananonlineDesa from '../../../_state/inovasi/layanan-online-desa';
function JenisPengaduan() {
const [search, setSearch] = useState("")
return (
<Box>
<HeaderSearch
title='Jenis Pengaduan'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListJenisPengaduan search={search} />
</Box>
);
}
function ListJenisPengaduan({ search }: { search: string }) {
const state = useProxy(layananonlineDesa.jenisPengaduan)
const router = useRouter()
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
useShallowEffect(() => {
state.findMany.load()
}, [])
const handleHapus = async () => {
if (selectedId) {
await state.delete.byId(selectedId);
setModalHapus(false)
setSelectedId(null)
}
}
const filteredData = (state.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.nama.toLowerCase().includes(keyword)
);
});
if (!state.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Jenis Pengaduan'
href='/admin/inovasi/layanan-online-desa/jenis-pengaduan/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Jenis Pengaduan</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Hapus</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.nama}</TableTd>
<TableTd>
<Button color="green"
onClick={() => {
if (item) {
router.push(`/admin/inovasi/layanan-online-desa/jenis-pengaduan/${item.id}`);
}
}}
>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<Button color="red"
onClick={() => {
if (item) {
setSelectedId(item.id);
setModalHapus(true);
}
}}
disabled={!item}
>
<IconTrash size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>
{/* Modal Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus jenis pengaduan ini?"
/>
</Box>
);
}
export default JenisPengaduan;

View File

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

View File

@@ -1,56 +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 LayananOnlineDesa() {
return (
<Box>
<HeaderSearch
title='Layanan Online Desa'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
/>
<ListLayananOnlineDesa/>
</Box>
);
}
function ListLayananOnlineDesa() {
const router = useRouter();
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Layanan Online Desa'
href='/admin/inovasi/layanan-online-desa/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Layanan</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>Layanan Online Desa 1</TableTd>
<TableTd>Deskripsi Layanan Online Desa 1</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/inovasi/layanan-online-desa/detail')}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
</TableTbody>
</Table>
</Paper>
</Box>
);
}
export default LayananOnlineDesa;

View File

@@ -0,0 +1,124 @@
'use client'
import { useProxy } from 'valtio/utils';
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa';
import colors from '@/con/colors';
function DetailPengaduanMasyarakat() {
const pengaduanState = useProxy(layananonlineDesa)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter()
useShallowEffect(() => {
pengaduanState.pengaduanMasyarakat.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
pengaduanState.pengaduanMasyarakat.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/inovasi/layanan-online-desa/pengaduan-masyarakat")
}
}
if (!pengaduanState.pengaduanMasyarakat.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>
<Flex gap={"xs"} justify={"space-between"} mt={10}>
<Text fz={"xl"} fw={"bold"}>Detail Pengaduan Masyarakat</Text>
<Button
onClick={() => {
if (pengaduanState.pengaduanMasyarakat.findUnique.data) {
setSelectedId(pengaduanState.pengaduanMasyarakat.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={pengaduanState.pengaduanMasyarakat.delete.loading || !pengaduanState.pengaduanMasyarakat.findUnique.data}
color={"red"}
>
<IconTrash size={20} />
</Button>
</Flex>
{pengaduanState.pengaduanMasyarakat.findUnique.data ? (
<Paper key={pengaduanState.pengaduanMasyarakat.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Nama</Text>
<Text fz={"lg"}>{pengaduanState.pengaduanMasyarakat.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Email</Text>
<Text fz={"lg"}>{pengaduanState.pengaduanMasyarakat.findUnique.data?.email}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Nomor Telepon</Text>
<Text fz={"lg"} >{pengaduanState.pengaduanMasyarakat.findUnique.data?.nomorTelepon}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>NIK</Text>
<Text fz={"lg"} >{pengaduanState.pengaduanMasyarakat.findUnique.data?.nik}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Judul Pengaduan</Text>
<Text fz={"lg"} >{pengaduanState.pengaduanMasyarakat.findUnique.data?.judulPengaduan}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Lokasi Kejadian</Text>
<Text fz={"lg"} >{pengaduanState.pengaduanMasyarakat.findUnique.data?.lokasiKejadian}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi Pengaduan</Text>
<Text fz={"lg"} >{pengaduanState.pengaduanMasyarakat.findUnique.data?.deskripsiPengaduan}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Jenis Pengaduan</Text>
<Text fz={"lg"} >{pengaduanState.pengaduanMasyarakat.findUnique.data?.jenisPengaduan?.nama}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
<Image w={{ base: 150, md: 150, lg: 150 }} src={pengaduanState.pengaduanMasyarakat.findUnique.data?.image?.link} alt="gambar" />
</Box>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus administrasi online ini?'
/>
</Box>
);
}
export default DetailPengaduanMasyarakat;

View File

@@ -0,0 +1,99 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Title } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useShallowEffect } from '@mantine/hooks';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import layananonlineDesa from '../../../_state/inovasi/layanan-online-desa';
function PengaduanMasyarakat() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Pengaduan Masyarakat'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListPengaduanMasyarakat search={search} />
</Box>
);
}
function ListPengaduanMasyarakat({ search }: { search: string }) {
const listState = useProxy(layananonlineDesa.pengaduanMasyarakat)
const router = useRouter();
const {
data,
page,
totalPages,
loading,
load,
} = listState.findMany;
useShallowEffect(() => {
load(page, 10);
}, [page]);
const filteredData = (data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.email.toLowerCase().includes(keyword) ||
item.nomorTelepon.toLowerCase().includes(keyword)
);
});
if (loading || !data) {
return <Skeleton h={500} />;
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Title order={3} mb={10}>List Pengaduan Masyarakat</Title>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Email</TableTh>
<TableTh>Nomor Telepon</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody style={{ overflowX: "auto" }}>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>{item.email}</TableTd>
<TableTd>{item.nomorTelepon}</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin//inovasi/layanan-online-desa/administrasi-online/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)} // ini penting!
total={totalPages}
mt="md"
mb="md"
/>
</Center>
</Box>
);
}
export default PengaduanMasyarakat;

View File

@@ -0,0 +1,149 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import programKreatifState from '@/app/admin/(dashboard)/_state/inovasi/program-kreatif';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import SelectIconProgramEdit from '../../_lib/selectIconEdit';
interface FormProgramKreatif {
name: string;
deskripsi: string;
slug: string;
icon: string;
}
type IconKey = 'ekowisata' | 'kompetisi' | 'wisata' | 'ekonomi' | 'sampah';
function EditProgramKreatifDesa() {
const stateProgramKreatif = useProxy(programKreatifState)
const params = useParams()
const router = useRouter();
const [formData, setFormData] = useState<FormProgramKreatif>({
name: '',
deskripsi: '',
slug: '',
icon: '',
})
useEffect(() => {
const loadProgramKreatif = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await stateProgramKreatif.update.load(id);
if (data) {
// ⬇️ FIX PENTING: tambahkan ini
stateProgramKreatif.update.id = id;
stateProgramKreatif.update.form = {
name: data.name,
slug: data.slug,
deskripsi: data.deskripsi,
icon: data.icon,
};
setFormData({
name: data.name,
slug: data.slug,
deskripsi: data.deskripsi,
icon: data.icon,
});
}
} catch (error) {
console.error("Error loading program kreatif:", error);
toast.error("Gagal memuat data program kreatif");
}
}
loadProgramKreatif();
}, [params?.id]);
const handleSubmit = async () => {
try {
stateProgramKreatif.update.form = {
...stateProgramKreatif.update.form,
name: formData.name.trim(),
deskripsi: formData.deskripsi.trim(),
slug: formData.slug.trim(),
icon: formData.icon.trim(),
}
await stateProgramKreatif.update.submit();
router.push("/admin/inovasi/program-kreatif-desa");
} catch (error) {
console.error("Error updating program kreatif:", error);
toast.error("Gagal memuat data program kreatif");
}
}
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={3}>Edit Program Kreatif Desa</Title>
<TextInput
value={formData.name}
label={<Text fz={"sm"} fw={"bold"}>Nama Program Kreatif Desa</Text>}
placeholder="masukkan nama program kreatif desa"
onChange={(val) => {
setFormData({
...formData,
name: val.target.value
})
}}
/>
<TextInput
value={formData.slug}
label={<Text fz={"sm"} fw={"bold"}>Deskripsi Singkat Program Kreatif Desa</Text>}
placeholder="masukkan deskripsi singkat program kreatif desa"
onChange={(val) => {
setFormData({
...formData,
slug: val.target.value
})
}}
/>
<Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }));
stateProgramKreatif.update.form.deskripsi = htmlContent;
}}
/>
</Box>
<Box>
<Text fz={"sm"} fw={"bold"}>Ikon Program Kreatif Desa</Text>
<SelectIconProgramEdit
value={formData.icon as IconKey}
onChange={(value) => {
setFormData((prev) => ({ ...prev, icon: value }));
stateProgramKreatif.update.form.icon = value;
}}
/>
</Box>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Edit Berita</Button>
</Stack>
</Paper>
</Box>
);
}
export default EditProgramKreatifDesa;

View File

@@ -0,0 +1,127 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'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, IconChartLine, IconEdit, IconLeaf, IconRecycle, IconTent, IconTrophy, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import React, { useState } from 'react';
import { useProxy } from 'valtio/utils';
import programKreatifState from '../../../_state/inovasi/program-kreatif';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailProgramKreatifDesa() {
const [modalHapus, setModalHapus] = useState(false)
const stateProgramKreatif = useProxy(programKreatifState)
const router = useRouter()
const params = useParams()
const [selectedId, setSelectedId] = useState<string | null>(null)
const iconMap: Record<string, React.FC<any>> = {
ekowisata: IconLeaf,
kompetisi: IconTrophy,
wisata: IconTent,
ekonomi: IconChartLine,
sampah: IconRecycle,
};
useShallowEffect(() => {
stateProgramKreatif.findUnique.load(params?.id as string)
}, [params?.id])
const handleHapus = () => {
if (selectedId) {
stateProgramKreatif.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/inovasi/program-kreatif-desa")
}
}
if (!stateProgramKreatif.findUnique.data) {
return (
<Stack>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Program Kreatif Desa</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Program Kreatif Desa</Text>
<Text fz={"lg"}>{stateProgramKreatif.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Ikon Program Kreatif Desa</Text>
{iconMap[stateProgramKreatif.findUnique.data?.icon] && (
<Box title={stateProgramKreatif.findUnique.data?.icon}>
{React.createElement(iconMap[stateProgramKreatif.findUnique.data?.icon], { size: 24 })}
</Box>
)}
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi Singkat</Text>
<Text fz={"lg"}>{stateProgramKreatif.findUnique.data?.slug}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: stateProgramKreatif.findUnique.data?.deskripsi }}></Text>
</Box>
<Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (stateProgramKreatif.findUnique.data) {
setSelectedId(stateProgramKreatif.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={stateProgramKreatif.delete.loading || !stateProgramKreatif.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (stateProgramKreatif.findUnique.data) {
router.push(`/admin/inovasi/program-kreatif-desa/${stateProgramKreatif.findUnique.data.id}/edit`);
}
}}
disabled={!stateProgramKreatif.findUnique.data}
color={"green"}
>
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus program kreatif desa ini?"
/>
</Box>
);
}
export default DetailProgramKreatifDesa;

View File

@@ -0,0 +1,84 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import { Box, rem, Select } from '@mantine/core';
import {
IconChartLine,
IconClipboardTextFilled,
IconLeaf,
IconRecycle,
IconScale,
IconTent,
IconTrashFilled,
IconTrophy,
IconTruckFilled,
} from '@tabler/icons-react';
import { useEffect, useState } from 'react';
const iconMap = {
ekowisata: { label: 'Ekowisata', icon: IconLeaf },
kompetisi: { label: 'Kompetisi', icon: IconTrophy },
wisata: { label: 'Wisata', icon: IconTent },
ekonomi: { label: 'Ekonomi', icon: IconChartLine },
sampah: { label: 'Sampah', icon: IconRecycle },
truck: { label: 'Truck', icon: IconTruckFilled },
scale: { label: 'Scale', icon: IconScale },
clipboard: { label: 'Clipboard', icon: IconClipboardTextFilled },
trash: { label: 'Trash', icon: IconTrashFilled },
};
type IconKey = keyof typeof iconMap;
const iconList = Object.entries(iconMap).map(([value, data]) => ({
value,
label: data.label,
}));
export default function SelectIconProgram(
{ onChange }: { onChange: (value: IconKey) => void }) {
const [selectedIcon, setSelectedIcon] = useState<IconKey>('ekowisata');
const IconComponent = iconMap[selectedIcon]?.icon || null;
// Push default icon ke state saat render awal
useEffect(() => {
onChange(selectedIcon);
}, []);
return (
<Box maw={300}>
<Select
placeholder="Pilih ikon"
value={selectedIcon}
onChange={(value) => {
if (value) {
setSelectedIcon(value as IconKey);
onChange(value as IconKey);
}
}}
data={iconList}
leftSection={
IconComponent && (
<Box>
<IconComponent size={24} stroke={1.5} />
</Box>
)
}
withCheckIcon={false}
searchable={false}
rightSectionWidth={0}
styles={{
input: {
textAlign: 'left',
fontSize: rem(16),
paddingLeft: 40,
},
section: {
left: 10,
right: 'auto',
},
}}
/>
</Box>
);
}

View File

@@ -0,0 +1,78 @@
'use client'
import { Box, rem, Select } from '@mantine/core';
import {
IconChartLine,
IconClipboardTextFilled,
IconLeaf,
IconRecycle,
IconScale,
IconTent,
IconTrashFilled,
IconTrophy,
IconTruckFilled,
} from '@tabler/icons-react';
const iconMap = {
ekowisata: { label: 'Ekowisata', icon: IconLeaf },
kompetisi: { label: 'Kompetisi', icon: IconTrophy },
wisata: { label: 'Wisata', icon: IconTent },
ekonomi: { label: 'Ekonomi', icon: IconChartLine },
sampah: { label: 'Sampah', icon: IconRecycle },
truck: { label: 'Truck', icon: IconTruckFilled },
scale: { label: 'Scale', icon: IconScale },
clipboard: { label: 'Clipboard', icon: IconClipboardTextFilled },
trash: { label: 'Trash', icon: IconTrashFilled },
};
type IconKey = keyof typeof iconMap;
const iconList = Object.entries(iconMap).map(([value, data]) => ({
value,
label: data.label,
}));
export default function SelectIconProgramEdit({
onChange,
value,
}: {
onChange: (value: IconKey) => void;
value: IconKey;
}) {
const IconComponent = iconMap[value]?.icon || null;
return (
<Box maw={300}>
<Select
placeholder="Pilih ikon"
value={value}
onChange={(value) => {
if (value) onChange(value as IconKey);
}}
data={iconList}
leftSection={
IconComponent && (
<Box>
<IconComponent size={24} stroke={1.5} />
</Box>
)
}
withCheckIcon={false}
searchable={false}
rightSectionWidth={0}
styles={{
input: {
textAlign: 'left',
fontSize: rem(16),
paddingLeft: 40,
},
section: {
left: 10,
right: 'auto',
},
}}
/>
</Box>
);
}

View File

@@ -1,48 +1,70 @@
'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 { 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 programKreatifState from '../../../_state/inovasi/program-kreatif';
import SelectIconProgram from '../_lib/selectIcon';
function CreateProgramKreatifDesa() {
const stateCreate = useProxy(programKreatifState)
const router = useRouter();
const resetForm = () => {
stateCreate.create.form = {
name: "",
slug: "",
deskripsi: "",
icon: "",
}
}
const handleSubmit = async () => {
await stateCreate.create.create();
resetForm();
router.push("/admin/inovasi/program-kreatif-desa")
}
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 Kreatif Desa</Title>
<Title order={3}>Create Program Kreatif Desa</Title>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Nama Program Kreatif Desa</Text>}
placeholder="masukkan nama program kreatif desa"
onChange={(val) => stateCreate.create.form.name = val.target.value}
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Masukkan Image</Text>
<IconImageInPicture size={50} />
<Text fz={"sm"} fw={"bold"}>Ikon Program Kreatif Desa</Text>
<SelectIconProgram onChange={(value) => stateCreate.create.form.icon = value} />
</Box>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Program Kreatif Desa</Text>}
placeholder='Masukkan nama program kreatif desa'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Deskripsi Singkat Program Kreatif Desa</Text>}
placeholder='Masukkan deskripsi singkat program kreatif desa'
onChange={(e) => stateCreate.create.form.slug = e.currentTarget.value}
label={<Text fw={"bold"} fz={"sm"}>Deskripsi Singkat Program Kreatif Desa</Text>}
placeholder='Masukkan deskripsi singkat program kreatif desa'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Program Kreatif Desa</Text>
<KeamananEditor
showSubmit={false}
<CreateEditor
value={stateCreate.create.form.deskripsi}
onChange={(htmlContent) => stateCreate.create.form.deskripsi = htmlContent}
/>
</Box>
<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, 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 DetailProgramKreatifDesa() {
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 Kreatif Desa</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Program Kreatif Desa</Text>
<Text fz={"lg"}>Test Judul</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
<Image src={"/"} alt="gambar" />
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi Singkat</Text>
<Text fz={"lg"}>Test Deskripsi Singkat</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text>
<Text fz={"lg"} >Test Deskripsi</Text>
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red">
<IconX size={20} />
</Button>
<Button onClick={() => router.push('/admin/inovasi/program-kreatif-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 DetailProgramKreatifDesa;

View File

@@ -1,49 +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 EditProgramKreatifDesa() {
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 Kreatif Desa</Title>
<Box>
<Text fw={"bold"} fz={"sm"}>Masukkan Image</Text>
<IconImageInPicture size={50} />
</Box>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Program Kreatif Desa</Text>}
placeholder='Masukkan nama Program Kreatif Desa'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Deskripsi Singkat Program Kreatif Desa</Text>}
placeholder='Masukkan deskripsi singkat program kreatif desa'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Program Kreatif Desa</Text>
<KeamananEditor
showSubmit={false}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditProgramKreatifDesa;

View File

@@ -1,58 +1,155 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import React from 'react';
import colors from '@/con/colors';
import { Box, Button, Paper, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } 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 programKreatifState from '../../_state/inovasi/program-kreatif';
import { useProxy } from 'valtio/utils';
import {
IconChartLine,
IconLeaf,
IconRecycle,
IconTent,
IconTrophy,
} from '@tabler/icons-react';
function ProgramKreatifDesa() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Program Kreatif Desa'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListProgramKreatifDesa/>
<ListProgramKreatifDesa search={search} />
</Box>
);
}
function ListProgramKreatifDesa() {
function ListProgramKreatifDesa({ search }: { search: string }) {
const listState = useProxy(programKreatifState)
const { data, loading, page, totalPages, load } = listState.findMany
const router = useRouter();
useEffect(() => {
load(page, 10)
}, [page])
const filteredData = (data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.deskripsi.toLowerCase().includes(keyword) ||
item.slug.toLowerCase().includes(keyword) ||
item.icon.toLowerCase().includes(keyword)
);
});
const iconMap: Record<string, React.FC<any>> = {
ekowisata: IconLeaf,
kompetisi: IconTrophy,
wisata: IconTent,
ekonomi: IconChartLine,
sampah: IconRecycle,
};
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={650} />
</Stack>
);
}
if (data.length === 0) {
return (
<Box py={10}>
<Paper p="md" >
<Stack>
<JudulList
title='List Program Kreatif Desa'
href='/admin/inovasi/program-kreatif-desa/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Nama Program Kreatif Desa</TableTh>
<TableTh>Deskripsi Singkat</TableTh>
<TableTh>Ikon</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
</Table>
<Text ta="center">Tidak ada data program kreatif desa yang tersedia</Text>
</Stack>
</Paper>
</Box >
);
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Paper bg={colors['white-1']} p={'md'} h={{ base: 'auto', md: 650 }}>
<JudulList
title='List Program Kreatif Desa'
href='/admin/inovasi/program-kreatif-desa/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Program Kreatif Desa</TableTh>
<TableTh>Image</TableTh>
<TableTh>Deskripsi Singkat</TableTh>
<TableTh>Detail</TableTh>
<Box style={{ overflowY: 'auto' }}>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh>
<TableTh style={{ width: '20%' }}>Nama Program Kreatif Desa</TableTh>
<TableTh style={{ width: '35%' }}>Deskripsi Singkat</TableTh>
<TableTh style={{ width: '10%' }}>Ikon</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>Program Kreatif Desa 1</TableTd>
<TableTd>Image</TableTd>
<TableTd>Deskripsi Singkat</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/inovasi/program-kreatif-desa/detail')}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
</TableTbody>
</Table>
</TableThead>
<TableTbody>
{filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd style={{ width: '5%', textAlign: 'center' }}>{index + 1}</TableTd>
<TableTd style={{ width: '20%', wordWrap: 'break-word' }}>{item.name}</TableTd>
<TableTd style={{ width: '35%', wordWrap: 'break-word' }} dangerouslySetInnerHTML={{ __html: item.slug }}></TableTd>
<TableTd style={{ width: '10%' }}>
{iconMap[item.icon] && (
<Box title={item.icon}>
{React.createElement(iconMap[item.icon], { size: 24 })}
</Box>
)}
</TableTd>
<TableTd style={{ width: '15%', textAlign: 'center' }}>
<Button onClick={() => router.push(`/admin/inovasi/program-kreatif-desa/${item.id}`)}>
<IconDeviceImac size={25} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
}}
total={totalPages}
mt="md"
mb="md"
/>
</Center>
</Box>
);
}
export default ProgramKreatifDesa;

View File

@@ -52,7 +52,6 @@ const handleSubmit = async () => {
toast.error('Gagal memperbarui data');
}
};
return (
<Box>
<Box mb={10}>

View File

@@ -0,0 +1,63 @@
/* 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 LayoutTabsPengelolaanSampahBankSampah({ children }: { children: React.ReactNode }) {
const router = useRouter()
const pathname = usePathname()
const tabs = [
{
label: "List Pengelolaan Sampah Bank Sampah",
value: "listpengelolaansampahbanksampah",
href: "/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah"
},
{
label: "Keterangan Bank Sampah Terdekat",
value: "keteranganbanksampahterdekat",
href: "/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat"
},
];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
const handleTabChange = (value: string | null) => {
const tab = tabs.find(t => t.value === value)
if (tab) {
router.push(tab.href)
}
setActiveTab(value)
}
useEffect(() => {
const match = tabs.find(tab => tab.href === pathname)
if (match) {
setActiveTab(match.value)
}
}, [pathname])
return (
<Stack>
<Title order={3}>Layanan Online 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 LayoutTabsPengelolaanSampahBankSampah;

View File

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

View File

@@ -0,0 +1,115 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah';
import SelectIconProgramEdit from '@/app/admin/(dashboard)/inovasi/program-kreatif-desa/_lib/selectIconEdit';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
interface FormProgramKreatif {
name: string;
icon: string;
}
type IconKey = 'ekowisata' | 'kompetisi' | 'wisata' | 'ekonomi' | 'sampah' | 'truck' | 'scale' | 'clipboard' | 'trash';
function EditProgramKreatifDesa() {
const stateSampah = useProxy(pengelolaanSampahState)
const params = useParams()
const router = useRouter();
const [formData, setFormData] = useState<FormProgramKreatif>({
name: '',
icon: '',
})
useEffect(() => {
const loadProgramKreatif = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await stateSampah.update.load(id);
if (data) {
// ⬇️ FIX PENTING: tambahkan ini
stateSampah.update.id = id;
stateSampah.update.form = {
name: data.name,
icon: data.icon,
};
setFormData({
name: data.name,
icon: data.icon,
});
}
} catch (error) {
console.error("Error loading pengelolaan sampah:", error);
toast.error("Gagal memuat data pengelolaan sampah");
}
}
loadProgramKreatif();
}, [params?.id]);
const handleSubmit = async () => {
try {
stateSampah.update.form = {
...stateSampah.update.form,
name: formData.name.trim(),
icon: formData.icon.trim(),
}
await stateSampah.update.submit();
router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah");
} catch (error) {
console.error("Error updating pengelolaan sampah:", error);
toast.error("Gagal memuat data pengelolaan sampah");
}
}
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={3}>Edit List Pengelolaan Sampah Bank Sampah</Title>
<TextInput
value={formData.name}
label={<Text fz={"sm"} fw={"bold"}>Nama List Pengelolaan Sampah Bank Sampah</Text>}
placeholder="masukkan nama list pengelolaan sampah bank sampah"
onChange={(val) => {
setFormData({
...formData,
name: val.target.value
})
}}
/>
<Box>
<Text fz={"sm"} fw={"bold"}>Ikon List Pengelolaan Sampah Bank Sampah</Text>
<SelectIconProgramEdit
value={formData.icon as IconKey}
onChange={(value) => {
setFormData((prev) => ({ ...prev, icon: value }));
stateSampah.update.form.icon = value;
}}
/>
</Box>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
</Stack>
</Paper>
</Box>
);
}
export default EditProgramKreatifDesa;

View File

@@ -0,0 +1,58 @@
'use client'
import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah';
import SelectIconProgram from '@/app/admin/(dashboard)/inovasi/program-kreatif-desa/_lib/selectIcon';
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';
function CreatePengelolaanSampahBankSampah() {
const stateCreate = useProxy(pengelolaanSampahState)
const router = useRouter();
const resetForm = () => {
stateCreate.create.form = {
name: "",
icon: "",
}
}
const handleSubmit = async () => {
await stateCreate.create.create();
resetForm();
router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah")
}
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={3}>Create List Pengelolaan Sampah Bank Sampah</Title>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Nama Pengelolaan Sampah Bank Sampah</Text>}
placeholder="masukkan nama pengelolaan sampah bank sampah"
onChange={(val) => stateCreate.create.form.name = val.target.value}
/>
<Box>
<Text fz={"sm"} fw={"bold"}>Ikon Pengelolaan Sampah Bank Sampah</Text>
<SelectIconProgram onChange={(value) => stateCreate.create.form.icon = value} />
</Box>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreatePengelolaanSampahBankSampah;

View File

@@ -0,0 +1,135 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'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 { IconChartLine, IconClipboardTextFilled, IconEdit, IconLeaf, IconRecycle, IconScale, IconSearch, IconTent, IconTrashFilled, IconTrophy, IconTruckFilled, 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 pengelolaanSampahState from '../../../_state/lingkungan/pengelolaan-sampah';
import React from 'react';
function PengelolaanSampahBankSampah() {
const [search, setSearch] = useState("")
return (
<Box>
<HeaderSearch
title='List Pengelolaan Sampah Bank Sampah'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListPengelolaanSampahBankSampah search={search} />
</Box>
);
}
function ListPengelolaanSampahBankSampah({ search }: { search: string }) {
const stateList = useProxy(pengelolaanSampahState)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter()
useShallowEffect(() => {
stateList.findMany.load()
}, [])
const handleHapus = () => {
if (selectedId) {
stateList.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
}
}
const filteredData = (stateList.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword)
|| item.icon.toLowerCase().includes(keyword)
);
});
const iconMap: Record<string, React.FC<any>> = {
ekowisata: IconLeaf,
kompetisi: IconTrophy,
wisata: IconTent,
ekonomi: IconChartLine,
sampah: IconRecycle,
truck: IconTruckFilled,
scale: IconScale,
clipboard: IconClipboardTextFilled,
trash: IconTrashFilled,
};
if (!stateList.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Pengelolaan Sampah Bank Sampah'
href='/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Pengelolaan Sampah</TableTh>
<TableTh>Icon</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd style={{ width: '10%' }}>
{iconMap[item.icon] && (
<Box title={item.icon}>
{React.createElement(iconMap[item.icon], { size: 24 })}
</Box>
)}
</TableTd>
<TableTd>
<Button color="green" onClick={() => router.push(`/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/${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 pengelolaan sampah bank sampah ini?'
/>
</Box>
);
}
export default PengelolaanSampahBankSampah;

View File

@@ -1,46 +0,0 @@
'use client'
import { KeamananEditor } from '@/app/admin/(dashboard)/keamanan/_com/keamananEditor';
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';
function CreateMekanismeBankSampah() {
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 Mekanisme Bank Sampah</Title>
<Box>
<Text fw={"bold"} fz={"sm"}>Masukkan Image</Text>
<IconImageInPicture size={50} />
</Box>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Judul</Text>}
placeholder='Masukkan judul'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi</Text>
<KeamananEditor
showSubmit={false}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreateMekanismeBankSampah;

View File

@@ -1,61 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Stack, Text } from '@mantine/core';
import { IconArrowBack, IconEdit, IconImageInPicture, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailPengelolaanSampahBankSampah() {
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 Mekanisme Bank Sampah</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Judul</Text>
<Text fz={"lg"}>Test Judul</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
<IconImageInPicture color={colors['blue-button']} size={25} />
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text>
<Text fz={"lg"} >Test Deskripsi</Text>
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red">
<IconX size={20} />
</Button>
<Button onClick={() => router.push('/admin/lingkungan/pengelolaan-sampah-bank-sampah/list_pengelolaan_sampah_bank_sampah/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 DetailPengelolaanSampahBankSampah;

View File

@@ -1,46 +0,0 @@
'use client'
import { KeamananEditor } from '@/app/admin/(dashboard)/keamanan/_com/keamananEditor';
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';
function EditMekanismeBankSampah() {
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 Mekanisme Bank Sampah</Title>
<Box>
<Text fw={"bold"} fz={"sm"}>Masukkan Image</Text>
<IconImageInPicture size={50} />
</Box>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Judul</Text>}
placeholder='Masukkan judul'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi</Text>
<KeamananEditor
showSubmit={false}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditMekanismeBankSampah;

View File

@@ -1,55 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Image, Paper, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import JudulListTab from '../../../_com/judulListTab';
function ListPengelolaanSampahBankSampah() {
const router = useRouter();
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<JudulListTab
title='Mekanisme Bank Sampah'
href='/admin/lingkungan/pengelolaan-sampah-bank-sampah/list_pengelolaan_sampah_bank_sampah/create'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
/>
<Title order={4}>List Mekanisme Bank Sampah</Title>
<Box style={{ overflowX: "auto" }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
<TableThead>
<TableTr>
<TableTh>Judul</TableTh>
<TableTh>Gambar</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>Judul</Text>
</Box>
</TableTd>
<TableTd>
<Image w={100} alt="image" />
</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/lingkungan/pengelolaan-sampah-bank-sampah/list_pengelolaan_sampah_bank_sampah/detail')}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
</TableTbody>
</Table>
</Box>
</Stack>
</Paper>
</Box>
)
}
export default ListPengelolaanSampahBankSampah;

View File

@@ -1,36 +0,0 @@
import { Box, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import colors from '@/con/colors';
import ListPengelolaanSampahBankSampah from './list_pengelolaan_sampah_bank_sampah/page';
import KeteranganBankSampahTerdekat from './keterangan_bank_sampah_terdekat/page';
function PengelolaanSampahBankSampah() {
return (
<Box>
<Stack>
<Title order={3}>Pengelolaan Sampah Bank Sampah</Title>
<Tabs defaultValue="list" color={colors['blue-button']} variant="pills">
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
<TabsTab value="list">
List Pengelolaan Sampah Bank Sampah
</TabsTab>
<TabsTab value="maps">
Keterangan Bank Sampah Terdekat
</TabsTab>
</TabsList>
<TabsPanel value="list">
<ListPengelolaanSampahBankSampah />
</TabsPanel>
<TabsPanel value="maps">
<KeteranganBankSampahTerdekat />
</TabsPanel>
</Tabs>
</Stack>
</Box>
)
}
export default PengelolaanSampahBankSampah;

View File

@@ -10,17 +10,29 @@ import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
interface FormDaftarInformasi {
jenisInformasi: string;
deskripsi: string;
tanggal: string;
}
function EditDaftarInformasiPublik() {
const daftarInformasi = useProxy(daftarInformasiPublik)
const router = useRouter()
const params = useParams()
const [formData, setFormData] = useState({
jenisInformasi: daftarInformasi.edit.form.jenisInformasi || '',
deskripsi: daftarInformasi.edit.form.deskripsi || '',
tanggal: daftarInformasi.edit.form.tanggal || '',
const [formData, setFormData] = useState<FormDaftarInformasi>({
jenisInformasi: '',
deskripsi: '',
tanggal: '',
})
const formatDateForInput = (dateString: string) => {
if (!dateString) return '';
const date = new Date(dateString);
return date.toISOString().split('T')[0];
};
useEffect(() => {
const loadDaftarInformasi = async () => {
const id = params?.id as string;
@@ -48,12 +60,11 @@ function EditDaftarInformasiPublik() {
try {
daftarInformasi.edit.form = {
...daftarInformasi.edit.form,
jenisInformasi: formData.jenisInformasi,
deskripsi: formData.deskripsi,
tanggal: formData.tanggal,
jenisInformasi: formData.jenisInformasi.trim(),
deskripsi: formData.deskripsi.trim(),
tanggal: formData.tanggal.trim(),
}
await daftarInformasi.edit.update()
toast.success("Berita berhasil diperbarui!");
router.push("/admin/ppid/daftar-informasi-publik-desa-darmasaba");
} catch (error) {
console.error("Error updating berita:", error);
@@ -73,7 +84,7 @@ function EditDaftarInformasiPublik() {
<Title order={3}>Edit Daftar Informasi Publik Desa Darmasaba</Title>
<TextInput
value={formData.jenisInformasi}
label="Jenis Informasi"
label={<Text fz={"sm"} fw={"bold"}>Jenis Informasi</Text>}
placeholder="masukkan jenis informasi"
onChange={(val) => {
setFormData({
@@ -93,8 +104,9 @@ function EditDaftarInformasiPublik() {
/>
</Box>
<TextInput
value={formData.tanggal}
label="Tanggal Publikasi"
type='date'
value={formatDateForInput(formData.tanggal)}
label={<Text fz={"sm"} fw={"bold"}>Tanggal Publikasi</Text>}
placeholder="masukkan tanggal publikasi"
onChange={(val) => {
setFormData({

View File

@@ -56,7 +56,9 @@ function DetailDaftarInformasiPublik() {
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Tanggal</Text>
<Text fz={"lg"}>{stateDaftarInformasi.findUnique.data?.tanggal}</Text>
<Text fz={"lg"}>{stateDaftarInformasi.findUnique.data?.tanggal
? new Date(stateDaftarInformasi.findUnique.data.tanggal).toLocaleDateString()
: "-"}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>

View File

@@ -42,7 +42,7 @@ export default function CreateBerita() {
<Stack gap={"xs"}>
<Title order={3}>Create Daftar Informasi Publik Desa Darmasaba</Title>
<TextInput
label="Jenis Informasi"
label={<Text fz={"sm"} fw={"bold"}>Jenis Informasi</Text>}
placeholder="masukkan jenis informasi"
onChange={(val) => {
daftarInformasi.create.form.jenisInformasi = val.target.value
@@ -58,13 +58,13 @@ export default function CreateBerita() {
/>
</Box>
<TextInput
label="Tanggal Publikasi"
placeholder="masukkan tanggal publikasi"
onChange={(val) => {
daftarInformasi.create.form.tanggal = val.target.value
}}
label={<Text fz={"sm"} fw={"bold"}>Tanggal Publikasi</Text>}
type="date"
placeholder="Contoh: 2022-01-01"
value={daftarInformasi.create.form.tanggal}
onChange={(e) => (daftarInformasi.create.form.tanggal = e.currentTarget.value)}
/>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan Berita</Button>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
</Stack>
</Paper>
</Box>

View File

@@ -1,12 +1,13 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Button, Grid, GridCol, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import daftarInformasiPublik from '../../_state/ppid/daftar_informasi_publik/daftarInformasiPublik';
function DaftarInformasiPublik() {
@@ -14,7 +15,7 @@ function DaftarInformasiPublik() {
return (
<Box>
<HeaderSearch
title='Posisi Organisasi'
title='Daftar Informasi Publik Desa Darmasaba'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
@@ -29,12 +30,14 @@ function ListDaftarInformasi({ search }: { search: string }) {
const listData = useProxy(daftarInformasiPublik)
const router = useRouter()
useShallowEffect(() => {
listData.findMany.load()
}, [])
const { data, page, totalPages, loading, load } = listData.findMany
useEffect(() => {
load(page, 10)
}, [page])
const filteredData = (listData.findMany.data || []).filter(item => {
const filteredData = (data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.jenisInformasi.toLowerCase().includes(keyword) ||
@@ -42,28 +45,47 @@ function ListDaftarInformasi({ search }: { search: string }) {
);
});
if (!listData.findMany.data) {
if (loading || !data) {
return (
<Stack>
<Skeleton h={500} />
<Stack py={10}>
<Skeleton height={790} />
</Stack>
)
);
}
if (data.length === 0) {
return (
<Box py={10}>
<Paper p="md" >
<Stack>
<JudulList
title='List Daftar Informasi Publik Desa Darmasaba'
href='/admin/ppid/daftar-informasi-publik-desa-darmasaba/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Jenis Informasi</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
</Table>
<Text ta="center">Tidak ada data daftar informasi publik yang tersedia</Text>
</Stack>
</Paper>
</Box >
);
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Paper bg={colors['white-1']} p={'md'} h={{ base: 870, md: 790 }}>
<Stack>
<Grid>
<GridCol span={{ base: 12, md: 11 }}>
<Text fz={"xl"} fw={"bold"}>List Daftar Informasi Publik Desa Darmasaba</Text>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button onClick={() => router.push("/admin/ppid/daftar-informasi-publik-desa-darmasaba/create")} bg={colors['blue-button']}>
<IconCircleDashedPlus size={25} />
</Button>
</GridCol>
</Grid>
<JudulList
title='List Daftar Informasi Publik Desa Darmasaba'
href='/admin/ppid/daftar-informasi-publik-desa-darmasaba/create'
/>
<Box style={{ overflowX: "auto" }}>
<Table
striped
@@ -74,19 +96,19 @@ function ListDaftarInformasi({ search }: { search: string }) {
>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Jenis Informasi</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Detail</TableTh>
<TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh>
<TableTh style={{ width: '20%' }}>Jenis Informasi</TableTh>
<TableTh style={{ width: '50%' }}>Deskripsi</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd>{index + 1}</TableTd>
<TableTd>{item.jenisInformasi}</TableTd>
<TableTd dangerouslySetInnerHTML={{ __html: item.deskripsi }}></TableTd>
<TableTd>
<TableTd style={{ textAlign: 'center' }}>{index + 1}</TableTd>
<TableTd style={{ wordWrap: 'break-word' }}>{item.jenisInformasi}</TableTd>
<TableTd style={{ wordWrap: 'break-word' }} dangerouslySetInnerHTML={{ __html: item.deskripsi }}></TableTd>
<TableTd style={{ textAlign: 'center' }}>
<Button bg={"green"} onClick={() => router.push(`/admin/ppid/daftar-informasi-publik-desa-darmasaba/${item.id}`)}>
<IconDeviceImacCog size={25} />
</Button>
@@ -98,6 +120,18 @@ function ListDaftarInformasi({ search }: { search: string }) {
</Box>
</Stack>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
}}
total={totalPages}
mt="md"
mb="md"
/>
</Center>
</Box>
)
}

View File

@@ -24,16 +24,20 @@ function GrafikBerdasarkanJenisKelaminRespondenCreate() {
}
const handleSubmit = async () => {
const id = await stategrafikBerdasarkanJenisKelamin.create.create();
if (id) {
const idStr = String(id);
await stategrafikBerdasarkanJenisKelamin.findUnique.load(idStr);
if (stategrafikBerdasarkanJenisKelamin.findUnique.data) {
setDonutData([stategrafikBerdasarkanJenisKelamin.findUnique.data]);
try {
const id = await stategrafikBerdasarkanJenisKelamin.create.create();
if (typeof id !== 'undefined') {
const idStr = String(id);
await stategrafikBerdasarkanJenisKelamin.findUnique.load(idStr);
if (stategrafikBerdasarkanJenisKelamin.findUnique.data) {
setDonutData([stategrafikBerdasarkanJenisKelamin.findUnique.data]);
}
}
resetForm();
router.push("/admin/ppid/ikm-desa-darmasaba/grafik_berdasarkan_jenis_kelamin_responden");
} catch (error) {
console.error('Error submitting form:', error);
}
resetForm();
router.push("/admin/ppid/ikm-desa-darmasaba/grafik_berdasarkan_jenis_kelamin_responden")
}
return (
<Box>

View File

@@ -2,16 +2,16 @@
'use client'
import grafikBerdasarkanJenisKelamin from '@/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanJenisKelamin';
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { Box, Button, Center, Flex, Pagination, 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/judulListTab';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function GrafikBerdasarkanJenisKelamin() {
const [search, setSearch] = useState("");
@@ -36,6 +36,32 @@ function ListGrafikBerdasarkanJenisKelamin({ search }: { search: string }) {
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter();
const { data, page, totalPages, loading, load } = stategrafikBerdasarkanJenisKelamin.findMany
useShallowEffect(() => {
setMounted(true);
load(page, 10)
}, [page]);
useEffect(() => {
if (data) {
const totalLaki = data.reduce((acc: number, cur: any) => acc + Number(cur.laki || 0), 0);
const totalPerempuan = data.reduce((acc: number, cur: any) => acc + Number(cur.perempuan || 0), 0);
setDonutData([
{ name: 'laki', value: totalLaki, color: colors['blue-button'], key: 'laki' },
{ name: 'perempuan', value: totalPerempuan, color: '#10A85AFF', key: 'perempuan' }
]);
}
}, [data])
const filteredData = (data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.laki.toString().toLowerCase().includes(keyword) ||
item.perempuan.toString().toLowerCase().includes(keyword)
);
});
const handleDelete = async () => {
if (selectedId) {
@@ -47,55 +73,56 @@ function ListGrafikBerdasarkanJenisKelamin({ search }: { search: string }) {
}
}
useShallowEffect(() => {
setMounted(true);
stategrafikBerdasarkanJenisKelamin.findMany.load()
}, []);
useEffect(() => {
if (stategrafikBerdasarkanJenisKelamin.findMany.data) {
const totalLaki = stategrafikBerdasarkanJenisKelamin.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.laki || 0), 0);
const totalPerempuan = stategrafikBerdasarkanJenisKelamin.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.perempuan || 0), 0);
setDonutData([
{ name: 'laki', value: totalLaki, color: colors['blue-button'], key: 'laki' },
{ name: 'perempuan', value: totalPerempuan, color: '#10A85AFF', key: 'perempuan' }
]);
}
}, [stategrafikBerdasarkanJenisKelamin.findMany.data])
const filteredData = (stategrafikBerdasarkanJenisKelamin.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
if (loading || !data) {
return (
item.laki.toString().toLowerCase().includes(keyword) ||
item.perempuan.toString().toLowerCase().includes(keyword)
<Stack py={10}>
<Skeleton height={730} />
</Stack>
);
});
if (!stategrafikBerdasarkanJenisKelamin.findMany.data) {
}
if (data.length === 0) {
return (
<Box>
<Skeleton h={500} />
</Box>
)
<Box py={10}>
<Paper p="md">
<Stack>
<JudulList
title='List Data Berdasarkan Jenis Kelamin Responden'
href='/admin/ppid/ikm-desa-darmasaba/grafik_berdasarkan_jenis_kelamin_responden/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Laki-laki</TableTh>
<TableTh>Perempuan</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
</TableThead>
</Table>
<Text ta="center">Tidak ada data berdasarkan jenis kelamin responden yang tersedia</Text>
</Stack>
</Paper>
</Box >
);
}
return (
<Box>
<Stack gap={"xs"}>
<Paper bg={colors['white-1']} p={"md"}>
<JudulListTab
title='List Grafik Berdasarkan Jenis Kelamin Responden'
<Paper bg={colors['white-1']} p={"md"} h={{ base: 730, md: 650 }}>
<JudulList
title='List Data Berdasarkan Jenis Kelamin Responden'
href='/admin/ppid/ikm-desa-darmasaba/grafik_berdasarkan_jenis_kelamin_responden/create'
placeholder='pencarian'
searchIcon={<IconSearch size={16} />}
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Laki-laki</TableTh>
<TableTh>Perempuan</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
<TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh>
<TableTh style={{ width: '20%', textAlign: 'center' }}>Laki-laki</TableTh>
<TableTh style={{ width: '20%', textAlign: 'center' }}>Perempuan</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Edit</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Delete</TableTh>
</TableTr>
</TableThead>
<TableTbody>
@@ -106,16 +133,17 @@ function ListGrafikBerdasarkanJenisKelamin({ search }: { search: string }) {
</TableTd>
</TableTr>
) : (
filteredData.map((item) => (
filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd>{item.laki}</TableTd>
<TableTd>{item.perempuan}</TableTd>
<TableTd>
<TableTd style={{ width: '5%', textAlign: 'center' }}>{index + 1}</TableTd>
<TableTd style={{ width: '20%', textAlign: 'center' }}>{item.laki}</TableTd>
<TableTd style={{ width: '20%', textAlign: 'center' }}>{item.perempuan}</TableTd>
<TableTd style={{ width: '15%', textAlign: 'center' }}>
<Button color='green' onClick={() => router.push(`/admin/ppid/ikm-desa-darmasaba/grafik_berdasarkan_jenis_kelamin_responden/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<TableTd style={{ width: '15%', textAlign: 'center' }}>
<Button
color='red'
disabled={stategrafikBerdasarkanJenisKelamin.delete.loading}
@@ -130,16 +158,27 @@ function ListGrafikBerdasarkanJenisKelamin({ search }: { search: string }) {
))
)}
</TableTbody>
</Table>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
}}
total={totalPages}
mt="md"
mb="md"
/>
</Center>
{/* Chart */}
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<Title pb={10} order={3}>Grafik Berdasarkan Responden</Title>
{mounted && donutData.length > 0 ? (<Box style={{ width: '100%', height: 'auto', minHeight: 200 }}>
<Title pb={10} order={3}>Grafik Berdasarkan Jenis Kelamin Responden</Title>
{mounted && donutData.length === 0 ? (<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text>) : (<Box style={{ width: '100%', height: 'auto', minHeight: 200 }}>
<PieChart
width={800} height={300}
data={donutData}
@@ -168,8 +207,6 @@ function ListGrafikBerdasarkanJenisKelamin({ search }: { search: string }) {
<Text>Perempuan: {donutData.find((entry) => entry.name === 'perempuan')?.value}</Text>
</Flex>
</Box>
) : (
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text>
)}
</Stack>
</Paper>

View File

@@ -45,7 +45,7 @@ function GrafikBerdasarkanRespondenCreate() {
</Box>
<Paper bg={colors['white-1']} w={{ base: '100%', md: '50%' }} p={'md'}>
<Stack>
<Title order={3}>Grafik Hasil Kepuasan Masyarakat Terhadap Pelayanan Publik</Title>
<Title order={3}>Grafik Hasil Kepuasan Masyarakat Berdasarkan Responden</Title>
<TextInput
label="Sangat Baik"
type='number'

View File

@@ -1,17 +1,17 @@
'use client'
/* eslint-disable @typescript-eslint/no-explicit-any */
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { Box, Button, Center, Flex, Pagination, 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 { useSnapshot } from 'valtio';
import JudulListTab from '../../../_com/judulListTab';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import grafikBerdasarkanResponden from '../../../_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanResponden';
import HeaderSearch from '../../../_com/header';
function GrafikBerdasarkanResponden() {
const [search, setSearch] = useState("");
@@ -37,24 +37,14 @@ function ListGrafikBerdasarkanResponden({ search }: { search: string }) {
const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter();
const handleDelete = async () => {
if (selectedId) {
await stategrafikBerdasarkanResponden.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
// Refresh data agar chart & tabel ikut update
stategrafikBerdasarkanResponden.findMany.load();
}
}
const { data, page, totalPages, loading, load } = stategrafikBerdasarkanResponden.findMany
useShallowEffect(() => {
setMounted(true)
stategrafikBerdasarkanResponden.findMany.load()
}, [])
load(page, 10)
}, [page])
const filteredData = (stategrafikBerdasarkanResponden.findMany.data || []).filter(item => {
const filteredData = (data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.sangatbaik.toString().toLowerCase().includes(keyword) ||
@@ -65,11 +55,11 @@ function ListGrafikBerdasarkanResponden({ search }: { search: string }) {
});
useEffect(() => {
if (stategrafikBerdasarkanResponden.findMany.data) {
const totalSangatBaik = stategrafikBerdasarkanResponden.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.sangatbaik || 0), 0);
const totalBaik = stategrafikBerdasarkanResponden.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.baik || 0), 0);
const totalKurangBaik = stategrafikBerdasarkanResponden.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.kurangbaik || 0), 0);
const totalTidakBaik = stategrafikBerdasarkanResponden.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.tidakbaik || 0), 0);
if (data) {
const totalSangatBaik = data.reduce((acc: number, cur: any) => acc + Number(cur.sangatbaik || 0), 0);
const totalBaik = data.reduce((acc: number, cur: any) => acc + Number(cur.baik || 0), 0);
const totalKurangBaik = data.reduce((acc: number, cur: any) => acc + Number(cur.kurangbaik || 0), 0);
const totalTidakBaik = data.reduce((acc: number, cur: any) => acc + Number(cur.tidakbaik || 0), 0);
setDonutData([
{ name: 'sangatbaik', value: totalSangatBaik, color: colors['blue-button'], key: 'sangatbaik' },
{ name: 'baik', value: totalBaik, color: '#10A85AFF', key: 'baik' },
@@ -78,34 +68,72 @@ function ListGrafikBerdasarkanResponden({ search }: { search: string }) {
]);
}
}, [stategrafikBerdasarkanResponden.findMany.data])
}, [data])
if (!stategrafikBerdasarkanResponden.findMany.data) {
const handleDelete = async () => {
if (selectedId) {
await stategrafikBerdasarkanResponden.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
// Refresh data agar chart & tabel ikut update
stategrafikBerdasarkanResponden.findMany.load();
}
}
if (loading || !data) {
return (
<Box>
<Skeleton h={500} />
</Box>
)
<Stack py={10}>
<Skeleton height={730} />
</Stack>
);
}
if (data.length === 0) {
return (
<Box py={10}>
<Paper p="md" >
<Stack>
<JudulList
title='List Data Berdasarkan Responden'
href='/admin/ppid/ikm-desa-darmasaba/grafik_berdasarkan_responden/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Sangat Baik</TableTh>
<TableTh>Baik</TableTh>
<TableTh>Kurang Baik</TableTh>
<TableTh>Tidak Baik</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
</TableThead>
</Table>
<Text ta="center">Tidak ada data grafik berdasarkan responden yang tersedia</Text>
</Stack>
</Paper>
</Box >
);
}
return (
<Box>
<Stack gap={"xs"}>
<Paper bg={colors['white-1']} p={"md"}>
<JudulListTab
title='List Grafik Berdasarkan Pilihan Responden'
<Paper bg={colors['white-1']} p={"md"} h={{ base: 730, md: 650 }}>
<JudulList
title='List Data Berdasarkan Pilihan Responden'
href='/admin/ppid/ikm-desa-darmasaba/grafik_berdasarkan_responden/create'
placeholder='pencarian'
searchIcon={<IconSearch size={16} />}
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Sangat Baik</TableTh>
<TableTh>Baik</TableTh>
<TableTh>Kurang Baik</TableTh>
<TableTh>Tidak Baik</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
<TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Sangat Baik</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Baik</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Kurang Baik</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Tidak Baik</TableTh>
<TableTh style={{ width: '5%', textAlign: 'center' }}>Edit</TableTh>
<TableTh style={{ width: '5%', textAlign: 'center' }}>Delete</TableTh>
</TableTr>
</TableThead>
<TableTbody>
@@ -116,18 +144,19 @@ function ListGrafikBerdasarkanResponden({ search }: { search: string }) {
</TableTd>
</TableTr>
) : (
filteredData.map((item) => (
filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd>{item.sangatbaik}</TableTd>
<TableTd>{item.baik}</TableTd>
<TableTd>{item.kurangbaik}</TableTd>
<TableTd>{item.tidakbaik}</TableTd>
<TableTd>
<TableTd style={{ width: '5%', textAlign: 'center' }}>{index + 1}</TableTd>
<TableTd style={{ width: '5%', textAlign: 'center' }}>{item.sangatbaik}</TableTd>
<TableTd style={{ width: '15%', textAlign: 'center' }}>{item.baik}</TableTd>
<TableTd style={{ width: '15%', textAlign: 'center' }}>{item.kurangbaik}</TableTd>
<TableTd style={{ width: '15%', textAlign: 'center' }}>{item.tidakbaik}</TableTd>
<TableTd style={{ width: '5%', textAlign: 'center' }}>
<Button color='green' onClick={() => router.push(`/admin/ppid/ikm-desa-darmasaba/grafik_berdasarkan_responden/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<TableTd style={{ width: '5%', textAlign: 'center' }}>
<Button
color='red'
disabled={stategrafikBerdasarkanResponden.delete.loading}
@@ -145,12 +174,24 @@ function ListGrafikBerdasarkanResponden({ search }: { search: string }) {
</Table>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
}}
total={totalPages}
mt="md"
mb="md"
/>
</Center>
{/* Chart */}
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<Title pb={10} order={3}>Grafik Berdasarkan Responden</Title>
<Title pb={10} order={3}>Grafik Berdasarkan Pilihan Responden</Title>
{mounted && donutData.length > 0 ? (<Box style={{ width: '100%', height: 'auto', minHeight: 200 }}>
<PieChart
width={800} height={300}

View File

@@ -2,7 +2,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import grafikBerdasarkanUmur from '@/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanUmur';
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { Box, Button, Center, Flex, Pagination, 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';
@@ -12,6 +12,7 @@ import { useProxy } from 'valtio/utils';
import JudulListTab from '../../../_com/judulListTab';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
function GrafikBerdasarakanUmur() {
const [search, setSearch] = useState("");
@@ -36,6 +37,37 @@ function ListGrafikBerdasarakanUmur({ search }: { search: string }) {
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter()
const { data, page, totalPages, loading, load } = stategrafikBerdasarkanUmur.findMany
useShallowEffect(() => {
setMounted(true);
load(page, 10)
}, [page]);
useEffect(() => {
if (data) {
const totalRemaja = data.reduce((acc: number, cur: any) => acc + Number(cur.remaja || 0), 0);
const totalDewasa = data.reduce((acc: number, cur: any) => acc + Number(cur.dewasa || 0), 0);
const totalOrangtua = data.reduce((acc: number, cur: any) => acc + Number(cur.orangtua || 0), 0);
const totalLansia = data.reduce((acc: number, cur: any) => acc + Number(cur.lansia || 0), 0);
setDonutData([
{ name: 'remaja', value: totalRemaja, color: colors['blue-button'], key: 'remaja' },
{ name: 'dewasa', value: totalDewasa, color: '#D32711FF', key: 'dewasa' },
{ name: 'orangtua', value: totalOrangtua, color: '#B46B04FF', key: 'orangtua' },
{ name: 'lansia', value: totalLansia, color: '#038617FF', key: 'lansia' }
]);
}
}, [data])
const filteredData = (data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.remaja.toString().toLowerCase().includes(keyword) ||
item.dewasa.toString().toLowerCase().includes(keyword) ||
item.orangtua.toString().toLowerCase().includes(keyword) ||
item.lansia.toString().toLowerCase().includes(keyword)
);
});
const handleDelete = async () => {
if (selectedId) {
@@ -47,50 +79,48 @@ function ListGrafikBerdasarakanUmur({ search }: { search: string }) {
}
}
useShallowEffect(() => {
setMounted(true);
stategrafikBerdasarkanUmur.findMany.load()
}, []);
useEffect(() => {
if (stategrafikBerdasarkanUmur.findMany.data) {
const totalRemaja = stategrafikBerdasarkanUmur.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.remaja || 0), 0);
const totalDewasa = stategrafikBerdasarkanUmur.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.dewasa || 0), 0);
const totalOrangtua = stategrafikBerdasarkanUmur.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.orangtua || 0), 0);
const totalLansia = stategrafikBerdasarkanUmur.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.lansia || 0), 0);
setDonutData([
{ name: 'remaja', value: totalRemaja, color: colors['blue-button'], key: 'remaja' },
{ name: 'dewasa', value: totalDewasa, color: '#D32711FF', key: 'dewasa' },
{ name: 'orangtua', value: totalOrangtua, color: '#B46B04FF', key: 'orangtua' },
{ name: 'lansia', value: totalLansia, color: '#038617FF', key: 'lansia' }
]);
}
}, [stategrafikBerdasarkanUmur.findMany.data])
const filteredData = (stategrafikBerdasarkanUmur.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
if (loading || !data) {
return (
item.remaja.toString().toLowerCase().includes(keyword) ||
item.dewasa.toString().toLowerCase().includes(keyword) ||
item.orangtua.toString().toLowerCase().includes(keyword) ||
item.lansia.toString().toLowerCase().includes(keyword)
<Stack py={10}>
<Skeleton height={730} />
</Stack>
);
});
if (!stategrafikBerdasarkanUmur.findMany.data) {
}
if (data.length === 0) {
return (
<Box>
<Skeleton h={500} />
</Box>
)
<Box py={10}>
<Paper p="md" >
<Stack>
<JudulList
title='List Data Berdasarkan Umur Responden'
href='/admin/ppid/ikm-desa-darmasaba/grafik_berdasarkan_umur/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Remaja</TableTh>
<TableTh>Dewasa</TableTh>
<TableTh>Orangtua</TableTh>
<TableTh>Lansia</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
</TableThead>
</Table>
<Text ta="center">Tidak ada data grafik berdasarkan umur responden yang tersedia</Text>
</Stack>
</Paper>
</Box >
);
}
return (
<Box>
<Stack gap={"xs"}>
<Paper bg={colors['white-1']} p={"md"}>
<Paper bg={colors['white-1']} p={"md"} h={{ base: 730, md: 650 }}>
<JudulListTab
title='List Grafik Berdasarkan Umur Responden'
title='List Data Berdasarkan Umur Responden'
href='/admin/ppid/ikm-desa-darmasaba/grafik_berdasarkan_umur/create'
placeholder='pencarian'
searchIcon={<IconSearch size={16} />}
@@ -98,12 +128,13 @@ function ListGrafikBerdasarakanUmur({ search }: { search: string }) {
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Remaja</TableTh>
<TableTh>Dewasa</TableTh>
<TableTh>Orangtua</TableTh>
<TableTh>Lansia</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
<TableTh style={{ width: '2%', textAlign: 'center' }}>No</TableTh>
<TableTh style={{ width: '5%', textAlign: 'center' }}>Remaja</TableTh>
<TableTh style={{ width: '5%', textAlign: 'center' }}>Dewasa</TableTh>
<TableTh style={{ width: '5%', textAlign: 'center' }}>Orangtua</TableTh>
<TableTh style={{ width: '5%', textAlign: 'center' }}>Lansia</TableTh>
<TableTh style={{ width: '5%', textAlign: 'center' }}>Edit</TableTh>
<TableTh style={{ width: '5%', textAlign: 'center' }}>Delete</TableTh>
</TableTr>
</TableThead>
<TableTbody>
@@ -116,16 +147,17 @@ function ListGrafikBerdasarakanUmur({ search }: { search: string }) {
) : (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.remaja}</TableTd>
<TableTd>{item.dewasa}</TableTd>
<TableTd>{item.orangtua}</TableTd>
<TableTd>{item.lansia}</TableTd>
<TableTd>
<TableTd style={{ textAlign: 'center' }}>{filteredData.indexOf(item) + 1}</TableTd>
<TableTd style={{ textAlign: 'center' }}>{item.remaja}</TableTd>
<TableTd style={{ textAlign: 'center' }}>{item.dewasa}</TableTd>
<TableTd style={{ textAlign: 'center' }}>{item.orangtua}</TableTd>
<TableTd style={{ textAlign: 'center' }}>{item.lansia}</TableTd>
<TableTd style={{ textAlign: 'center' }}>
<Button color='green' onClick={() => router.push(`/admin/ppid/ikm-desa-darmasaba/grafik_berdasarkan_umur/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<TableTd style={{ textAlign: 'center' }}>
<Button
color='red'
disabled={stategrafikBerdasarkanUmur.delete.loading}
@@ -143,12 +175,24 @@ function ListGrafikBerdasarakanUmur({ search }: { search: string }) {
</Table>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
}}
total={totalPages}
mt="md"
mb="md"
/>
</Center>
{/* Chart */}
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<Title pb={10} order={3}>Grafik Berdasarkan Responden</Title>
<Title pb={10} order={3}>Grafik Umur Berdasarkan Responden</Title>
{mounted && donutData.length > 0 ? (<Box style={{ width: '100%', height: 'auto', minHeight: 200 }}>
<PieChart
width={800} height={300}

View File

@@ -1,16 +1,16 @@
'use client'
import JudulListTab from '@/app/admin/(dashboard)/_com/judulListTab';
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, 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 { useSnapshot } from 'valtio';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import grafikHasilKepuasanMasyarakat from '../../../_state/ppid/indeks_kepuasan_masyarakat/grafikHasilKepuasan';
import HeaderSearch from '../../../_com/header';
function GrafikHasilKepuasanMasyarakat() {
@@ -46,8 +46,34 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter();
const { data, page, totalPages, loading, load } = stateGrafikHasilKepuasan.findMany
useShallowEffect(() => {
setMounted(true)
load(page, 10)
}, [page])
useEffect(() => {
if (data) {
setChartData(
data.map((item) => ({
id: item.id,
label: item.label,
kepuasan: Number(item.kepuasan),
}))
);
}
}, [data]);
const filteredData = (data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.label.toLowerCase().includes(keyword) ||
item.kepuasan.toString().toLowerCase().includes(keyword)
);
});
const handleDelete = () => {
if (selectedId) {
stateGrafikHasilKepuasan.delete.byId(selectedId)
@@ -58,70 +84,69 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
}
}
useShallowEffect(() => {
setMounted(true)
stateGrafikHasilKepuasan.findMany.load()
}, [])
useEffect(() => {
if (stateGrafikHasilKepuasan.findMany.data) {
setChartData(
stateGrafikHasilKepuasan.findMany.data.map((item) => ({
id: item.id,
label: item.label,
kepuasan: Number(item.kepuasan),
}))
);
}
}, [stateGrafikHasilKepuasan.findMany.data]);
const filteredData = (stateGrafikHasilKepuasan.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
if (loading || !data) {
return (
item.label.toLowerCase().includes(keyword) ||
item.kepuasan.toString().toLowerCase().includes(keyword)
);
});
if (!stateGrafikHasilKepuasan.findMany.data) {
return (
<Stack>
<Skeleton h={500} />
<Stack py={10}>
<Skeleton height={730} />
</Stack>
)
);
}
if (data.length === 0) {
return (
<Box py={10}>
<Paper p="md" >
<Stack>
<JudulList
title='List Data Hasil Kepuasan Masyarakat'
href='/admin/ppid/ikm-desa-darmasaba/grafik_hasil_kepuasan_masyarakat/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Label</TableTh>
<TableTh>Jumlah Kepuasan</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
</TableThead>
</Table>
<Text ta="center">Tidak ada data grafik hasil kepuasan masyarakat yang tersedia</Text>
</Stack>
</Paper>
</Box >
);
}
return (
<Box>
<Stack gap={"xs"}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulListTab
title='List Grafik Hasil Kepuasan Masyarakat'
<Paper bg={colors['white-1']} p={'md'} h={{ base: 730, md: 650 }}>
<JudulList
title='List Data Hasil Kepuasan Masyarakat'
href='/admin/ppid/ikm-desa-darmasaba/grafik_hasil_kepuasan_masyarakat/create'
placeholder='pencarian'
searchIcon={<IconSearch size={16} />}
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Label</TableTh>
<TableTh>Jumlah Kepuasan</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
<TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh>
<TableTh style={{ width: '20%', textAlign: 'center' }}>Label</TableTh>
<TableTh style={{ width: '20%', textAlign: 'center' }}>Jumlah Kepuasan</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Edit</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Delete</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
{filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd>{item.label}</TableTd>
<TableTd>{item.kepuasan}</TableTd>
<TableTd>
<TableTd style={{ textAlign: 'center' }}>{index + 1}</TableTd>
<TableTd style={{ textAlign: 'center' }}>{item.label}</TableTd>
<TableTd style={{ textAlign: 'center' }}>{item.kepuasan}</TableTd>
<TableTd style={{ textAlign: 'center' }}>
<Button color='green' onClick={() => router.push(`/admin/ppid/ikm-desa-darmasaba/grafik_hasil_kepuasan_masyarakat/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<TableTd style={{ textAlign: 'center' }}>
<Button
color='red'
disabled={stateGrafikHasilKepuasan.delete.loading}
@@ -137,12 +162,24 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
</TableTbody>
</Table>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
}}
total={totalPages}
mt="md"
mb="md"
/>
</Center>
{/* Chart */}
<Box style={{ width: '100%', minWidth: 300, height: 500, minHeight: 300 }}>
<Paper style={{ width: '100%', minWidth: 300, height: 500, minHeight: 300 }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title pb={10} order={3}>Data Kepuasan Masyarakat</Title>
<Title pb={10} order={3}>Grafik Hasil Kepuasan Masyarakat</Title>
{mounted && chartData.length > 0 ? (
<BarChart width={isMobile ? 300 : isTablet ? 300 : 300} height={380} data={chartData} >
<XAxis dataKey="label" />

View File

@@ -276,7 +276,7 @@ export const navBar = [
{
id: "Inovasi_2",
name: "Layanan Online Desa",
path: "/admin/inovasi/layanan-online-desa"
path: "/admin/inovasi/layanan-online-desa/administrasi-online"
},
{
id: "Inovasi_3",
@@ -308,7 +308,7 @@ export const navBar = [
{
id: "Lingkungan_1",
name: "Pengelolaan Sampah (Bank Sampah)",
path: "/admin/lingkungan/pengelolaan-sampah-bank-sampah"
path: "/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah"
},
{
id: "Lingkungan_2",

View File

@@ -0,0 +1,30 @@
import prisma from '@/lib/prisma';
export default async function beritaFindFirst() {
try {
const result = await prisma.berita.findFirst({
where: {
isActive: true, // opsional kalau kamu punya field ini
},
orderBy: {
createdAt: 'desc', // ambil yang paling terbaru
},
include: {
image: true,
kategoriBerita: true,
}
});
return {
success: true,
message: 'Berhasil ambil berita terbaru',
data: result,
};
} catch (error) {
console.error('[findFirstBerita] Error:', error);
return {
success: false,
message: 'Gagal ambil berita terbaru',
};
}
}

View File

@@ -0,0 +1,19 @@
import prisma from "@/lib/prisma";
export default async function findRecentBerita() {
const result = await prisma.berita.findMany({
orderBy: {
createdAt: "desc",
},
take: 4, // ambil 4 data terbaru
include: {
image: true,
kategoriBerita: true,
},
});
return {
success: true,
data: result,
};
}

View File

@@ -5,6 +5,8 @@ import beritaCreate from "./create";
import beritaDelete from "./del";
import beritaUpdate from "./updt";
import findBeritaById from "./find-by-id";
import beritaFindFirst from "./findFirst";
import findRecentBerita from "./findRecent";
const Berita = new Elysia({ prefix: "/berita", tags: ["Desa/Berita"] })
.get("/category/find-many", kategoriBeritaFindMany)
@@ -22,6 +24,8 @@ const Berita = new Elysia({ prefix: "/berita", tags: ["Desa/Berita"] })
kategoriBeritaId: t.Union([t.String(), t.Null()]),
}),
})
.get("/find-first", beritaFindFirst)
.get("/find-recent", findRecentBerita)
.delete("/delete/:id", beritaDelete)
.put(
"/:id",
@@ -39,5 +43,6 @@ const Berita = new Elysia({ prefix: "/berita", tags: ["Desa/Berita"] })
}),
}
);
export default Berita;

View File

@@ -0,0 +1,29 @@
import prisma from '@/lib/prisma';
export default async function pengumumanFindFirst() {
try {
const result = await prisma.pengumuman.findFirst({
where: {
isActive: true, // opsional kalau kamu punya field ini
},
orderBy: {
createdAt: 'desc', // ambil yang paling terbaru
},
include: {
CategoryPengumuman: true,
}
});
return {
success: true,
message: 'Berhasil ambil pengumuman terbaru',
data: result,
};
} catch (error) {
console.error('[findFirstPengumuman] Error:', error);
return {
success: false,
message: 'Gagal ambil pengumuman terbaru',
};
}
}

View File

@@ -0,0 +1,18 @@
import prisma from "@/lib/prisma";
export default async function pengumumanFindRecent() {
const result = await prisma.pengumuman.findMany({
orderBy: {
createdAt: "desc",
},
take: 4, // ambil 4 data terbaru
include: {
CategoryPengumuman: true,
},
});
return {
success: true,
data: result,
};
}

View File

@@ -6,6 +6,8 @@ import pengumumanCategoryFindMany from "./category";
import pengumumanDelete from "./del";
import pengumumanFindById from "./find-by-id";
import pengumumanUpdate from "./updt";
import pengumumanFindFirst from "./findFirst";
import pengumumanFindRecent from "./findRecent";
const Pengumuman = new Elysia({ prefix: "/pengumuman", tags: ["Desa/Pengumuman"] })
.get("/category/find-many", pengumumanCategoryFindMany)
@@ -20,6 +22,8 @@ const Pengumuman = new Elysia({ prefix: "/pengumuman", tags: ["Desa/Pengumuman"]
categoryPengumumanId: t.Union([t.String(), t.Null()]),
}),
})
.get("/find-first", pengumumanFindFirst)
.get("/find-recent", pengumumanFindRecent)
.put("/:id", pengumumanUpdate, {
body: t.Object({
id: t.String(),

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