Compare commits

..

27 Commits

Author SHA1 Message Date
5c66eccf23 Fix Menu Ekonomi :
Pasar Desa : Kategorinya ga tampil,
Bug inputan edit di submenu : Demografi pekerjaa
2025-10-04 21:34:31 +08:00
f7fd9be255 QC User & Admin Responsive : Menu Kesehatan - Ekonomi 2025-10-03 10:17:06 +08:00
8a6d8ed8db QC User & Admin Responsive : Menu Landing Page - Desa 2025-10-02 00:10:33 +08:00
63054cedf0 fix inputan edit menu: desa, ekonomi, inovasi, keamanan, kesehatan, landing-page, & lingkungan 2025-09-30 21:41:26 +08:00
c2f1ab8179 Fix Menu Desa Admin & User 2025-09-30 17:13:06 +08:00
295d6f7d63 Fix tampilan admin pertama kali 2025-09-29 14:33:25 +08:00
dbd56a1493 Fix All Text Input User & Admin, fix deskripsi detail break word 2025-09-29 14:06:04 +08:00
2a26db6e17 Tambahan Fix Form Di Menu Landing Page & PPID IKM 2025-09-25 16:21:44 +08:00
33fc472472 Fix Tampilan Mobile Penghargaan Landing Page 2025-09-25 11:41:43 +08:00
d8fa56d923 Tambahan fix menu prestasi desa 2025-09-25 11:14:38 +08:00
cac146471a Fix UI Mobile User & Admin Menu Kesehatan, QC Menu Kesehatan 2025-09-25 10:40:47 +08:00
3e4a7a1c0a Fix Ui Admin & User to Mobile && QC Menu Landing Page, PPID, Desa 2025-09-24 14:50:53 +08:00
b5c044df6e Fix Tampilan User & Admin Menu Inovasi & Lingkungan 2025-09-22 17:15:11 +08:00
0fc47c28ff fix tampilan admin menu inovasi, sisa menu lingkungan 2025-09-22 10:53:48 +08:00
8e25c91e85 Fix Tampilab DesaAntiKorupsi Landing Page Mobile 2025-09-20 03:49:20 +08:00
068d8b1077 Fix All Image Add Lazy Loading 2025-09-19 10:41:18 +08:00
9f72e94557 Fix Admin - User Menu Keamanan, Submenu Pencegahan Kriminalitas 2025-09-17 17:54:03 +08:00
79ad39fc55 Fix Admin - User Menu Keamanan, Submenu Laporan Kontak Darurat, Laporan Publik 2025-09-17 14:59:46 +08:00
39e1e7b575 QC Tampilan Admin & User, Api berfungsi 2025-09-16 16:47:12 +08:00
4ceea5203f QC Admin - User Menu Ekonomi : Jumlah Pengangguran 2025-09-16 10:11:54 +08:00
a5d841bb6b Fix Compres Gambar && seed gambar profile - landing page 2025-09-12 11:55:40 +08:00
6a7bd386ae Add Layout Kontak Darurat - Admin Menu Keamanan 2025-09-11 12:15:40 +08:00
a9d98895bb Fix Admin Menu SDGs Desa & APBdes Desa, Fix UI IMage Layanan Landing Page & Layanan Desa 2025-09-09 17:14:28 +08:00
75475dc62e Fix Package.json Bun 2025-09-08 21:52:17 +08:00
b39800a475 Fix UI Admin Keamanan Lingkungan 2025-09-08 15:45:56 +08:00
797713ef49 Fix UI Admin Menu Kesehatan, Login Admin, OTP 2025-09-08 14:02:21 +08:00
8817b937b1 API Auth 2025-09-04 11:46:08 +08:00
743 changed files with 37797 additions and 21520 deletions

5
.gitignore vendored
View File

@@ -41,6 +41,9 @@ next-env.d.ts
# uploads # uploads
/uploads /uploads
# download
/download
# cache # cache
/cache /cache
@@ -48,3 +51,5 @@ next-env.d.ts
.env.* .env.*
*.tar.gz

BIN
bun.lockb

Binary file not shown.

View File

@@ -1,6 +1,11 @@
import type { NextConfig } from "next"; import type { NextConfig } from "next";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
experimental: {},
allowedDevOrigins: [
"http://192.168.1.82:3000", // buat akses dari HP/device lain
"http://localhost:3000", // akses lokal
],
async headers() { async headers() {
return [ return [
{ {

View File

@@ -3,11 +3,9 @@
"version": "0.1.5", "version": "0.1.5",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev --turbopack", "dev": "bun --bun next dev --hostname 0.0.0.0",
"build": "next build", "build": "bun --bun next build",
"start": "next start", "start": "bun --bun next start"
"lint": "next lint",
"prisma:seed": "bun run prisma/seed.ts"
}, },
"prisma": { "prisma": {
"seed": "bun run prisma/seed.ts" "seed": "bun run prisma/seed.ts"
@@ -41,22 +39,28 @@
"@tiptap/pm": "^2.11.7", "@tiptap/pm": "^2.11.7",
"@tiptap/react": "^2.11.7", "@tiptap/react": "^2.11.7",
"@tiptap/starter-kit": "^2.11.7", "@tiptap/starter-kit": "^2.11.7",
"@types/adm-zip": "^0.5.7",
"@types/bun": "^1.2.2", "@types/bun": "^1.2.2",
"@types/leaflet": "^1.9.20", "@types/leaflet": "^1.9.20",
"@types/lodash": "^4.17.16", "@types/lodash": "^4.17.16",
"add": "^2.0.6", "add": "^2.0.6",
"adm-zip": "^0.5.16",
"animate.css": "^4.1.1", "animate.css": "^4.1.1",
"bcryptjs": "^3.0.2", "bcryptjs": "^3.0.2",
"bun": "^1.2.2", "bun": "^1.2.2",
"chart.js": "^4.4.8", "chart.js": "^4.4.8",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"colors": "^1.4.0",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"elysia": "^1.3.5", "elysia": "^1.3.5",
"embla-carousel-autoplay": "^8.5.2", "embla-carousel-autoplay": "^8.5.2",
"embla-carousel-react": "^7.1.0", "embla-carousel-react": "^7.1.0",
"extract-zip": "^2.0.1",
"form-data": "^4.0.2", "form-data": "^4.0.2",
"framer-motion": "^12.23.5", "framer-motion": "^12.23.5",
"get-port": "^7.1.0", "get-port": "^7.1.0",
"iron-session": "^8.0.4",
"jose": "^6.1.0",
"jotai": "^2.12.3", "jotai": "^2.12.3",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
@@ -64,7 +68,7 @@
"lodash": "^4.17.21", "lodash": "^4.17.21",
"motion": "^12.4.1", "motion": "^12.4.1",
"nanoid": "^5.1.5", "nanoid": "^5.1.5",
"next": "15.1.6", "next": "^15.5.2",
"next-view-transitions": "^0.3.4", "next-view-transitions": "^0.3.4",
"node-fetch": "^3.3.2", "node-fetch": "^3.3.2",
"p-limit": "^6.2.0", "p-limit": "^6.2.0",
@@ -73,15 +77,18 @@
"prisma": "^6.3.1", "prisma": "^6.3.1",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-international-phone": "^4.6.0",
"react-leaflet": "^5.0.0", "react-leaflet": "^5.0.0",
"react-simple-toasts": "^6.1.0", "react-simple-toasts": "^6.1.0",
"react-toastify": "^11.0.5", "react-toastify": "^11.0.5",
"react-transition-group": "^4.4.5", "react-transition-group": "^4.4.5",
"readdirp": "^4.1.1", "readdirp": "^4.1.1",
"recharts": "^2.15.3", "recharts": "^2.15.3",
"sharp": "^0.34.3",
"swr": "^2.3.2", "swr": "^2.3.2",
"uuid": "^11.1.0", "uuid": "^11.1.0",
"valtio": "^2.1.3", "valtio": "^2.1.3",
"zlib": "^1.0.5",
"zod": "^3.24.3" "zod": "^3.24.3"
}, },
"devDependencies": { "devDependencies": {

View File

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

View File

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

View File

@@ -1,51 +1,99 @@
[ [
{ {
"month": "Jan", "month": "Jan",
"year": 2025, "year": 2025,
"totalUnemployment": 160, "totalUnemployment": 160,
"educatedUnemployment": 95, "educatedUnemployment": 95,
"uneducatedUnemployment": 65, "uneducatedUnemployment": 65,
"percentageChange": null "percentageChange": 0.0
}, },
{ {
"month": "Feb", "month": "Feb",
"year": 2025, "year": 2025,
"totalUnemployment": 155, "totalUnemployment": 158,
"educatedUnemployment": 90, "educatedUnemployment": 93,
"uneducatedUnemployment": 65, "uneducatedUnemployment": 65,
"percentageChange": -3.1 "percentageChange": -1.25
}, },
{ {
"month": "Mar", "month": "Mar",
"year": 2025, "year": 2025,
"totalUnemployment": 150, "totalUnemployment": 155,
"educatedUnemployment": 88, "educatedUnemployment": 91,
"uneducatedUnemployment": 62, "uneducatedUnemployment": 64,
"percentageChange": -3.2 "percentageChange": -1.90
}, },
{ {
"month": "Apr", "month": "Apr",
"year": 2025, "year": 2025,
"totalUnemployment": 148, "totalUnemployment": 152,
"educatedUnemployment": 85, "educatedUnemployment": 89,
"uneducatedUnemployment": 63, "uneducatedUnemployment": 63,
"percentageChange": -1.3 "percentageChange": -1.94
}, },
{ {
"month": "Mei", "month": "Mei",
"year": 2025, "year": 2025,
"totalUnemployment": 145, "totalUnemployment": 150,
"educatedUnemployment": 82, "educatedUnemployment": 88,
"uneducatedUnemployment": 63, "uneducatedUnemployment": 62,
"percentageChange": -2.0 "percentageChange": -1.32
}, },
{ {
"month": "Jun", "month": "Jun",
"year": 2025, "year": 2025,
"totalUnemployment": 140, "totalUnemployment": 148,
"educatedUnemployment": 80, "educatedUnemployment": 87,
"uneducatedUnemployment": 60, "uneducatedUnemployment": 61,
"percentageChange": -3.4 "percentageChange": -1.33
} },
] {
"month": "Jul",
"year": 2025,
"totalUnemployment": 145,
"educatedUnemployment": 85,
"uneducatedUnemployment": 60,
"percentageChange": -2.03
},
{
"month": "Agu",
"year": 2025,
"totalUnemployment": 142,
"educatedUnemployment": 84,
"uneducatedUnemployment": 58,
"percentageChange": -2.07
},
{
"month": "Sep",
"year": 2025,
"totalUnemployment": 140,
"educatedUnemployment": 83,
"uneducatedUnemployment": 57,
"percentageChange": -1.41
},
{
"month": "Okt",
"year": 2025,
"totalUnemployment": 138,
"educatedUnemployment": 82,
"uneducatedUnemployment": 56,
"percentageChange": -1.43
},
{
"month": "Nov",
"year": 2025,
"totalUnemployment": 135,
"educatedUnemployment": 80,
"uneducatedUnemployment": 55,
"percentageChange": -2.17
},
{
"month": "Des",
"year": 2025,
"totalUnemployment": 132,
"educatedUnemployment": 78,
"uneducatedUnemployment": 54,
"percentageChange": -2.22
}
]

View File

@@ -0,0 +1,137 @@
[
{
"id": "cmff0rr4z0002vn0twp333m2",
"name": "S6RIjFaPvdQm3oq4rM4X9-desktop.webp",
"realName": "bares.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/S6RIjFaPvdQm3oq4rM4X9-desktop.webp",
"category": "image"
},
{
"id": "cmff0tnf00003vn0t3kgzi0u0",
"name": "_pVNEmThU5ICGa8gv3gh_-desktop.webp",
"realName": "bicara-darma.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/_pVNEmThU5ICGa8gv3gh_-desktop.webp",
"category": "image"
},
{
"id": "cmff0uykf0004vn0trmmxpgfh",
"name": "bv6rdKvjxkkjUSGLQ0lvB-desktop.webp",
"realName": "daves.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/bv6rdKvjxkkjUSGLQ0lvB-desktop.webp",
"category": "image"
},
{
"id": "cmff0z34f0005vn0tjtvq519p",
"name": "Z4hWaV04CvoE20MjccQsV-desktop.webp",
"realName": "mangan.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/Z4hWaV04CvoE20MjccQsV-desktop.webp",
"category": "image"
},
{
"id": "cmff38cyq000bvn0t9f01cz3f",
"name": "LvLAtOqWojx4sn6NjJWB9-desktop.webp",
"realName": "gelah-melah.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/LvLAtOqWojx4sn6NjJWB9-desktop.webp",
"category": "image"
},
{
"id": "cmff0zqvd0007vn0tv6o5hjcq",
"name": "gR2mcvAQVgJ2-rM5coYJj-desktop.webp",
"realName": "inovasi-desa-darmasaba.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/gR2mcvAQVgJ2-rM5coYJj-desktop.webp",
"category": "image"
},
{
"id": "cmff1013m0008vn0th7t0d64d",
"name": "JpL-9F8-IGztMn8E2ce02-desktop.webp",
"realName": "pdkt.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/JpL-9F8-IGztMn8E2ce02-desktop.webp",
"category": "image"
},
{
"id": "cmff10cwq0009vn0tse8dzu3j",
"name": "bxAk4AsGbJTC705_IVdes-desktop.webp",
"realName": "sajjiana-dharma-raksaka.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/bxAk4AsGbJTC705_IVdes-desktop.webp",
"category": "image"
},
{
"id": "cmff2w5ly000avn0telhct71k",
"name": "Vbj_osnMJUkGEQGDTLwV--desktop.webp",
"realName": "perbekel.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/Vbj_osnMJUkGEQGDTLwV--desktop.webp",
"category": "image"
},
{
"id": "cmff3joae0000vn6h8sgs0ilg",
"name": "7hox9spUxj56hY_EBYLnj-desktop.webp",
"realName": "youtube.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/7hox9spUxj56hY_EBYLnj-desktop.webp",
"category": "image"
},
{
"id": "cmff3ll130001vn6hkhls3f5y",
"name": "ChihV7_1eS-AGtSg9UwMv-desktop.webp",
"realName": "gmail.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/ChihV7_1eS-AGtSg9UwMv-desktop.webp",
"category": "image"
},
{
"id": "cmff3mtat0002vn6hs8vyyhdd",
"name": "z8v9ZREwOJHKGIRYauROt-desktop.webp",
"realName": "facebook.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/z8v9ZREwOJHKGIRYauROt-desktop.webp",
"category": "image"
},
{
"id": "cmff3nv180003vn6h5jvedidq",
"name": "BLjMxTKoCNE31uOURR3IU-desktop.webp",
"realName": "telephone-call.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/BLjMxTKoCNE31uOURR3IU-desktop.webp",
"category": "image"
},
{
"id": "cmff3oouh0004vn6hd94brzv9",
"name": "hkJYAeTNWK_vYaYS20w3I-desktop.webp",
"realName": "instagram.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/hkJYAeTNWK_vYaYS20w3I-desktop.webp",
"category": "image"
},
{
"id": "cmff3q12g0005vn6h5ojov2qa",
"name": "6XEoZ9SFu59COpil03Gya-desktop.webp",
"realName": "tiktok.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/6XEoZ9SFu59COpil03Gya-desktop.webp",
"category": "image"
}
]

View File

@@ -2,31 +2,37 @@
{ {
"id": "cmds8w2q60002vnbe6i8qhkuo", "id": "cmds8w2q60002vnbe6i8qhkuo",
"name": "Telephone Desa Darmasaba", "name": "Telephone Desa Darmasaba",
"iconUrl": "081239580000" "iconUrl": "081239580000",
"imageId": "cmff3nv180003vn6h5jvedidq"
}, },
{ {
"id": "cmds8z7u20005vnbegyyvnbk0", "id": "cmds8z7u20005vnbegyyvnbk0",
"name": "Email Desa Darmasaba", "name": "Email Desa Darmasaba",
"iconUrl": "desadarmasaba@badungkab.go.id" "iconUrl": "desadarmasaba@badungkab.go.id",
"imageId": "cmff3ll130001vn6hkhls3f5y"
}, },
{ {
"id": "cmds9023u0008vnbe3oxmhwyf", "id": "cmds9023u0008vnbe3oxmhwyf",
"name": "Desa Darmasaba", "name": "Desa Darmasaba",
"iconUrl": "https://www.youtube.com/channel/UCtPw9WOQO7d2HIKzKgel4Xg" "iconUrl": "https://www.youtube.com/channel/UCtPw9WOQO7d2HIKzKgel4Xg",
"imageId": "cmff3joae0000vn6h8sgs0ilg"
}, },
{ {
"id": "cmds90oul000bvnbe2bqkptoi", "id": "cmds90oul000bvnbe2bqkptoi",
"name": "Pemerintah Desa Darmasaba", "name": "Pemerintah Desa Darmasaba",
"iconUrl": "https://www.facebook.com/DarmasabaDesaku" "iconUrl": "https://www.facebook.com/DarmasabaDesaku",
"imageId": "cmff3mtat0002vn6hs8vyyhdd"
}, },
{ {
"id": "cmds91i4e000evnbe8gtf1gub", "id": "cmds91i4e000evnbe8gtf1gub",
"name": "ddarmasaba", "name": "ddarmasaba",
"iconUrl": "https://www.instagram.com/ddarmasaba/" "iconUrl": "https://www.instagram.com/ddarmasaba/",
"imageId": "cmff3oouh0004vn6hd94brzv9"
}, },
{ {
"id": "cmds92de5000hvnbemlu6sq5x", "id": "cmds92de5000hvnbemlu6sq5x",
"name": "desa.darmasaba", "name": "desa.darmasaba",
"iconUrl": "https://www.tiktok.com/@desa.darmasaba?is_from_webapp=1&sender_device=pc" "iconUrl": "https://www.tiktok.com/@desa.darmasaba?is_from_webapp=1&sender_device=pc",
"imageId": "cmff3q12g0005vn6h5ojov2qa"
} }
] ]

View File

@@ -2,6 +2,7 @@
{ {
"id": "edit", "id": "edit",
"name": "I.B Surya Prabhawa Manuaba, S.H., M.H.", "name": "I.B Surya Prabhawa Manuaba, S.H., M.H.",
"position": "Perbekel Darmasaba periode 2021-2027" "position": "Perbekel Darmasaba periode 2021-2027",
"imageId": "cmff2w5ly000avn0telhct71k"
} }
] ]

View File

@@ -1,50 +1,51 @@
[ [
{
"id": "cmdr7039z0002vn5rttctt9hn",
"name": "Davest",
"description": "Darmasaba Village Festval",
"link": "https://darmasaba.desa.id/berita/55862-rakor-davest-2024"
},
{ {
"id": "cmdr755pf0005vn5rp8tyuubw", "id": "cmdr755pf0005vn5rp8tyuubw",
"name": "Dmangan", "name": "Dmangan",
"description": "Darmasaba Aman Pangan", "description": "Darmasaba Aman Pangan",
"link": "https://darmasaba.desa.id/berita/61452-kader-d-mangan-berhasil-meraih-prestasi-dalam-ajang-lomba-banjar-bali-quis-bbq-tahun-2024" "link": "https://darmasaba.desa.id/berita/61452-kader-d-mangan-berhasil-meraih-prestasi-dalam-ajang-lomba-banjar-bali-quis-bbq-tahun-2024",
"imageId" : "cmff0z34f0005vn0tjtvq519p"
}, },
{ {
"id": "cmdr76nqk0008vn5rdddvcxnr", "id": "cmdr76nqk0008vn5rdddvcxnr",
"name": "Bicara Darmasaba", "name": "Bicara Darmasaba",
"description": "Bicara Darmasaba", "description": "Bicara Darmasaba",
"link": "https://darmasaba.desa.id/berita/42506-bicara-darmasaba" "link": "https://darmasaba.desa.id/berita/42506-bicara-darmasaba",
"imageId" : "cmff0tnf00003vn0t3kgzi0u0"
}, },
{ {
"id": "cmdr77vbw000bvn5rvpmoq31s", "id": "cmdr77vbw000bvn5rvpmoq31s",
"name": "Bares", "name": "Bares",
"description": "Darmasaba Recycling Stock/Exchange", "description": "Darmasaba Recycling Stock/Exchange",
"link": "http://darmasaba.desa.id/berita/56722-bares" "link": "http://darmasaba.desa.id/berita/56722-bares",
"imageId" : "cmff0rr4z0002vn0twp333m2"
}, },
{ {
"id": "cmdr7bxtp000evn5rmy85wihx", "id": "cmdr7bxtp000evn5rmy85wihx",
"name": "Sajjana Dharma Raksaka", "name": "Sajjana Dharma Raksaka",
"description": "Sajjana Dharma Raksaka", "description": "Sajjana Dharma Raksaka",
"link": "https://ppid.badungkab.go.id/storage/dokumen/5RS9dldGkrgzMQq6bKdZsqsVRHI8gffWv4PGfb3r.pdf" "link": "https://ppid.badungkab.go.id/storage/dokumen/5RS9dldGkrgzMQq6bKdZsqsVRHI8gffWv4PGfb3r.pdf",
"imageId" : "cmff10cwq0009vn0tse8dzu3j"
}, },
{ {
"id": "cmdr7dlnk000hvn5r9lur3z35", "id": "cmdr7dlnk000hvn5r9lur3z35",
"name": "PDKT", "name": "PDKT",
"description": "Perangkat Desa Kuat Teknologi", "description": "Perangkat Desa Kuat Teknologi",
"link": "https://darmasaba.desa.id/berita/53752-p-d-k-t" "link": "https://darmasaba.desa.id/berita/53752-p-d-k-t",
"imageId" : "cmff1013m0008vn0th7t0d64d"
}, },
{ {
"id": "cmdr7ftob000mvn5rfhgdtg8v", "id": "cmdr7ftob000mvn5rfhgdtg8v",
"name": "GM", "name": "GM",
"description": "Galah Melah", "description": "Galah Melah",
"link": "https://darmasaba.desa.id/berita/52880-galah-melah" "link": "https://darmasaba.desa.id/berita/52880-galah-melah",
"imageId" : "cmff38cyq000bvn0t9f01cz3f"
}, },
{ {
"id": "cmdr7glue000pvn5r6onzslju", "id": "cmdr7glue000pvn5r6onzslju",
"name": "Inovasi Desa Darmasaba", "name": "Inovasi Desa Darmasaba",
"description": "Inovasi Desa Darmasaba", "description": "Inovasi Desa Darmasaba",
"link": "https://darmasaba.desa.id/produk-lokal-desa" "link": "https://darmasaba.desa.id/produk-lokal-desa",
"imageId" : "cmff0zqvd0007vn0tv6o5hjcq"
} }
] ]

View File

@@ -1,29 +1,23 @@
[ [
{ {
"id": "role_admin_desa", "id": "role-1",
"name": "ADMIN_DESA", "name": "ADMIN DESA",
"description": "Administrator Desa", "description": "Administrator Desa",
"permissions": ["manage_users", "manage_content", "view_reports"], "permissions": ["manage_users", "manage_content", "view_reports"],
"isActive": true, "isActive": true
"createdAt": "2025-09-01T00:00:00.000Z",
"updatedAt": "2025-09-01T00:00:00.000Z"
}, },
{ {
"id": "role_admin_kesehatan", "id": "role-2",
"name": "ADMIN_KESEHATAN", "name": "ADMIN KESEHATAN",
"description": "Administrator Bidang Kesehatan", "description": "Administrator Bidang Kesehatan",
"permissions": ["manage_health_data", "view_reports"], "permissions": ["manage_health_data", "view_reports"],
"isActive": true, "isActive": true
"createdAt": "2025-09-01T00:00:00.000Z",
"updatedAt": "2025-09-01T00:00:00.000Z"
}, },
{ {
"id": "role_admin_sekolah", "id": "role-3",
"name": "ADMIN_SEKOLAH", "name": "ADMIN SEKOLAH",
"description": "Administrator Sekolah", "description": "Administrator Sekolah",
"permissions": ["manage_school_data", "view_reports"], "permissions": ["manage_school_data", "view_reports"],
"isActive": true, "isActive": true
"createdAt": "2025-09-01T00:00:00.000Z",
"updatedAt": "2025-09-01T00:00:00.000Z"
} }
] ]

View File

@@ -1,35 +1,23 @@
[ [
{ {
"id": "user_admin_desa", "id": "user-1",
"nama": "Admin Desa", "nama": "Admin Desa",
"email": "admin.desa@example.com", "nomor": "089647037426",
"password": "$2b$10$XFDWYOJFxQ7ZM5bA0N4Z0O8u0eKYv58wLsaR7h6XK9bqWJ1YQJQ9q", "roleId": "role-1",
"roleId": "role_admin_desa", "isActive": true
"isActive": true, },
"lastLogin": "2025-08-31T10:00:00.000Z", {
"createdAt": "2025-09-01T00:00:00.000Z", "id": "user-2",
"updatedAt": "2025-09-01T00:00:00.000Z" "nama": "Admin Kesehatan",
}, "nomor": "082339004198",
{ "roleId": "role-2",
"id": "user_admin_puskesmas", "isActive": true
"nama": "Admin Kesehatan", },
"email": "admin.kesehatan@example.com", {
"password": "$2b$10$XFDWYOJFxQ7ZM5bA0N4Z0O8u0eKYv58wLsaR7h6XK9bqWJ1YQJQ9q", "id": "user-3",
"roleId": "role_admin_kesehatan", "nama": "Admin Sekolah",
"isActive": true, "nomor": "085237157222",
"lastLogin": null, "roleId": "role-3",
"createdAt": "2025-09-01T00:00:00.000Z", "isActive": true
"updatedAt": "2025-09-01T00:00:00.000Z" }
}, ]
{
"id": "user_admin_sekolah",
"nama": "Admin Sekolah",
"email": "admin.sekolah@example.com",
"password": "$2b$10$XFDWYOJFxQ7ZM5bA0N4Z0O8u0eKYv58wLsaR7h6XK9bqWJ1YQJQ9q",
"roleId": "role_admin_sekolah",
"isActive": true,
"lastLogin": null,
"createdAt": "2025-09-01T00:00:00.000Z",
"updatedAt": "2025-09-01T00:00:00.000Z"
}
]

30
prisma/safeseedUnique.ts Normal file
View File

@@ -0,0 +1,30 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// helpers/safeSeedUnique.ts
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
/**
* Helper generic buat seed dengan upsert aman
*/
export async function safeSeedUnique<T extends keyof PrismaClient>(
model: T,
where: Record<string, any>,
data: Record<string, any>
) {
const m = prisma[model];
if (!m) throw new Error(`Model ${String(model)} tidak ditemukan di PrismaClient`);
try {
// @ts-expect-error upsert dynamic
await m.upsert({
where,
update: data,
create: { ...where, ...data },
});
console.log(`✅ Seeded ${String(model)} -> ${JSON.stringify(where)}`);
} catch (err) {
console.error(`❌ Gagal seed ${String(model)} -> ${JSON.stringify(where)}`, err);
}
}

View File

@@ -81,8 +81,6 @@ model FileStorage {
PelayananSuratKeteranganImage PelayananSuratKeterangan[] @relation("PelayananSuratKeteranganImage") PelayananSuratKeteranganImage PelayananSuratKeterangan[] @relation("PelayananSuratKeteranganImage")
PelayananSuratKeteranganImage2 PelayananSuratKeterangan[] @relation("PelayananSuratKeteranganImage2") PelayananSuratKeteranganImage2 PelayananSuratKeterangan[] @relation("PelayananSuratKeteranganImage2")
PasarDesa PasarDesa[] PasarDesa PasarDesa[]
KontakDaruratKeamanan KontakDaruratKeamanan[]
KontakItem KontakItem[]
Pegawai Pegawai[] Pegawai Pegawai[]
DesaDigital DesaDigital[] DesaDigital DesaDigital[]
InfoTekno InfoTekno[] InfoTekno InfoTekno[]
@@ -92,7 +90,7 @@ model FileStorage {
PejabatDesa PejabatDesa[] PejabatDesa PejabatDesa[]
MediaSosial MediaSosial[] MediaSosial MediaSosial[]
DesaAntiKorupsi DesaAntiKorupsi[] DesaAntiKorupsi DesaAntiKorupsi[]
SDGSDesa SDGSDesa[] SDGSDesa SdgsDesa[]
APBDesImage APBDes[] @relation("APBDesImage") APBDesImage APBDes[] @relation("APBDesImage")
APBDesFile APBDes[] @relation("APBDesFile") APBDesFile APBDes[] @relation("APBDesFile")
PrestasiDesa PrestasiDesa[] PrestasiDesa PrestasiDesa[]
@@ -101,6 +99,8 @@ model FileStorage {
PerbekelDariMasaKeMasa PerbekelDariMasaKeMasa[] PerbekelDariMasaKeMasa PerbekelDariMasaKeMasa[]
MitraKolaborasi MitraKolaborasi[] MitraKolaborasi MitraKolaborasi[]
ArtikelKesehatan ArtikelKesehatan[]
} }
//========================================= MENU LANDING PAGE ========================================= // //========================================= MENU LANDING PAGE ========================================= //
@@ -168,9 +168,9 @@ model KategoriDesaAntiKorupsi {
} }
//========================================= SDGS Desa ========================================= // //========================================= SDGS Desa ========================================= //
model SDGSDesa { model SdgsDesa {
id String @id @default(cuid()) id String @id @default(cuid())
name String @unique name String
jumlah String jumlah String
image FileStorage? @relation(fields: [imageId], references: [id]) image FileStorage? @relation(fields: [imageId], references: [id])
imageId String? imageId String?
@@ -183,7 +183,7 @@ model SDGSDesa {
//========================================= APBDes ========================================= // //========================================= APBDes ========================================= //
model APBDes { model APBDes {
id String @id @default(cuid()) id String @id @default(cuid())
name String @unique name String
jumlah String jumlah String
image FileStorage? @relation("APBDesImage", fields: [imageId], references: [id]) image FileStorage? @relation("APBDesImage", fields: [imageId], references: [id])
imageId String? imageId String?
@@ -198,7 +198,7 @@ model APBDes {
//========================================= PRESTASI DESA ========================================= // //========================================= PRESTASI DESA ========================================= //
model PrestasiDesa { model PrestasiDesa {
id String @id @default(cuid()) id String @id @default(cuid())
name String @unique name String
deskripsi String @db.Text deskripsi String @db.Text
kategori KategoriPrestasiDesa @relation(fields: [kategoriId], references: [id]) kategori KategoriPrestasiDesa @relation(fields: [kategoriId], references: [id])
kategoriId String kategoriId String
@@ -672,17 +672,18 @@ model GalleryVideo {
// ========================================= LAYANAN DESA ========================================= // // ========================================= LAYANAN DESA ========================================= //
model PelayananSuratKeterangan { model PelayananSuratKeterangan {
id String @id @default(cuid()) id String @id @default(cuid())
name String name String
deskripsi String @db.Text deskripsi String @db.Text
image FileStorage? @relation("PelayananSuratKeteranganImage", fields: [imageId], references: [id]) image FileStorage? @relation("PelayananSuratKeteranganImage", fields: [imageId], references: [id])
imageId String? imageId String?
image2 FileStorage? @relation("PelayananSuratKeteranganImage2", fields: [image2Id], references: [id]) image2 FileStorage? @relation("PelayananSuratKeteranganImage2", fields: [image2Id], references: [id])
image2Id String? image2Id String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
deletedAt DateTime @default(now()) deletedAt DateTime @default(now())
isActive Boolean @default(true) isActive Boolean @default(true)
AjukanPermohonan AjukanPermohonan[]
} }
model PelayananTelunjukSaktiDesa { model PelayananTelunjukSaktiDesa {
@@ -717,6 +718,20 @@ model PelayananPendudukNonPermanen {
isActive Boolean @default(true) isActive Boolean @default(true)
} }
model AjukanPermohonan {
id String @id @default(cuid())
nama String
nik String
alamat String
nomorKk String
kategori PelayananSuratKeterangan @relation(fields: [kategoriId], references: [id])
kategoriId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= PENGHARGAAN ========================================= // // ========================================= PENGHARGAAN ========================================= //
model Penghargaan { model Penghargaan {
id String @id @default(cuid()) id String @id @default(cuid())
@@ -835,8 +850,8 @@ model JadwalKegiatan {
syaratKetentuanJadwalKegiatanId String syaratKetentuanJadwalKegiatanId String
dokumenjadwalkegiatan DokumenJadwalKegiatan @relation(fields: [dokumenJadwalKegiatanId], references: [id]) dokumenjadwalkegiatan DokumenJadwalKegiatan @relation(fields: [dokumenJadwalKegiatanId], references: [id])
dokumenJadwalKegiatanId String dokumenJadwalKegiatanId String
pendaftaranjadwalkegiatan PendaftaranJadwalKegiatan @relation(fields: [pendaftaranJadwalKegiatanId], references: [id]) pendaftaranjadwalkegiatan PendaftaranJadwalKegiatan? @relation(fields: [pendaftaranJadwalKegiatanId], references: [id])
pendaftaranJadwalKegiatanId String pendaftaranJadwalKegiatanId String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
deletedAt DateTime @default(now()) deletedAt DateTime @default(now())
@@ -972,8 +987,10 @@ model ArtikelKesehatan {
id String @id @default(cuid()) id String @id @default(cuid())
title String title String
content String content String
introduction Introduction @relation(fields: [introductionId], references: [id]) image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
introductionId String introductionId String
introduction Introduction @relation(fields: [introductionId], references: [id])
symptom Symptom @relation(fields: [symptomId], references: [id]) symptom Symptom @relation(fields: [symptomId], references: [id])
symptomId String symptomId String
prevention Prevention @relation(fields: [preventionId], references: [id]) prevention Prevention @relation(fields: [preventionId], references: [id])
@@ -1150,6 +1167,7 @@ model KontakDarurat {
deskripsi String deskripsi String
image FileStorage @relation(fields: [imageId], references: [id]) image FileStorage @relation(fields: [imageId], references: [id])
imageId String imageId String
whatsapp String
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
deletedAt DateTime @default(now()) deletedAt DateTime @default(now())
@@ -1216,71 +1234,51 @@ model LayananPolsek {
// ========================================= KONTAK DARURAT ========================================= // // ========================================= KONTAK DARURAT ========================================= //
model KontakDaruratKeamanan { model KontakDaruratKeamanan {
id String @id @default(uuid()) id String @id @default(uuid())
nama String // contoh: "Layanan Darurat", "Fasilitas Kesehatan" nama String
image FileStorage? @relation(fields: [imageId], references: [id]) icon String
imageId String? kategori KontakItem @relation(fields: [kategoriId], references: [id])
kontakItems KontakItem[] kategoriId String
createdAt DateTime @default(now()) kontakItems KontakDaruratToItem[]
updatedAt DateTime @updatedAt createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
isActive Boolean @default(true)
} }
model KontakItem { model KontakItem {
id String @id @default(uuid()) id String @id @default(uuid())
nama String // contoh: "Polisi", "Ambulans", "Puskesmas Darmasaba" nama String
nomorTelepon String nomorTelepon String
image FileStorage? @relation(fields: [imageId], references: [id]) icon String
imageId String? createdAt DateTime @default(now())
kategori KontakDaruratKeamanan @relation(fields: [kategoriId], references: [id]) updatedAt DateTime @updatedAt
kategoriId String deletedAt DateTime @default(now())
createdAt DateTime @default(now()) isActive Boolean @default(true)
updatedAt DateTime @updatedAt KontakDaruratToItem KontakDaruratToItem[]
KontakDaruratKeamanan KontakDaruratKeamanan[]
}
model KontakDaruratToItem {
id String @id @default(uuid())
kontakDaruratId String
kontakItemId String
kontakDarurat KontakDaruratKeamanan @relation(fields: [kontakDaruratId], references: [id])
kontakItem KontakItem @relation(fields: [kontakItemId], references: [id])
createdAt DateTime @default(now())
} }
// ========================================= PENCEGAHAN KRIMINALITAS ========================================= // // ========================================= PENCEGAHAN KRIMINALITAS ========================================= //
model PencegahanKriminalitas { model PencegahanKriminalitas {
id String @id @default(cuid()) id String @id @default(cuid())
programKeamanan ProgramKeamanan @relation(fields: [programKeamananId], references: [id]) judul String
programKeamananId String deskripsi String
tipsKeamanan TipsKeamanan @relation(fields: [tipsKeamananId], references: [id]) deskripsiSingkat String
tipsKeamananId String linkVideo String
videoKeamanan VideoKeamanan @relation(fields: [videoKeamananId], references: [id]) createdAt DateTime @default(now())
videoKeamananId String updatedAt DateTime @updatedAt
createdAt DateTime @default(now()) deletedAt DateTime @default(now())
updatedAt DateTime @updatedAt isActive Boolean @default(true)
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model ProgramKeamanan {
id String @id @default(cuid())
nama String // contoh: "Ronda Malam"
deskripsi String? // jika mau tambahkan info detail
slug String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
PencegahanKriminalitas PencegahanKriminalitas[]
}
model TipsKeamanan {
id String @id @default(cuid())
judul String
konten String
slug String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
PencegahanKriminalitas PencegahanKriminalitas[]
}
model VideoKeamanan {
id String @id @default(cuid())
judul String
deskripsi String?
videoUrl String // link youtube atau embed url
slug String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
PencegahanKriminalitas PencegahanKriminalitas[]
} }
// ========================================= LAPORAN PUBLIK ========================================= // // ========================================= LAPORAN PUBLIK ========================================= //
@@ -1289,11 +1287,13 @@ model LaporanPublik {
judul String judul String
lokasi String lokasi String
tanggalWaktu DateTime tanggalWaktu DateTime
status StatusLaporan status StatusLaporan @default(Proses)
penanganan PenangananLaporanPublik[] penanganan PenangananLaporanPublik[]
kronologi String? // Optional, bisa diisi detail kronologi kronologi String? // Optional, bisa diisi detail kronologi
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
} }
model PenangananLaporanPublik { model PenangananLaporanPublik {
@@ -1341,6 +1341,7 @@ model PasarDesa {
harga Int harga Int
rating Float rating Float
alamatUsaha String alamatUsaha String
kontak String
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
deletedAt DateTime @default(now()) deletedAt DateTime @default(now())
@@ -1383,6 +1384,7 @@ model LowonganPekerjaan {
gaji String gaji String
deskripsi String deskripsi String
kualifikasi String kualifikasi String
notelp String
tanggalPosting DateTime @default(now()) tanggalPosting DateTime @default(now())
isActive Boolean @default(true) isActive Boolean @default(true)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
@@ -1401,6 +1403,9 @@ model PosisiOrganisasi {
pegawai Pegawai[] pegawai Pegawai[]
strukturOrganisasi StrukturOrganisasi[] // Relasi balik strukturOrganisasi StrukturOrganisasi[] // Relasi balik
StrukturOrganisasiPPID StrukturOrganisasiPPID[] StrukturOrganisasiPPID StrukturOrganisasiPPID[]
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("posisi_organisasi") @@map("posisi_organisasi")
} }
@@ -1469,7 +1474,7 @@ model ProgramKemiskinan {
id String @id @default(uuid()) id String @id @default(uuid())
nama String nama String
deskripsi String deskripsi String
ikonUrl String? icon String
isActive Boolean @default(true) isActive Boolean @default(true)
statistikId String? @unique statistikId String? @unique
statistik StatistikKemiskinan? @relation(fields: [statistikId], references: [id]) statistik StatistikKemiskinan? @relation(fields: [statistikId], references: [id])
@@ -1551,7 +1556,7 @@ model DataDemografiPekerjaan {
model DetailDataPengangguran { model DetailDataPengangguran {
id String @id @default(uuid()) @db.Uuid id String @id @default(uuid()) @db.Uuid
month String @db.VarChar(20) month String @db.VarChar(20)
year DateTime year Int
totalUnemployment Int totalUnemployment Int
educatedUnemployment Int educatedUnemployment Int
uneducatedUnemployment Int uneducatedUnemployment Int
@@ -2103,19 +2108,21 @@ model KategoriBuku {
DataPerpustakaan DataPerpustakaan[] DataPerpustakaan DataPerpustakaan[]
} }
// ========================================= USER ========================================= //
model User { model User {
id String @id @default(cuid()) id String @id @default(cuid())
nama String username String
email String @unique nomor String @unique
password String? role Role @relation(fields: [roleId], references: [id])
role Role @relation(fields: [roleId], references: [id]) roleId String @default("1")
roleId String instansi String?
instansi String? // Nama instansi (Puskesmas, Sekolah, dll) UserSession UserSession? // Nama instansi (Puskesmas, Sekolah, dll)
isActive Boolean @default(true) isActive Boolean @default(true)
lastLogin DateTime? lastLogin DateTime?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
deletedAt DateTime? deletedAt DateTime?
} }
model Role { model Role {
@@ -2132,6 +2139,15 @@ model Role {
@@map("roles") @@map("roles")
} }
model KodeOtp {
id String @id @default(cuid())
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
nomor String
otp Int
}
// Tabel untuk menyimpan permission // Tabel untuk menyimpan permission
model Permission { model Permission {
id String @id @default(cuid()) id String @id @default(cuid())
@@ -2143,6 +2159,17 @@ model Permission {
@@map("permissions") @@map("permissions")
} }
model UserSession {
id String @id @default(cuid())
token String
expires DateTime?
active Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
User User @relation(fields: [userId], references: [id])
userId String @unique
}
// ========================================= DATA PENDIDIKAN ========================================= // // ========================================= DATA PENDIDIKAN ========================================= //
model DataPendidikan { model DataPendidikan {
id String @id @default(cuid()) id String @id @default(cuid())

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import profilePejabatDesa from "./data/landing-page/profile/profile.json"; import profilePejabatDesa from "./data/landing-page/profile/profile.json";
import programInovasi from "./data/landing-page/profile/programInovasi.json"; import programInovasi from "./data/landing-page/profile/programInovasi.json";
@@ -15,7 +16,7 @@ import posisiOrganisasiPPID from "./data/ppid/struktur-ppid/posisi-organisasi-PP
import visiMisiPPID from "./data/ppid/visi-misi-ppid/visimisiPPID.json"; import visiMisiPPID from "./data/ppid/visi-misi-ppid/visimisiPPID.json";
import dasarHukumPPID from "./data/ppid/dasar-hukum-ppid/dasarhukumPPID.json"; import dasarHukumPPID from "./data/ppid/dasar-hukum-ppid/dasarhukumPPID.json";
import jenisKelamin from "./data/ppid/ikm/jenis-kelamin/jenis-kelamin.json"; import jenisKelamin from "./data/ppid/ikm/jenis-kelamin/jenis-kelamin.json";
import daftarInformasiPublik from "./data/ppid/daftar-informasi-publik-desa-darmasaba/daftarInformasi.json" import daftarInformasiPublik from "./data/ppid/daftar-informasi-publik-desa-darmasaba/daftarInformasi.json";
import pilihanRatingResponden from "./data/ppid/ikm/pilihan-rating-responden/rating-responden.json"; import pilihanRatingResponden from "./data/ppid/ikm/pilihan-rating-responden/rating-responden.json";
import umurResponden from "./data/ppid/ikm/umur-responden/umur-responden.json"; import umurResponden from "./data/ppid/ikm/umur-responden/umur-responden.json";
import categoryPengumuman from "./data/category-pengumuman.json"; import categoryPengumuman from "./data/category-pengumuman.json";
@@ -54,50 +55,73 @@ import programUnggulan from "./data/pendidikan/program-pendidikan-anak/program-u
import tujuanProgram from "./data/pendidikan/program-pendidikan-anak/tujuan-program.json"; import tujuanProgram from "./data/pendidikan/program-pendidikan-anak/tujuan-program.json";
import roles from "./data/user/roles.json"; import roles from "./data/user/roles.json";
import users from "./data/user/users.json"; import users from "./data/user/users.json";
import fileStorage from "./data/file-storage.json";
import seedAssets from "./seed_assets";
import { safeSeedUnique } from "./safeseedUnique";
(async () => { (async () => {
// =========== USER & ROLE =========== // =========== USER & ROLE ===========
// In your seed.ts
// =========== ROLES ===========
console.log("🔄 Seeding roles...");
for (const r of roles) { for (const r of roles) {
await prisma.role.upsert({ await safeSeedUnique("role", { id: r.id }, {
where: { id: r.id }, name: r.name,
update: { description: r.description,
name: r.name, permissions: r.permissions,
description: r.description, isActive: r.isActive,
permissions: r.permissions,
isActive: true,
},
create: {
id: r.id,
name: r.name,
description: r.description,
permissions: r.permissions,
isActive: true,
},
}); });
} }
console.log("✅ Roles seeded"); console.log("✅ Roles seeded");
//users
// =========== USERS ===========
console.log("🔄 Seeding users...");
for (const u of users) { for (const u of users) {
await prisma.user.upsert({ // First verify the role exists
where: { id: u.id }, const roleExists = await prisma.role.findUnique({
update: { where: { id: u.roleId },
nama: u.nama,
email: u.email,
password: u.password,
roleId: u.roleId,
isActive: true,
},
create: {
id: u.id,
nama: u.nama,
email: u.email,
password: u.password,
roleId: u.roleId,
isActive: true,
},
}); });
if (!roleExists) {
console.error(`❌ Role with id ${u.roleId} not found for user ${u.nama}`);
continue;
}
await safeSeedUnique("user", { id: u.id }, {
username: u.nama,
nomor: u.nomor,
roleId: u.roleId,
isActive: u.isActive,
});
} }
console.log("✅ Users seeded"); console.log("✅ Users seeded");
// =========== FILE STORAGE ===========
console.log("🔄 Seeding file storage...");
for (const f of fileStorage) {
await prisma.fileStorage.upsert({
where: { id: f.id },
update: {
name: f.name,
realName: f.realName,
path: f.path,
mimeType: f.mimeType,
link: f.link,
category: f.category,
},
create: {
id: f.id,
name: f.name,
realName: f.realName,
path: f.path,
mimeType: f.mimeType,
link: f.link,
category: f.category,
},
});
}
console.log("✅ File storage seeded");
// =========== LANDING PAGE =========== // =========== LANDING PAGE ===========
// =========== SUBMENU PROFILE =========== // =========== SUBMENU PROFILE ===========
// =========== PROFILE PEJABAT DESA =========== // =========== PROFILE PEJABAT DESA ===========
@@ -107,11 +131,13 @@ import users from "./data/user/users.json";
update: { update: {
name: p.name, name: p.name,
position: p.position, position: p.position,
imageId: p.imageId,
}, },
create: { create: {
id: p.id, id: p.id,
name: p.name, name: p.name,
position: p.position, position: p.position,
imageId: p.imageId,
}, },
}); });
} }
@@ -121,18 +147,35 @@ import users from "./data/user/users.json";
// =========== PROGRAM INOVASI =========== // =========== PROGRAM INOVASI ===========
for (const p of programInovasi) { for (const p of programInovasi) {
let imageId: string | null = null;
if (p.imageId) {
const imageExists = await prisma.fileStorage.findUnique({
where: { id: p.imageId },
});
if (imageExists) {
imageId = p.imageId;
} else {
console.warn(
`⚠️ imageId ${p.imageId} tidak ditemukan untuk ProgramInovasi ${p.name}`
);
}
}
await prisma.programInovasi.upsert({ await prisma.programInovasi.upsert({
where: { id: p.id }, where: { id: p.id },
update: { update: {
name: p.name, name: p.name,
description: p.description, description: p.description,
link: p.link, link: p.link,
imageId: p.imageId,
}, },
create: { create: {
id: p.id, id: p.id,
name: p.name, name: p.name,
description: p.description, description: p.description,
link: p.link, link: p.link,
imageId: p.imageId,
}, },
}); });
} }
@@ -145,11 +188,13 @@ import users from "./data/user/users.json";
update: { update: {
name: p.name, name: p.name,
iconUrl: p.iconUrl, iconUrl: p.iconUrl,
imageId: p.imageId,
}, },
create: { create: {
id: p.id, id: p.id,
name: p.name, name: p.name,
iconUrl: p.iconUrl, iconUrl: p.iconUrl,
imageId: p.imageId,
}, },
}); });
} }
@@ -295,16 +340,14 @@ import users from "./data/user/users.json";
// =========== SDGSDesa =========== // =========== SDGSDesa ===========
for (const l of sdgsDesa) { for (const l of sdgsDesa) {
await prisma.sDGSDesa.upsert({ await prisma.sdgsDesa.upsert({
where: { where: { id: l.id },
name: l.name,
jumlah: l.jumlah,
},
update: { update: {
name: l.name, name: l.name,
jumlah: l.jumlah, jumlah: l.jumlah,
}, },
create: { create: {
id: l.id,
name: l.name, name: l.name,
jumlah: l.jumlah, jumlah: l.jumlah,
}, },
@@ -317,8 +360,7 @@ import users from "./data/user/users.json";
for (const l of apbdes) { for (const l of apbdes) {
await prisma.aPBDes.upsert({ await prisma.aPBDes.upsert({
where: { where: {
name: l.name, id: l.id,
jumlah: l.jumlah,
}, },
update: { update: {
name: l.name, name: l.name,
@@ -545,29 +587,29 @@ import users from "./data/user/users.json";
} }
console.log("dasar hukum PPID success ..."); console.log("dasar hukum PPID success ...");
// =========== SUBMENU DAFTAR INFORMASI PUBLIK PPID =========== // =========== SUBMENU DAFTAR INFORMASI PUBLIK PPID ===========
for (const v of daftarInformasiPublik) { for (const v of daftarInformasiPublik) {
// Convert string date to Date object // Convert string date to Date object
const tanggal = new Date(v.tanggal); const tanggal = new Date(v.tanggal);
await prisma.daftarInformasiPublik.upsert({ await prisma.daftarInformasiPublik.upsert({
where: { where: {
id: v.id, id: v.id,
}, },
update: { update: {
jenisInformasi: v.jenisInformasi, jenisInformasi: v.jenisInformasi,
deskripsi: v.deskripsi, deskripsi: v.deskripsi,
tanggal: tanggal, tanggal: tanggal,
}, },
create: { create: {
id: v.id, id: v.id,
jenisInformasi: v.jenisInformasi, jenisInformasi: v.jenisInformasi,
deskripsi: v.deskripsi, deskripsi: v.deskripsi,
tanggal: tanggal, tanggal: tanggal,
}, },
}); });
} }
console.log("daftar informasi publik PPID success ..."); console.log("daftar informasi publik PPID success ...");
for (const l of pelayananPerizinanBerusaha) { for (const l of pelayananPerizinanBerusaha) {
await prisma.pelayananPerizinanBerusaha.upsert({ await prisma.pelayananPerizinanBerusaha.upsert({
@@ -836,12 +878,9 @@ import users from "./data/user/users.json";
console.log("hubungan organisasi success ..."); console.log("hubungan organisasi success ...");
for (const d of detailDataPengangguran) { for (const d of detailDataPengangguran) {
// Convert the year to a Date object (using January 1st of the year as the date)
const yearAsDate = new Date(d.year, 0, 1);
await prisma.detailDataPengangguran.upsert({ await prisma.detailDataPengangguran.upsert({
where: { where: {
month_year: { month: d.month, year: yearAsDate }, month_year: { month: d.month, year: d.year },
}, },
update: { update: {
totalUnemployment: d.totalUnemployment, totalUnemployment: d.totalUnemployment,
@@ -851,7 +890,7 @@ import users from "./data/user/users.json";
}, },
create: { create: {
month: d.month, month: d.month,
year: yearAsDate, year: d.year,
totalUnemployment: d.totalUnemployment, totalUnemployment: d.totalUnemployment,
educatedUnemployment: d.educatedUnemployment, educatedUnemployment: d.educatedUnemployment,
uneducatedUnemployment: d.uneducatedUnemployment, uneducatedUnemployment: d.uneducatedUnemployment,
@@ -1114,6 +1153,10 @@ import users from "./data/user/users.json";
console.log( console.log(
"✅ fasilitas bimbingan belajar desa seeded (editable later via UI)" "✅ fasilitas bimbingan belajar desa seeded (editable later via UI)"
); );
// seed assets
await seedAssets();
})() })()
.then(() => prisma.$disconnect()) .then(() => prisma.$disconnect())
.catch((e) => { .catch((e) => {

118
prisma/seed_assets.ts Normal file
View File

@@ -0,0 +1,118 @@
// prisma/seedAssets.ts
import fs from "fs/promises";
import path from "path";
import sharp from "sharp";
import fetch from "node-fetch";
import AdmZip from "adm-zip";
import prisma from "@/lib/prisma";
const UPLOADS_DIR =
process.env.WIBU_UPLOAD_DIR || path.join(process.cwd(), "uploads");
// --- Helper: deteksi kategori file ---
function detectCategory(filename: string): "image" | "document" | "other" {
const ext = path.extname(filename).toLowerCase();
if ([".jpg", ".jpeg", ".png", ".webp"].includes(ext)) return "image";
if ([".pdf", ".doc", ".docx"].includes(ext)) return "document";
return "other";
}
// --- Helper: recursive walk dir ---
async function walkDir(dir: string, fileList: string[] = []): Promise<string[]> {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
if (entry.name === "__MACOSX") continue; // skip folder sampah
await walkDir(fullPath, fileList);
} else {
if (entry.name.startsWith(".") || entry.name === ".DS_Store") continue; // skip file sampah
fileList.push(fullPath);
}
}
return fileList;
}
export default async function seedAssets() {
console.log("🚀 Seeding assets...");
// 1. Download zip
const url =
"https://cld-dkr-makuro-seafile.wibudev.com/f/ffd5a548a04f47939474/?dl=1";
const res = await fetch(url);
if (!res.ok) throw new Error(`Gagal download assets: ${res.statusText}`);
const buffer = Buffer.from(await res.arrayBuffer());
// 2. Extract zip ke folder tmp
const extractDir = path.join(process.cwd(), "tmp_assets");
await fs.rm(extractDir, { recursive: true, force: true });
await fs.mkdir(extractDir, { recursive: true });
const zip = new AdmZip(buffer);
zip.extractAllTo(extractDir, true);
// 3. Cari semua file valid (recursive)
const files = await walkDir(extractDir);
// 4. Loop tiap file & simpan
for (const filePath of files) {
const entryName = path.basename(filePath);
const category = detectCategory(entryName);
let finalName = entryName;
let mimeType = "application/octet-stream";
let targetPath = "";
if (category === "image") {
const fileBaseName = path.parse(entryName).name;
finalName = `${fileBaseName}.webp`;
targetPath = path.join(UPLOADS_DIR, "images", finalName);
await fs.mkdir(path.dirname(targetPath), { recursive: true });
await sharp(filePath).webp({ quality: 80 }).toFile(targetPath);
mimeType = "image/webp";
} else if (category === "document") {
targetPath = path.join(UPLOADS_DIR, "documents", entryName);
await fs.mkdir(path.dirname(targetPath), { recursive: true });
await fs.copyFile(filePath, targetPath);
mimeType = "application/pdf";
} else {
targetPath = path.join(UPLOADS_DIR, "other", entryName);
await fs.mkdir(path.dirname(targetPath), { recursive: true });
await fs.copyFile(filePath, targetPath);
}
// 5. Simpan ke DB
await prisma.fileStorage.create({
data: {
name: finalName,
realName: entryName,
path: targetPath,
mimeType,
link: `/uploads/${category}/${finalName}`,
category,
},
});
console.log(`📂 saved: ${category}/${finalName}`);
}
// 6. Cleanup
await fs.rm(extractDir, { recursive: true, force: true });
console.log("✅ Selesai seed assets!");
}
// --- Auto run kalau dipanggil langsung ---
if (import.meta.main) {
seedAssets()
.catch((err) => {
console.error("❌ Error seeding assets:", err);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

BIN
public/beasiswa-siswa.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

After

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

View File

@@ -23,6 +23,7 @@ export default function SpashScreen() {
<Paper p={"md"} miw={320}> <Paper p={"md"} miw={320}>
<Flex> <Flex>
<Image <Image
loading="lazy"
src={images["darmasaba-icon"]} src={images["darmasaba-icon"]}
alt="darmasaba" alt="darmasaba"
w={100} w={100}

View File

@@ -0,0 +1,100 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import React from 'react'
import {
IconLeaf,
IconTrophy,
IconTent,
IconChartLine,
IconRecycle,
IconTruck,
IconScale,
IconClipboard,
IconTrash,
IconHomeEco,
IconChristmasTreeFilled,
IconTrendingUp,
IconShieldFilled,
IconHome,
IconTree,
IconDroplet,
IconCash,
IconSchool,
IconShoppingCart,
IconHospital,
IconAmbulance,
IconFiretruck,
IconBuilding,
IconAlertTriangle,
} from '@tabler/icons-react'
export type IconKey =
| 'ekowisata'
| 'kompetisi'
| 'wisata'
| 'ekonomi'
| 'sampah'
| 'truck'
| 'scale'
| 'clipboard'
| 'trash'
| 'lingkunganSehat'
| 'sumberOksigen'
| 'ekonomiBerkelanjutan'
| 'mencegahBencana'
| 'rumah'
| 'pohon'
| 'air'
| 'bantuan'
| 'pelatihan'
| 'subsidi'
| 'layananKesehatan'
| 'polisi'
| 'ambulans'
| 'pemadam'
| 'rumahSakit'
| 'bangunan'
| 'darurat'
const iconMap: Record<IconKey, React.FC<any>> = {
ekowisata: IconLeaf,
kompetisi: IconTrophy,
wisata: IconTent,
ekonomi: IconChartLine,
sampah: IconRecycle,
truck: IconTruck,
scale: IconScale,
clipboard: IconClipboard,
trash: IconTrash,
lingkunganSehat: IconHomeEco,
sumberOksigen: IconChristmasTreeFilled,
ekonomiBerkelanjutan: IconTrendingUp,
mencegahBencana: IconShieldFilled,
rumah: IconHome,
pohon: IconTree,
air: IconDroplet,
bantuan: IconCash,
pelatihan: IconSchool,
subsidi: IconShoppingCart,
layananKesehatan: IconHospital,
polisi: IconShieldFilled,
ambulans: IconAmbulance,
pemadam: IconFiretruck,
rumahSakit: IconHospital,
bangunan: IconBuilding,
darurat: IconAlertTriangle
}
type Props = {
name: IconKey
size?: number
color?: string
}
export const IconMapper: React.FC<Props> = ({ name, size = 24, color }) => {
const IconComponent = iconMap[name]
if (!IconComponent) return null
return <IconComponent size={size} color={color} />
}

View File

@@ -3,16 +3,24 @@
import { Box, rem, Select } from '@mantine/core'; import { Box, rem, Select } from '@mantine/core';
import { import {
IconAlertTriangle,
IconAmbulance,
IconBuilding,
IconCash,
IconChartLine, IconChartLine,
IconChristmasTreeFilled, IconChristmasTreeFilled,
IconClipboardTextFilled, IconClipboardTextFilled,
IconDroplet, IconDroplet,
IconFiretruck,
IconHome, IconHome,
IconHomeEco, IconHomeEco,
IconHospital,
IconLeaf, IconLeaf,
IconRecycle, IconRecycle,
IconScale, IconScale,
IconSchool,
IconShieldFilled, IconShieldFilled,
IconShoppingCart,
IconTent, IconTent,
IconTrashFilled, IconTrashFilled,
IconTree, IconTree,
@@ -32,13 +40,23 @@ const iconMap = {
scale: { label: 'Scale', icon: IconScale }, scale: { label: 'Scale', icon: IconScale },
clipboard: { label: 'Clipboard', icon: IconClipboardTextFilled }, clipboard: { label: 'Clipboard', icon: IconClipboardTextFilled },
trash: { label: 'Trash', icon: IconTrashFilled }, trash: { label: 'Trash', icon: IconTrashFilled },
lingkunganSehat: {label: 'Lingkungan Sehat', icon: IconHomeEco}, lingkunganSehat: { label: 'Lingkungan Sehat', icon: IconHomeEco },
sumberOksigen: {label: 'Sumber Oksigen', icon: IconChristmasTreeFilled}, sumberOksigen: { label: 'Sumber Oksigen', icon: IconChristmasTreeFilled },
ekonomiBerkelanjutan: {label: 'Ekonomi Berkelanjutan', icon: IconTrendingUp}, ekonomiBerkelanjutan: { label: 'Ekonomi Berkelanjutan', icon: IconTrendingUp },
mencegahBencana: {label: 'Mencegah Bencana', icon: IconShieldFilled}, mencegahBencana: { label: 'Mencegah Bencana', icon: IconShieldFilled },
rumah: {label: 'Rumah', icon: IconHome}, rumah: { label: 'Rumah', icon: IconHome },
pohon: {label: 'Pohon', icon: IconTree}, pohon: { label: 'Pohon', icon: IconTree },
air: {label: 'Air', icon: IconDroplet} air: { label: 'Air', icon: IconDroplet },
bantuan: { label: 'Bantuan', icon: IconCash },
pelatihan: { label: 'Pelatihan', icon: IconSchool },
subsidi: { label: 'Subsidi', icon: IconShoppingCart },
layananKesehatan: { label: 'Layanan Kesehatan', icon: IconHospital },
polisi: { label: 'Polisi', icon: IconShieldFilled },
ambulans: { label: 'Ambulans', icon: IconAmbulance },
pemadam: { label: 'Pemadam', icon: IconFiretruck },
rumahSakit: { label: 'Rumah Sakit', icon: IconHospital },
bangunan: { label: 'Bangunan', icon: IconBuilding },
darurat: { label: 'Darurat', icon: IconAlertTriangle },
}; };

View File

@@ -2,22 +2,30 @@
import { Box, rem, Select } from '@mantine/core'; import { Box, rem, Select } from '@mantine/core';
import { import {
IconAmbulance,
IconCash,
IconChartLine, IconChartLine,
IconChristmasTreeFilled, IconChristmasTreeFilled,
IconClipboardTextFilled, IconClipboardTextFilled,
IconDroplet, IconDroplet,
IconFiretruck,
IconHome, IconHome,
IconHomeEco, IconHomeEco,
IconHospital,
IconLeaf, IconLeaf,
IconRecycle, IconRecycle,
IconScale, IconScale,
IconSchool,
IconShieldFilled, IconShieldFilled,
IconShoppingCart,
IconTent, IconTent,
IconTrashFilled, IconTrashFilled,
IconTree, IconTree,
IconTrendingUp, IconTrendingUp,
IconTrophy, IconTrophy,
IconTruckFilled, IconTruckFilled,
IconBuilding,
IconAlertTriangle
} from '@tabler/icons-react'; } from '@tabler/icons-react';
const iconMap = { const iconMap = {
@@ -36,7 +44,17 @@ const iconMap = {
mencegahBencana: {label: 'Mencegah Bencana', icon: IconShieldFilled}, mencegahBencana: {label: 'Mencegah Bencana', icon: IconShieldFilled},
rumah: {label: 'Rumah', icon: IconHome}, rumah: {label: 'Rumah', icon: IconHome},
pohon: {label: 'Pohon', icon: IconTree}, pohon: {label: 'Pohon', icon: IconTree},
air: {label: 'Air', icon: IconDroplet} air: {label: 'Air', icon: IconDroplet},
bantuan: {label: 'Bantuan', icon: IconCash},
pelatihan: {label: 'Pelatihan', icon: IconSchool},
subsidi: {label: 'Subsidi', icon: IconShoppingCart},
layananKesehatan: {label: 'Layanan Kesehatan', icon: IconHospital},
polisi: {label: 'Polisi', icon: IconShieldFilled},
ambulans: {label: 'Ambulans', icon: IconAmbulance},
pemadam: {label: 'Pemadam', icon: IconFiretruck},
rumahSakit: {label: 'Rumah Sakit', icon: IconHospital},
bangunan: {label: 'Bangunan', icon: IconBuilding},
darurat: {label: 'Darurat', icon: IconAlertTriangle},
}; };
type IconKey = keyof typeof iconMap; type IconKey = keyof typeof iconMap;

View File

@@ -71,6 +71,22 @@ const pelayananPendudukNonPermanenForm = {
deskripsi: "", deskripsi: "",
}; };
const templateAjukanForm = z.object({
nama: z.string().min(1).max(5000),
nik: z.string().min(1).max(5000),
alamat: z.string().min(1).max(5000),
nomorKk: z.string().min(1).max(5000),
kategoriId: z.string().min(1).max(5000),
});
const defaultAjukanForm = {
nama: "",
nik: "",
alamat: "",
nomorKk: "",
kategoriId: "",
};
const suratKeterangan = proxy({ const suratKeterangan = proxy({
create: { create: {
form: { ...suratKeteranganForm }, form: { ...suratKeteranganForm },
@@ -146,6 +162,30 @@ const suratKeterangan = proxy({
} }
}, },
}, },
findManyAll: {
data: null as Prisma.PelayananSuratKeteranganGetPayload<{
omit: { isActive: true };
}>[] | null,
loading: false,
load: async () => {
suratKeterangan.findManyAll.loading = true;
try {
const res = await ApiFetch.api.desa.layanan.pelayanansuratketerangan["findManyAll"].get();
if (res.status === 200 && res.data?.success) {
suratKeterangan.findManyAll.data = res.data.data || [];
} else {
suratKeterangan.findManyAll.data = [];
console.error("Failed to load surat keterangan all:", res.data?.message);
}
} catch (error) {
console.error("Error loading surat keterangan all:", error);
suratKeterangan.findManyAll.data = [];
} finally {
suratKeterangan.findManyAll.loading = false;
}
},
},
findUnique: { findUnique: {
data: null as Prisma.PelayananSuratKeteranganGetPayload<{ data: null as Prisma.PelayananSuratKeteranganGetPayload<{
include: { include: {
@@ -541,33 +581,24 @@ const pelayananPerizinanBerusaha = proxy({
findById: { findById: {
data: null as pelayananPerizinanBerusahaForm | null, data: null as pelayananPerizinanBerusahaForm | null,
loading: false, loading: false,
initialize() {
pelayananPerizinanBerusaha.findById.data = {
id: "",
name: "",
deskripsi: "",
link: "",
} as pelayananPerizinanBerusahaForm;
},
async load(id: string) { async load(id: string) {
try { try {
pelayananPerizinanBerusaha.findById.loading = true; this.loading = true;
const res = await fetch( const response = await fetch(`/api/desa/layanan/pelayananperizinanberusaha/${id}`);
`/api/desa/layanan/pelayananperizinanberusaha/${id}` if (!response.ok) {
); throw new Error(`HTTP error! status: ${response.status}`);
if (res.ok) {
const data = await res.json();
pelayananPerizinanBerusaha.findById.data = data.data ?? null;
} else {
console.error(
"Failed to fetch pelayanan perizinan berusaha:",
res.statusText
);
pelayananPerizinanBerusaha.findById.data = null;
} }
const result = await response.json();
if (result?.success) {
this.data = result.data; // Make sure this matches your API response structure
}
return result?.data || null;
} catch (error) { } catch (error) {
console.error("Error fetching pelayanan perizinan berusaha:", error); console.error('Error loading data:', error);
pelayananPerizinanBerusaha.findById.data = null; toast.error('Gagal memuat data');
return null;
} finally {
this.loading = false;
} }
}, },
}, },
@@ -769,11 +800,250 @@ const pelayananPendudukNonPermanen = proxy({
}, },
}); });
const ajukanPermohonan = proxy({
create: {
form: { ...defaultAjukanForm },
loading: false,
async create() {
const cek = templateAjukanForm.safeParse(
ajukanPermohonan.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
ajukanPermohonan.create.loading = true;
const res = await ApiFetch.api.desa.ajukanpermohonan[
"create"
].post(ajukanPermohonan.create.form);
if (res.status === 200) {
ajukanPermohonan.findMany.load();
return toast.success("Ajukan permohonan berhasil disimpan!");
}
return toast.error("Gagal menyimpan ajukan permohonan");
} catch (error) {
console.log((error as Error).message);
} finally {
ajukanPermohonan.create.loading = false;
}
},
resetForm() {
ajukanPermohonan.create.form = { ...defaultAjukanForm };
},
},
findMany: {
data: null as Prisma.AjukanPermohonanGetPayload<{
include: {
kategori: true;
};
}>[] | null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
// Change to arrow function
ajukanPermohonan.findMany.loading = true; // Use the full path to access the property
ajukanPermohonan.findMany.page = page;
ajukanPermohonan.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.desa.ajukanpermohonan[
"findMany"
].get({
query,
});
if (res.status === 200 && res.data?.success) {
ajukanPermohonan.findMany.data = res.data.data || [];
ajukanPermohonan.findMany.total = res.data.total || 0;
ajukanPermohonan.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error("Failed to load ajukan permohonan:", res.data?.message);
ajukanPermohonan.findMany.data = [];
ajukanPermohonan.findMany.total = 0;
ajukanPermohonan.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading ajukan permohonan:", error);
ajukanPermohonan.findMany.data = [];
ajukanPermohonan.findMany.total = 0;
ajukanPermohonan.findMany.totalPages = 1;
} finally {
ajukanPermohonan.findMany.loading = false;
}
},
},
findUnique: {
data: null as Prisma.AjukanPermohonanGetPayload<{
include: {
kategori: true;
}
}> | null,
async load(id: string) {
try {
const res = await fetch(
`/api/desa/ajukanpermohonan/${id}`
);
if (res.ok) {
const data = await res.json();
ajukanPermohonan.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch ajukan permohonan:", res.statusText);
ajukanPermohonan.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching ajukan permohonan:", error);
ajukanPermohonan.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
ajukanPermohonan.delete.loading = true;
const response = await fetch(
`/api/desa/ajukanpermohonan/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok) {
toast.success(result.message || "Ajukan permohonan berhasil dihapus");
await ajukanPermohonan.findMany.load(); // refresh list
} else {
toast.error(result.message || "Gagal menghapus ajukan permohonan");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus ajukan permohonan");
} finally {
ajukanPermohonan.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...defaultAjukanForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(
`/api/desa/ajukanpermohonan/${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,
nik: data.nik,
alamat: data.alamat,
nomorKk: data.nomorKk,
kategoriId: data.kategoriId,
};
return data;
} else {
throw new Error(result.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error fetching ajukan permohonan:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateAjukanForm.safeParse(
ajukanPermohonan.edit.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
ajukanPermohonan.edit.loading = true;
const response = await fetch(
`/api/desa/ajukanpermohonan/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
nama: this.form.nama,
nik: this.form.nik,
alamat: this.form.alamat,
nomorKk: this.form.nomorKk,
kategoriId: this.form.kategoriId,
}),
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
const result = await response.json();
if (result.success) {
toast.success(result.message || "Ajukan permohonan berhasil diupdate");
await ajukanPermohonan.findMany.load(); // refresh list
return true;
} else {
throw new Error(
result.message || "Gagal mengupdate ajukan permohonan"
);
}
} catch (error) {
console.error("Error updating ajukan permohonan:", error);
toast.error(
error instanceof Error
? error.message
: "Terjadi kesalahan saat update ajukan permohonan"
);
return false;
} finally {
ajukanPermohonan.edit.loading = false;
}
},
},
});
const stateLayananDesa = proxy({ const stateLayananDesa = proxy({
suratKeterangan, suratKeterangan,
pelayananPerizinanBerusaha, pelayananPerizinanBerusaha,
pelayananTelunjukSaktiDesa, pelayananTelunjukSaktiDesa,
pelayananPendudukNonPermanen, pelayananPendudukNonPermanen,
ajukanPermohonan,
}); });
export default stateLayananDesa; export default stateLayananDesa;

View File

@@ -7,9 +7,13 @@ import { z } from "zod";
const templateApbDesa = z.object({ const templateApbDesa = z.object({
tahun: z.number().min(4, "Tahun minimal 4 karakter"), tahun: z.number().min(4, "Tahun minimal 4 karakter"),
pembiayaanIds: z.array(z.string().uuid()).nonempty("Pilih minimal 1 pembiayaan"), pembiayaanIds: z
.array(z.string().uuid())
.nonempty("Pilih minimal 1 pembiayaan"),
belanjaIds: z.array(z.string().uuid()).nonempty("Pilih minimal 1 belanja"), belanjaIds: z.array(z.string().uuid()).nonempty("Pilih minimal 1 belanja"),
pendapatanIds: z.array(z.string().uuid()).nonempty("Pilih minimal 1 pendapatan"), pendapatanIds: z
.array(z.string().uuid())
.nonempty("Pilih minimal 1 pendapatan"),
}); });
const ApbDesaDefaultForm = { const ApbDesaDefaultForm = {
@@ -54,32 +58,46 @@ const ApbDesa = proxy({
}, },
}, },
findMany: { findMany: {
data: null as data: null as
| Prisma.ApbDesaGetPayload<{ | Prisma.ApbDesaGetPayload<{
include: { include: {
pendapatan: true; pendapatan: true;
belanja: true; belanja: true;
pembiayaan: true; pembiayaan: true;
}; };
}>[] }>[]
| null, | null,
page: 1,
totalPages: 1,
loading: false, loading: false,
async load() { search: "",
load: async (page = 1, limit = 10, search = "") => {
ApbDesa.findMany.loading = true; // ✅ Akses langsung via nama path
ApbDesa.findMany.page = page;
ApbDesa.findMany.search = search;
try { try {
this.loading = true; const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.ekonomi.pendapatanaslidesa.apbdesa[ const res = await ApiFetch.api.ekonomi.pendapatanaslidesa.apbdesa[
"find-many" "find-many"
].get(); ].get({ query });
if (res.status === 200) {
this.data = res.data?.data ?? []; if (res.status === 200 && res.data?.success) {
ApbDesa.findMany.data = res.data.data ?? [];
ApbDesa.findMany.totalPages =
res.data.totalPages ?? 1;
} else { } else {
toast.error(res.data?.message || "Gagal mengambil APB Desa"); ApbDesa.findMany.data = [];
ApbDesa.findMany.totalPages = 1;
} }
} catch (error) { } catch (err) {
console.error("Find many error:", error); console.error("Gagal fetch APB Desa paginated:", err);
toast.error("Gagal mengambil APB Desa"); ApbDesa.findMany.data = [];
ApbDesa.findMany.totalPages = 1;
} finally { } finally {
this.loading = false; ApbDesa.findMany.loading = false;
} }
}, },
}, },
@@ -106,13 +124,13 @@ const ApbDesa = proxy({
throw new Error("Gagal mengambil APB Desa"); throw new Error("Gagal mengambil APB Desa");
} }
const result = await response.json(); const result = await response.json();
if (!result.success) { if (!result.success) {
throw new Error(result.message || "Gagal memuat APB Desa"); throw new Error(result.message || "Gagal memuat APB Desa");
} }
const data = result.data; const data = result.data;
this.id = id; this.id = id;
this.form = { this.form = {
tahun: data.tahun || 0, tahun: data.tahun || 0,
@@ -120,7 +138,7 @@ const ApbDesa = proxy({
belanjaIds: data.belanja?.map((b: any) => b.id) || [], belanjaIds: data.belanja?.map((b: any) => b.id) || [],
pembiayaanIds: data.pembiayaan?.map((p: any) => p.id) || [], pembiayaanIds: data.pembiayaan?.map((p: any) => p.id) || [],
}; };
return data; return data;
} catch (error) { } catch (error) {
console.error("Error loading APB Desa:", error); console.error("Error loading APB Desa:", error);
@@ -189,7 +207,7 @@ const ApbDesa = proxy({
data: null as Prisma.ApbDesaGetPayload<{ data: null as Prisma.ApbDesaGetPayload<{
include: { pendapatan: true; belanja: true; pembiayaan: true }; include: { pendapatan: true; belanja: true; pembiayaan: true };
}> | null, }> | null,
async load(id: string) { async load(id: string) {
try { try {
const response = await fetch( const response = await fetch(
@@ -199,11 +217,11 @@ const ApbDesa = proxy({
throw new Error("Gagal mengambil detail APB Desa"); throw new Error("Gagal mengambil detail APB Desa");
} }
const result = await response.json(); const result = await response.json();
if (!result.success) { if (!result.success) {
throw new Error(result.message || "Gagal mengambil data"); throw new Error(result.message || "Gagal mengambil data");
} }
this.data = result.data; // ✅ fix utama di sini this.data = result.data; // ✅ fix utama di sini
return result.data; return result.data;
} catch (error) { } catch (error) {
@@ -264,34 +282,32 @@ const pendapatan = proxy({
data: null as any[] | null, data: null as any[] | null,
page: 1, page: 1,
totalPages: 1, totalPages: 1,
total: 0,
loading: false, loading: false,
load: async (page = 1, limit = 10) => { search: "",
// Change to arrow function load: async (page = 1, limit = 10, search = "") => {
pendapatan.findMany.loading = true; // Use the full path to access the property pendapatan.findMany.loading = true; // ✅ Akses langsung via nama path
pendapatan.findMany.page = page; pendapatan.findMany.page = page;
pendapatan.findMany.search = search;
try { try {
const res = const query: any = { page, limit };
await ApiFetch.api.ekonomi.pendapatanaslidesa.pendapatanasli[ if (search) query.search = search;
"find-many"
].get({ const res = await ApiFetch.api.ekonomi.pendapatanaslidesa.pendapatanasli[
query: { page, limit }, "find-many"
}); ].get({ query });
if (res.status === 200 && res.data?.success) { if (res.status === 200 && res.data?.success) {
pendapatan.findMany.data = res.data.data || []; pendapatan.findMany.data = res.data.data ?? [];
pendapatan.findMany.total = res.data.total || 0; pendapatan.findMany.totalPages =
pendapatan.findMany.totalPages = res.data.totalPages || 1; res.data.totalPages ?? 1;
} else { } else {
console.error("Failed to load pendapatan:", res.data?.message);
pendapatan.findMany.data = []; pendapatan.findMany.data = [];
pendapatan.findMany.total = 0;
pendapatan.findMany.totalPages = 1; pendapatan.findMany.totalPages = 1;
} }
} catch (error) { } catch (err) {
console.error("Error loading pendapatan:", error); console.error("Gagal fetch pendapatan asli desa paginated:", err);
pendapatan.findMany.data = []; pendapatan.findMany.data = [];
pendapatan.findMany.total = 0;
pendapatan.findMany.totalPages = 1; pendapatan.findMany.totalPages = 1;
} finally { } finally {
pendapatan.findMany.loading = false; pendapatan.findMany.loading = false;
@@ -308,12 +324,15 @@ const pendapatan = proxy({
return null; return null;
} }
try { try {
const response = await fetch(`/api/ekonomi/pendapatanaslidesa/pendapatanasli/${id}`, { const response = await fetch(
method: "GET", `/api/ekonomi/pendapatanaslidesa/pendapatanasli/${id}`,
headers: { {
"Content-Type": "application/json", method: "GET",
}, headers: {
}); "Content-Type": "application/json",
},
}
);
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
@@ -349,16 +368,19 @@ const pendapatan = proxy({
try { try {
pendapatan.update.loading = true; pendapatan.update.loading = true;
const response = await fetch(`/api/ekonomi/pendapatanaslidesa/pendapatanasli/${this.id}`, { const response = await fetch(
method: "PUT", `/api/ekonomi/pendapatanaslidesa/pendapatanasli/${this.id}`,
headers: { {
"Content-Type": "application/json", method: "PUT",
}, headers: {
body: JSON.stringify({ "Content-Type": "application/json",
name: this.form.name, },
value: this.form.value, body: JSON.stringify({
}), name: this.form.name,
}); value: this.form.value,
}),
}
);
if (!response.ok) { if (!response.ok) {
const errorData = await response.json().catch(() => ({})); const errorData = await response.json().catch(() => ({}));
throw new Error( throw new Error(
@@ -495,23 +517,37 @@ const belanja = proxy({
name: string; name: string;
value: number; value: number;
}>, }>,
page: 1,
totalPages: 1,
loading: false, loading: false,
async load() { search: "",
load: async (page = 1, limit = 10, search = "") => {
belanja.findMany.loading = true; // ✅ Akses langsung via nama path
belanja.findMany.page = page;
belanja.findMany.search = search;
try { try {
this.loading = true; const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.ekonomi.pendapatanaslidesa.belanja[ const res = await ApiFetch.api.ekonomi.pendapatanaslidesa.belanja[
"find-many" "find-many"
].get(); ].get({ query });
if (res.status === 200) {
this.data = res.data?.data ?? []; if (res.status === 200 && res.data?.success) {
belanja.findMany.data = res.data.data ?? [];
belanja.findMany.totalPages =
res.data.totalPages ?? 1;
} else { } else {
toast.error(res.data?.message || "Gagal mengambil Belanja"); belanja.findMany.data = [];
belanja.findMany.totalPages = 1;
} }
} catch (error) { } catch (err) {
console.error("Find many error:", error); console.error("Gagal fetch Belanja paginated:", err);
toast.error("Gagal mengambil Belanja"); belanja.findMany.data = [];
belanja.findMany.totalPages = 1;
} finally { } finally {
this.loading = false; belanja.findMany.loading = false;
} }
}, },
}, },
@@ -525,12 +561,15 @@ const belanja = proxy({
return null; return null;
} }
try { try {
const response = await fetch(`/api/ekonomi/pendapatanaslidesa/belanja/${id}`, { const response = await fetch(
method: "GET", `/api/ekonomi/pendapatanaslidesa/belanja/${id}`,
headers: { {
"Content-Type": "application/json", method: "GET",
}, headers: {
}); "Content-Type": "application/json",
},
}
);
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
@@ -566,16 +605,19 @@ const belanja = proxy({
try { try {
belanja.update.loading = true; belanja.update.loading = true;
const response = await fetch(`/api/ekonomi/pendapatanaslidesa/belanja/${this.id}`, { const response = await fetch(
method: "PUT", `/api/ekonomi/pendapatanaslidesa/belanja/${this.id}`,
headers: { {
"Content-Type": "application/json", method: "PUT",
}, headers: {
body: JSON.stringify({ "Content-Type": "application/json",
name: this.form.name, },
value: this.form.value, body: JSON.stringify({
}), name: this.form.name,
}); value: this.form.value,
}),
}
);
if (!response.ok) { if (!response.ok) {
const errorData = await response.json().catch(() => ({})); const errorData = await response.json().catch(() => ({}));
throw new Error( throw new Error(
@@ -710,23 +752,37 @@ const pembiayaan = proxy({
name: string; name: string;
value: number; value: number;
}>, }>,
page: 1,
totalPages: 1,
loading: false, loading: false,
async load() { search: "",
load: async (page = 1, limit = 10, search = "") => {
pembiayaan.findMany.loading = true; // ✅ Akses langsung via nama path
pembiayaan.findMany.page = page;
pembiayaan.findMany.search = search;
try { try {
this.loading = true; const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.ekonomi.pendapatanaslidesa.pembiayaan[ const res = await ApiFetch.api.ekonomi.pendapatanaslidesa.pembiayaan[
"find-many" "find-many"
].get(); ].get({ query });
if (res.status === 200) {
this.data = res.data?.data ?? []; if (res.status === 200 && res.data?.success) {
pembiayaan.findMany.data = res.data.data ?? [];
pembiayaan.findMany.totalPages =
res.data.totalPages ?? 1;
} else { } else {
toast.error(res.data?.message || "Gagal mengambil Pembiayaan"); pembiayaan.findMany.data = [];
pembiayaan.findMany.totalPages = 1;
} }
} catch (error) { } catch (err) {
console.error("Find many error:", error); console.error("Gagal fetch Pembiayaan paginated:", err);
toast.error("Gagal mengambil Pembiayaan"); pembiayaan.findMany.data = [];
pembiayaan.findMany.totalPages = 1;
} finally { } finally {
this.loading = false; pembiayaan.findMany.loading = false;
} }
}, },
}, },
@@ -740,12 +796,15 @@ const pembiayaan = proxy({
return null; return null;
} }
try { try {
const response = await fetch(`/api/ekonomi/pendapatanaslidesa/pembiayaan/${id}`, { const response = await fetch(
method: "GET", `/api/ekonomi/pendapatanaslidesa/pembiayaan/${id}`,
headers: { {
"Content-Type": "application/json", method: "GET",
}, headers: {
}); "Content-Type": "application/json",
},
}
);
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
@@ -781,16 +840,19 @@ const pembiayaan = proxy({
try { try {
pembiayaan.update.loading = true; pembiayaan.update.loading = true;
const response = await fetch(`/api/ekonomi/pendapatanaslidesa/pembiayaan/${this.id}`, { const response = await fetch(
method: "PUT", `/api/ekonomi/pendapatanaslidesa/pembiayaan/${this.id}`,
headers: { {
"Content-Type": "application/json", method: "PUT",
}, headers: {
body: JSON.stringify({ "Content-Type": "application/json",
name: this.form.name, },
value: this.form.value, body: JSON.stringify({
}), name: this.form.name,
}); value: this.form.value,
}),
}
);
if (!response.ok) { if (!response.ok) {
const errorData = await response.json().catch(() => ({})); const errorData = await response.json().catch(() => ({}));
throw new Error( throw new Error(

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch"; import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
@@ -71,12 +72,37 @@ const demografiPekerjaan = proxy({
omit: { isActive: true }; omit: { isActive: true };
}>[] }>[]
| null, | null,
async load() { page: 1,
const res = await ApiFetch.api.ekonomi.demografipekerjaan[ totalPages: 1,
"find-many" loading: false,
].get(); search: "",
if (res.status === 200) { load: async (page = 1, limit = 10, search = "") => {
demografiPekerjaan.findMany.data = res.data?.data ?? []; demografiPekerjaan.findMany.loading = true; // ✅ Akses langsung via nama path
demografiPekerjaan.findMany.page = page;
demografiPekerjaan.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.ekonomi.demografipekerjaan[
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
demografiPekerjaan.findMany.data = res.data.data ?? [];
demografiPekerjaan.findMany.totalPages =
res.data.totalPages ?? 1;
} else {
demografiPekerjaan.findMany.data = [];
demografiPekerjaan.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch demografi pekerjaan paginated:", err);
demografiPekerjaan.findMany.data = [];
demografiPekerjaan.findMany.totalPages = 1;
} finally {
demografiPekerjaan.findMany.loading = false;
} }
}, },
}, },
@@ -194,4 +220,4 @@ const demografiPekerjaan = proxy({
}, },
}, },
}); });
export default demografiPekerjaan export default demografiPekerjaan;

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch"; import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
@@ -69,16 +70,37 @@ const jumlahPendudukMiskin = proxy({
select: { id: true; year: true; totalPoorPopulation: true }; select: { id: true; year: true; totalPoorPopulation: true };
}>[] }>[]
| null, | null,
loading: false, page: 1,
async load() { totalPages: 1,
const res = await ApiFetch.api.ekonomi.jumlahpendudukmiskin[ loading: false,
"find-many" search: "",
].get(); load: async (page = 1, limit = 10, search = "") => {
if (res.status === 200) { jumlahPendudukMiskin.findMany.loading = true; // ✅ Akses langsung via nama path
jumlahPendudukMiskin.findMany.data = res.data?.data ?? []; jumlahPendudukMiskin.findMany.page = page;
} jumlahPendudukMiskin.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.ekonomi.jumlahpendudukmiskin["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
jumlahPendudukMiskin.findMany.data = res.data.data ?? [];
jumlahPendudukMiskin.findMany.totalPages = res.data.totalPages ?? 1;
} else {
jumlahPendudukMiskin.findMany.data = [];
jumlahPendudukMiskin.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch jumlah penduduk miskin paginated:", err);
jumlahPendudukMiskin.findMany.data = [];
jumlahPendudukMiskin.findMany.totalPages = 1;
} finally {
jumlahPendudukMiskin.findMany.loading = false;
}
},
}, },
},
findUnique: { findUnique: {
data: null as Prisma.GrafikJumlahPendudukMiskinGetPayload<{ data: null as Prisma.GrafikJumlahPendudukMiskinGetPayload<{
select: { id: true; year: true; totalPoorPopulation: true }; select: { id: true; year: true; totalPoorPopulation: true };

View File

@@ -15,7 +15,8 @@ const templateJumlahPengngguran = z.object({
uneducatedUnemployment: z uneducatedUnemployment: z
.number() .number()
.min(1, "Pengangguran tidak pendidikan harus diisi"), .min(1, "Pengangguran tidak pendidikan harus diisi"),
percentageChange: z.number().min(0, "Persentase perubahan harus diisi"), percentageChange: z.number({ invalid_type_error: "Persentase perubahan harus angka" }),
}); });
type JumlahPengangguran = { type JumlahPengangguran = {
@@ -29,7 +30,7 @@ type JumlahPengangguran = {
const jumlahPengangguranForm: JumlahPengangguran = { const jumlahPengangguranForm: JumlahPengangguran = {
month: "", month: "",
year: 0, year: new Date().getFullYear(), // Default to current year
totalUnemployment: 0, totalUnemployment: 0,
educatedUnemployment: 0, educatedUnemployment: 0,
uneducatedUnemployment: 0, uneducatedUnemployment: 0,
@@ -60,13 +61,21 @@ const jumlahPengangguran = proxy({
form: jumlahPengangguranForm, form: jumlahPengangguranForm,
loading: false, loading: false,
async create() { async create() {
const cek = templateJumlahPengngguran.safeParse( // Ensure all number fields are actual numbers
jumlahPengangguran.create.form const formData = {
); ...jumlahPengangguran.create.form,
year: Number(jumlahPengangguran.create.form.year) || new Date().getFullYear(),
totalUnemployment: Number(jumlahPengangguran.create.form.totalUnemployment) || 0,
educatedUnemployment: Number(jumlahPengangguran.create.form.educatedUnemployment) || 0,
uneducatedUnemployment: Number(jumlahPengangguran.create.form.uneducatedUnemployment) || 0,
percentageChange: Number(jumlahPengangguran.create.form.percentageChange) || 0,
};
const cek = templateJumlahPengngguran.safeParse(formData);
if (!cek.success) { if (!cek.success) {
const err = `[${cek.error.issues const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`) .map((v) => `${v.path.join(".")} (${v.message})`)
.join("\n")}] required`; .join("\n")}]`;
toast.error(err); toast.error(err);
return null; return null;
} }
@@ -78,7 +87,7 @@ const jumlahPengangguran = proxy({
].post(jumlahPengangguran.create.form); ].post(jumlahPengangguran.create.form);
if (res.status === 200) { if (res.status === 200) {
const id = res.data?.data?.id; const id = res.data?.id;
if (id) { if (id) {
toast.success("Success create"); toast.success("Success create");
jumlahPengangguran.create.form = { ...jumlahPengangguranForm }; jumlahPengangguran.create.form = { ...jumlahPengangguranForm };
@@ -103,16 +112,40 @@ const jumlahPengangguran = proxy({
omit: { isActive: true }; omit: { isActive: true };
}>[] }>[]
| null, | null,
async load() { page: 1,
const res = totalPages: 1,
await ApiFetch.api.ekonomi.jumlahpengangguran.detaildatapengangguran[ loading: false,
"find-many" search: "",
].get(); load: async (page = 1, limit = 10, search = "") => {
if (res.status === 200) { jumlahPengangguran.findMany.loading = true; // ✅ Akses langsung via nama path
jumlahPengangguran.findMany.data = res.data?.data ?? []; jumlahPengangguran.findMany.page = page;
} jumlahPengangguran.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.ekonomi.jumlahpengangguran.detaildatapengangguran[
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
jumlahPengangguran.findMany.data = res.data.data ?? [];
jumlahPengangguran.findMany.totalPages =
res.data.totalPages ?? 1;
} else {
jumlahPengangguran.findMany.data = [];
jumlahPengangguran.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch jumlah pengangguran paginated:", err);
jumlahPengangguran.findMany.data = [];
jumlahPengangguran.findMany.totalPages = 1;
} finally {
jumlahPengangguran.findMany.loading = false;
}
},
}, },
},
findUnique: { findUnique: {
data: null as Prisma.DetailDataPengangguranGetPayload<{ data: null as Prisma.DetailDataPengangguranGetPayload<{

View File

@@ -13,6 +13,7 @@ const templateForm = z.object({
gaji: z.string(), gaji: z.string(),
deskripsi: z.string(), deskripsi: z.string(),
kualifikasi: z.string(), kualifikasi: z.string(),
notelp: z.string(),
}); });
const defaultForm = { const defaultForm = {
@@ -23,6 +24,7 @@ const defaultForm = {
gaji: "", gaji: "",
deskripsi: "", deskripsi: "",
kualifikasi: "", kualifikasi: "",
notelp: "",
}; };
const lowonganKerjaState = proxy({ const lowonganKerjaState = proxy({
@@ -179,6 +181,7 @@ const lowonganKerjaState = proxy({
gaji: data.gaji, gaji: data.gaji,
deskripsi: data.deskripsi, deskripsi: data.deskripsi,
kualifikasi: data.kualifikasi, kualifikasi: data.kualifikasi,
notelp: data.notelp,
}; };
return data; return data;
} else { } else {
@@ -218,6 +221,7 @@ const lowonganKerjaState = proxy({
gaji: this.form.gaji, gaji: this.form.gaji,
deskripsi: this.form.deskripsi, deskripsi: this.form.deskripsi,
kualifikasi: this.form.kualifikasi, kualifikasi: this.form.kualifikasi,
notelp: this.form.notelp,
}), }),
}); });
if (!response.ok) { if (!response.ok) {

View File

@@ -12,6 +12,7 @@ const templatePasarDesaForm = z.object({
imageId: z.string().min(1, "Gambar wajib dipilih"), imageId: z.string().min(1, "Gambar wajib dipilih"),
rating: z.number().min(1, "Rating minimal 1"), rating: z.number().min(1, "Rating minimal 1"),
kategoriId: z.array(z.string()).min(1, "Minimal pilih satu kategori"), kategoriId: z.array(z.string()).min(1, "Minimal pilih satu kategori"),
kontak: z.string().min(1, "Kontak wajib diisi"),
}); });
const defaultPasarDesaForm = { const defaultPasarDesaForm = {
@@ -21,6 +22,7 @@ const defaultPasarDesaForm = {
imageId: "", imageId: "",
rating: 0, rating: 0,
kategoriId: [] as string[], kategoriId: [] as string[],
kontak: "",
}; };
const pasarDesa = proxy({ const pasarDesa = proxy({
@@ -188,6 +190,7 @@ const pasarDesa = proxy({
imageId: data.imageId, imageId: data.imageId,
rating: data.rating, rating: data.rating,
kategoriId: data.kategoriId, kategoriId: data.kategoriId,
kontak: data.kontak,
}; };
return data; return data;
} else { } else {
@@ -225,6 +228,7 @@ const pasarDesa = proxy({
imageId: this.form.imageId, imageId: this.form.imageId,
rating: this.form.rating, rating: this.form.rating,
kategoriId: this.form.kategoriId, kategoriId: this.form.kategoriId,
kontak: this.form.kontak,
}), }),
}); });
if (!response.ok) { if (!response.ok) {
@@ -336,6 +340,40 @@ const kategoriProduk = proxy({
} }
}, },
}, },
// ✅ Versi findManyAll (ambil semua tanpa pagination)
findManyAll: {
data: null as
| Prisma.KategoriProdukGetPayload<{
omit: { isActive: true };
}>[]
| null,
loading: false,
search: "",
load: async (search = "") => {
kategoriProduk.findManyAll.loading = true;
kategoriProduk.findManyAll.search = search;
try {
const query: any = {};
if (search) query.search = search;
const res = await ApiFetch.api.ekonomi.kategoriproduk["find-many-all"].get({
query,
});
if (res.status === 200 && res.data?.success) {
kategoriProduk.findManyAll.data = res.data.data ?? [];
} else {
kategoriProduk.findManyAll.data = [];
}
} catch (err) {
console.error("Gagal fetch kategori produk (all):", err);
kategoriProduk.findManyAll.data = [];
} finally {
kategoriProduk.findManyAll.loading = false;
}
},
},
findUnique: { findUnique: {
data: null as Prisma.KategoriProdukGetPayload<{ data: null as Prisma.KategoriProdukGetPayload<{
omit: { isActive: true }; omit: { isActive: true };

View File

@@ -8,7 +8,7 @@ import { z } from "zod";
const templateForm = z.object({ const templateForm = z.object({
nama: z.string().min(1, "Nama minimal 1 karakter"), nama: z.string().min(1, "Nama minimal 1 karakter"),
deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"), deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
ikonUrl: z.string().optional(), icon: z.string().min(1, "Icon minimal 1 karakter"),
statistik: z.object({ statistik: z.object({
tahun: z.string().min(1, "Tahun minimal 1 karakter"), tahun: z.string().min(1, "Tahun minimal 1 karakter"),
jumlah: z.string().min(1, "Jumlah minimal 1 karakter"), jumlah: z.string().min(1, "Jumlah minimal 1 karakter"),
@@ -18,7 +18,7 @@ const templateForm = z.object({
const defaultForm = { const defaultForm = {
nama: "", nama: "",
deskripsi: "", deskripsi: "",
ikonUrl: "", icon: "",
statistik: { statistik: {
tahun: "", tahun: "",
jumlah: "", jumlah: "",
@@ -148,7 +148,7 @@ const programKemiskinanState = proxy({
this.form = { this.form = {
nama: data.nama, nama: data.nama,
deskripsi: data.deskripsi, deskripsi: data.deskripsi,
ikonUrl: data.ikonUrl || "", icon: data.icon,
statistik: { statistik: {
tahun: data.statistik.tahun, tahun: data.statistik.tahun,
jumlah: data.statistik.jumlah, jumlah: data.statistik.jumlah,
@@ -189,7 +189,7 @@ const programKemiskinanState = proxy({
body: JSON.stringify({ body: JSON.stringify({
nama: this.form.nama, nama: this.form.nama,
deskripsi: this.form.deskripsi, deskripsi: this.form.deskripsi,
ikonUrl: this.form.ikonUrl, icon: this.form.icon,
statistik: { statistik: {
tahun: this.form.statistik.tahun, tahun: this.form.statistik.tahun,
jumlah: this.form.statistik.jumlah, jumlah: this.form.statistik.jumlah,

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch"; import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
@@ -76,13 +77,37 @@ const grafikSektorUnggulan = proxy({
}; };
}>[] }>[]
| null, | null,
page: 1,
totalPages: 1,
loading: false, loading: false,
async load() { search: "",
const res = await ApiFetch.api.ekonomi.sektourunggulandesa[ load: async (page = 1, limit = 10, search = "") => {
"find-many" grafikSektorUnggulan.findMany.loading = true; // ✅ Akses langsung via nama path
].get(); grafikSektorUnggulan.findMany.page = page;
if (res.status === 200) { grafikSektorUnggulan.findMany.search = search;
grafikSektorUnggulan.findMany.data = res.data?.data ?? [];
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.ekonomi.sektourunggulandesa[
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
grafikSektorUnggulan.findMany.data = res.data.data ?? [];
grafikSektorUnggulan.findMany.totalPages =
res.data.totalPages ?? 1;
} else {
grafikSektorUnggulan.findMany.data = [];
grafikSektorUnggulan.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch sektor unggulan desa paginated:", err);
grafikSektorUnggulan.findMany.data = [];
grafikSektorUnggulan.findMany.totalPages = 1;
} finally {
grafikSektorUnggulan.findMany.loading = false;
} }
}, },
}, },

View File

@@ -161,18 +161,34 @@ const posisiOrganisasi = proxy({
deskripsi: string | null; deskripsi: string | null;
hierarki: number; hierarki: number;
}>, }>,
async load() { page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
posisiOrganisasi.findMany.loading = true; // ✅ Akses langsung via nama path
posisiOrganisasi.findMany.page = page;
posisiOrganisasi.findMany.search = search;
try { try {
const res = await ApiFetch.api.ekonomi["struktur-organisasi"][ const query: any = { page, limit };
"posisi-organisasi" if (search) query.search = search;
]["find-many"].get();
if (res.status === 200) { const res = await ApiFetch.api.ekonomi["struktur-organisasi"]["posisi-organisasi"]["find-many"].get({ query });
// The API now returns the id field, so we can use it directly
this.data = res.data?.data ?? []; if (res.status === 200 && res.data?.success) {
posisiOrganisasi.findMany.data = res.data.data ?? [];
posisiOrganisasi.findMany.totalPages = res.data.totalPages ?? 1;
} else {
posisiOrganisasi.findMany.data = [];
posisiOrganisasi.findMany.totalPages = 1;
} }
} catch (error) { } catch (err) {
console.error("Find many error:", error); console.error("Gagal fetch posisi organisasi paginated:", err);
this.data = []; posisiOrganisasi.findMany.data = [];
posisiOrganisasi.findMany.totalPages = 1;
} finally {
posisiOrganisasi.findMany.loading = false;
} }
}, },
}, },

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch"; import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
@@ -75,16 +76,37 @@ const grafikBerdasarkanUsiaKerjaNganggur = proxy({
omit: { isActive: true }; omit: { isActive: true };
}>[] }>[]
| null, | null,
loading: false, page: 1,
async load() { totalPages: 1,
const res = await ApiFetch.api.ekonomi.grafikusiakerjayangmenganggur[ loading: false,
"find-many" search: "",
].get(); load: async (page = 1, limit = 10, search = "") => {
if (res.status === 200) { grafikBerdasarkanUsiaKerjaNganggur.findMany.loading = true; // ✅ Akses langsung via nama path
grafikBerdasarkanUsiaKerjaNganggur.findMany.data = res.data?.data ?? []; grafikBerdasarkanUsiaKerjaNganggur.findMany.page = page;
} grafikBerdasarkanUsiaKerjaNganggur.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.ekonomi.grafikusiakerjayangmenganggur["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
grafikBerdasarkanUsiaKerjaNganggur.findMany.data = res.data.data ?? [];
grafikBerdasarkanUsiaKerjaNganggur.findMany.totalPages = res.data.totalPages ?? 1;
} else {
grafikBerdasarkanUsiaKerjaNganggur.findMany.data = [];
grafikBerdasarkanUsiaKerjaNganggur.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch grafik berdasarkan usia kerja yang menganggur paginated:", err);
grafikBerdasarkanUsiaKerjaNganggur.findMany.data = [];
grafikBerdasarkanUsiaKerjaNganggur.findMany.totalPages = 1;
} finally {
grafikBerdasarkanUsiaKerjaNganggur.findMany.loading = false;
}
},
}, },
},
findUnique: { findUnique: {
data: null as Prisma.GrafikMenganggurBerdasarkanUsiaGetPayload<{ data: null as Prisma.GrafikMenganggurBerdasarkanUsiaGetPayload<{
omit: { isActive: true }; omit: { isActive: true };
@@ -259,15 +281,36 @@ const grafikBerdasarkanPendidikan = proxy({
omit: { isActive: true }; omit: { isActive: true };
}>[] }>[]
| null, | null,
loading: false, page: 1,
async load() { totalPages: 1,
const res = await ApiFetch.api.ekonomi.grafikmenganggurberdasarkanpendidikan[ loading: false,
"find-many" search: "",
].get(); load: async (page = 1, limit = 10, search = "") => {
if (res.status === 200) { grafikBerdasarkanPendidikan.findMany.loading = true; // ✅ Akses langsung via nama path
grafikBerdasarkanPendidikan.findMany.data = res.data?.data ?? []; grafikBerdasarkanPendidikan.findMany.page = page;
} grafikBerdasarkanPendidikan.findMany.search = search;
},
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.ekonomi.grafikmenganggurberdasarkanpendidikan["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
grafikBerdasarkanPendidikan.findMany.data = res.data.data ?? [];
grafikBerdasarkanPendidikan.findMany.totalPages = res.data.totalPages ?? 1;
} else {
grafikBerdasarkanPendidikan.findMany.data = [];
grafikBerdasarkanPendidikan.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch grafik berdasarkan pendidikan paginated:", err);
grafikBerdasarkanPendidikan.findMany.data = [];
grafikBerdasarkanPendidikan.findMany.totalPages = 1;
} finally {
grafikBerdasarkanPendidikan.findMany.loading = false;
}
},
}, },
findUnique: { findUnique: {
data: null as Prisma.GrafikMenganggurBerdasarkanPendidikanGetPayload<{ data: null as Prisma.GrafikMenganggurBerdasarkanPendidikanGetPayload<{

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch"; import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
@@ -61,10 +62,37 @@ const ajukanIdeInovatifState = proxy({
}; };
}>[] }>[]
| null, | null,
async load() { page: 1,
const res = await ApiFetch.api.inovasi.ajukanideinovatif["find-many"].get(); totalPages: 1,
if (res.status === 200) { loading: false,
ajukanIdeInovatifState.findMany.data = res.data?.data ?? []; search: "",
load: async (page = 1, limit = 10, search = "") => {
ajukanIdeInovatifState.findMany.loading = true; // ✅ Akses langsung via nama path
ajukanIdeInovatifState.findMany.page = page;
ajukanIdeInovatifState.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res =
await ApiFetch.api.inovasi.ajukanideinovatif[
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
ajukanIdeInovatifState.findMany.data = res.data.data ?? [];
ajukanIdeInovatifState.findMany.totalPages = res.data.totalPages ?? 1;
} else {
ajukanIdeInovatifState.findMany.data = [];
ajukanIdeInovatifState.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch ajukan ide inovatif paginated:", err);
ajukanIdeInovatifState.findMany.data = [];
ajukanIdeInovatifState.findMany.totalPages = 1;
} finally {
ajukanIdeInovatifState.findMany.loading = false;
} }
}, },
}, },
@@ -97,16 +125,21 @@ const ajukanIdeInovatifState = proxy({
try { try {
ajukanIdeInovatifState.delete.loading = true; ajukanIdeInovatifState.delete.loading = true;
const response = await fetch(`/api/inovasi/ajukanideinovatif/del/${id}`, { const response = await fetch(
method: "DELETE", `/api/inovasi/ajukanideinovatif/del/${id}`,
headers: { {
"Content-Type": "application/json", method: "DELETE",
}, headers: {
}); "Content-Type": "application/json",
},
}
);
const result = await response.json(); const result = await response.json();
if (response.ok) { if (response.ok) {
toast.success(result.message || "Ajukan Ide Inovatif berhasil dihapus"); toast.success(
result.message || "Ajukan Ide Inovatif berhasil dihapus"
);
await ajukanIdeInovatifState.findMany.load(); await ajukanIdeInovatifState.findMany.load();
} else { } else {
toast.error(result?.message || "Gagal menghapus ajukan ide inovatif"); toast.error(result?.message || "Gagal menghapus ajukan ide inovatif");

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch"; import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
@@ -54,19 +55,20 @@ const administrasiOnline = proxy({
}, },
findMany: { findMany: {
data: null as Array< data: null as Array<
Prisma.AdministrasiOnlineGetPayload<{ Prisma.AdministrasiOnlineGetPayload<{
include: { include: {
jenisLayanan: true; jenisLayanan: true;
}; };
}> }>
> | null, > | null,
page: 1, page: 1,
totalPages: 1, totalPages: 1,
loading: false, loading: false,
search: "",
async load(page = 1, limit = 10) { async load(page = 1, limit = 10, search = "") {
administrasiOnline.findMany.loading = true; administrasiOnline.findMany.loading = true;
administrasiOnline.findMany.page = page; administrasiOnline.findMany.page = page;
administrasiOnline.findMany.search = search;
try { try {
const res = const res =
await ApiFetch.api.inovasi.layananonlinedesa.administrasionline[ await ApiFetch.api.inovasi.layananonlinedesa.administrasionline[
@@ -75,6 +77,7 @@ const administrasiOnline = proxy({
query: { query: {
page, page,
limit, limit,
search,
}, },
}); });
@@ -91,10 +94,10 @@ const administrasiOnline = proxy({
}, },
findUnique: { findUnique: {
data: null as Prisma.AdministrasiOnlineGetPayload<{ data: null as Prisma.AdministrasiOnlineGetPayload<{
include: { include: {
jenisLayanan: true; jenisLayanan: true;
}; };
}> | null, }> | null,
async load(id: string) { async load(id: string) {
try { try {
const res = await fetch( const res = await fetch(
@@ -199,13 +202,37 @@ const jenisLayanan = proxy({
nama: string; nama: string;
deskripsi: string; deskripsi: string;
}> | null, }> | null,
async load() { page: 1,
const res = totalPages: 1,
await ApiFetch.api.inovasi.layananonlinedesa.administrasionline.jenislayanan[ loading: false,
"find-many" search: "",
].get(); load: async (page = 1, limit = 10, search = "") => {
if (res.status === 200) { jenisLayanan.findMany.loading = true; // ✅ Akses langsung via nama path
jenisLayanan.findMany.data = res.data?.data ?? []; jenisLayanan.findMany.page = page;
jenisLayanan.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res =
await ApiFetch.api.inovasi.layananonlinedesa.administrasionline.jenislayanan[
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
jenisLayanan.findMany.data = res.data.data ?? [];
jenisLayanan.findMany.totalPages = res.data.totalPages ?? 1;
} else {
jenisLayanan.findMany.data = [];
jenisLayanan.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch jenis layanan paginated:", err);
jenisLayanan.findMany.data = [];
jenisLayanan.findMany.totalPages = 1;
} finally {
jenisLayanan.findMany.loading = false;
} }
}, },
}, },
@@ -403,7 +430,9 @@ const templatePengaduanMasyarakatForm = z.object({
nik: z.string().min(1, "NIK minimal 1 karakter"), nik: z.string().min(1, "NIK minimal 1 karakter"),
judulPengaduan: z.string().min(1, "Judul pengaduan minimal 1 karakter"), judulPengaduan: z.string().min(1, "Judul pengaduan minimal 1 karakter"),
lokasiKejadian: z.string().min(1, "Lokasi kejadian minimal 1 karakter"), lokasiKejadian: z.string().min(1, "Lokasi kejadian minimal 1 karakter"),
deskripsiPengaduan: z.string().min(1, "Deskripsi pengaduan minimal 1 karakter"), deskripsiPengaduan: z
.string()
.min(1, "Deskripsi pengaduan minimal 1 karakter"),
jenisPengaduanId: z.string().min(1, "Jenis pengaduan minimal 1 karakter"), jenisPengaduanId: z.string().min(1, "Jenis pengaduan minimal 1 karakter"),
imageId: z.string().min(1, "Image minimal 1 karakter"), imageId: z.string().min(1, "Image minimal 1 karakter"),
}); });
@@ -455,37 +484,42 @@ const pengaduanMasyarakat = proxy({
}, },
findMany: { findMany: {
data: null as Array< data: null as Array<
Prisma.PengaduanMasyarakatGetPayload<{ Prisma.PengaduanMasyarakatGetPayload<{
include: { include: {
jenisPengaduan: true; jenisPengaduan: true;
image: true; image: true;
}; };
}> }>
> | null, > | null,
page: 1, page: 1,
totalPages: 1, totalPages: 1,
loading: false, loading: false,
search: "",
async load(page = 1, limit = 10) { load: async (page = 1, limit = 10, search = "") => {
pengaduanMasyarakat.findMany.loading = true; pengaduanMasyarakat.findMany.loading = true; // ✅ Akses langsung via nama path
pengaduanMasyarakat.findMany.page = page; pengaduanMasyarakat.findMany.page = page;
pengaduanMasyarakat.findMany.search = search;
try { try {
const query: any = { page, limit };
if (search) query.search = search;
const res = const res =
await ApiFetch.api.inovasi.layananonlinedesa.pengaduanmasyarakat[ await ApiFetch.api.inovasi.layananonlinedesa.pengaduanmasyarakat[
"find-many" "find-many"
].get({ ].get({ query });
query: {
page,
limit,
},
});
if (res.status === 200 && res.data?.success) { if (res.status === 200 && res.data?.success) {
pengaduanMasyarakat.findMany.data = res.data.data ?? []; pengaduanMasyarakat.findMany.data = res.data.data ?? [];
pengaduanMasyarakat.findMany.totalPages = res.data.totalPages ?? 1; pengaduanMasyarakat.findMany.totalPages = res.data.totalPages ?? 1;
} else {
pengaduanMasyarakat.findMany.data = [];
pengaduanMasyarakat.findMany.totalPages = 1;
} }
} catch (err) { } catch (err) {
console.error("Gagal fetch pengaduan masyarakat paginated:", err); console.error("Gagal fetch pengaduan masyarakat paginated:", err);
pengaduanMasyarakat.findMany.data = [];
pengaduanMasyarakat.findMany.totalPages = 1;
} finally { } finally {
pengaduanMasyarakat.findMany.loading = false; pengaduanMasyarakat.findMany.loading = false;
} }
@@ -493,11 +527,11 @@ const pengaduanMasyarakat = proxy({
}, },
findUnique: { findUnique: {
data: null as Prisma.PengaduanMasyarakatGetPayload<{ data: null as Prisma.PengaduanMasyarakatGetPayload<{
include: { include: {
jenisPengaduan: true; jenisPengaduan: true;
image: true; image: true;
}; };
}> | null, }> | null,
async load(id: string) { async load(id: string) {
try { try {
const res = await fetch( const res = await fetch(
@@ -507,7 +541,10 @@ const pengaduanMasyarakat = proxy({
const data = await res.json(); const data = await res.json();
pengaduanMasyarakat.findUnique.data = data.data ?? null; pengaduanMasyarakat.findUnique.data = data.data ?? null;
} else { } else {
console.error("Failed to fetch pengaduan masyarakat:", res.statusText); console.error(
"Failed to fetch pengaduan masyarakat:",
res.statusText
);
pengaduanMasyarakat.findUnique.data = null; pengaduanMasyarakat.findUnique.data = null;
} }
} catch (error) { } catch (error) {
@@ -542,7 +579,9 @@ const pengaduanMasyarakat = proxy({
); );
await pengaduanMasyarakat.findMany.load(); // refresh list await pengaduanMasyarakat.findMany.load(); // refresh list
} else { } else {
toast.error(result?.message || "Gagal menghapus pengaduan masyarakat"); toast.error(
result?.message || "Gagal menghapus pengaduan masyarakat"
);
} }
} catch (error) { } catch (error) {
console.error("Gagal delete:", error); console.error("Gagal delete:", error);
@@ -567,7 +606,9 @@ const jenisPengaduan = proxy({
form: { ...defaultJenisPengaduanForm }, form: { ...defaultJenisPengaduanForm },
loading: false, loading: false,
async create() { async create() {
const cek = templateJenisPengaduanForm.safeParse(jenisPengaduan.create.form); const cek = templateJenisPengaduanForm.safeParse(
jenisPengaduan.create.form
);
if (!cek.success) { if (!cek.success) {
const err = `[${cek.error.issues const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`) .map((v) => `${v.path.join(".")}`)
@@ -598,13 +639,37 @@ const jenisPengaduan = proxy({
id: string; id: string;
nama: string; nama: string;
}> | null, }> | null,
async load() { page: 1,
const res = totalPages: 1,
await ApiFetch.api.inovasi.layananonlinedesa.pengaduanmasyarakat.jenispengaduan[ loading: false,
"find-many" search: "",
].get(); load: async (page = 1, limit = 10, search = "") => {
if (res.status === 200) { jenisPengaduan.findMany.loading = true; // ✅ Akses langsung via nama path
jenisPengaduan.findMany.data = res.data?.data ?? []; jenisPengaduan.findMany.page = page;
jenisPengaduan.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res =
await ApiFetch.api.inovasi.layananonlinedesa.pengaduanmasyarakat.jenispengaduan[
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
jenisPengaduan.findMany.data = res.data.data ?? [];
jenisPengaduan.findMany.totalPages = res.data.totalPages ?? 1;
} else {
jenisPengaduan.findMany.data = [];
jenisPengaduan.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch jenis pengaduan paginated:", err);
jenisPengaduan.findMany.data = [];
jenisPengaduan.findMany.totalPages = 1;
} finally {
jenisPengaduan.findMany.loading = false;
} }
}, },
}, },
@@ -693,7 +758,7 @@ const jenisPengaduan = proxy({
const data = result.data; const data = result.data;
this.id = data.id; this.id = data.id;
this.form = { this.form = {
nama: data.nama nama: data.nama,
}; };
return data; return data;
} else { } else {
@@ -709,7 +774,9 @@ const jenisPengaduan = proxy({
}, },
async update() { async update() {
const cek = templateJenisPengaduanForm.safeParse(jenisPengaduan.edit.form); const cek = templateJenisPengaduanForm.safeParse(
jenisPengaduan.edit.form
);
if (!cek.success) { if (!cek.success) {
const err = `[${cek.error.issues const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`) .map((v) => `${v.path.join(".")}`)
@@ -759,7 +826,9 @@ const jenisPengaduan = proxy({
await jenisPengaduan.findMany.load(); // refresh list await jenisPengaduan.findMany.load(); // refresh list
return true; return true;
} else { } else {
throw new Error(result.message || "Gagal mengupdate jenis pengaduan"); throw new Error(
result.message || "Gagal mengupdate jenis pengaduan"
);
} }
} catch (error) { } catch (error) {
// If JSON parsing fails, try to get the response text for better error messages // If JSON parsing fails, try to get the response text for better error messages
@@ -792,7 +861,6 @@ const jenisPengaduan = proxy({
}, },
}); });
const layananonlineDesa = proxy({ const layananonlineDesa = proxy({
administrasiOnline, administrasiOnline,
jenisLayanan, jenisLayanan,

View File

@@ -54,34 +54,32 @@ const programKreatifState = proxy({
data: null as any[] | null, data: null as any[] | null,
page: 1, page: 1,
totalPages: 1, totalPages: 1,
total: 0,
loading: false, loading: false,
load: async (page = 1, limit = 10) => { search: "",
// Change to arrow function load: async (page = 1, limit = 10, search = "") => {
programKreatifState.findMany.loading = true; // Use the full path to access the property programKreatifState.findMany.loading = true; // ✅ Akses langsung via nama path
programKreatifState.findMany.page = page; programKreatifState.findMany.page = page;
programKreatifState.findMany.search = search;
try { try {
const res = await ApiFetch.api.inovasi.programkreatif["find-many"].get({ const query: any = { page, limit };
query: { page, limit }, if (search) query.search = search;
});
const res = await ApiFetch.api.inovasi.programkreatif[
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) { if (res.status === 200 && res.data?.success) {
programKreatifState.findMany.data = res.data.data || []; programKreatifState.findMany.data = res.data.data ?? [];
programKreatifState.findMany.total = res.data.total || 0; programKreatifState.findMany.totalPages =
programKreatifState.findMany.totalPages = res.data.totalPages || 1; res.data.totalPages ?? 1;
} else { } else {
console.error(
"Failed to load grafik berdasarkan jenis kelamin:",
res.data?.message
);
programKreatifState.findMany.data = []; programKreatifState.findMany.data = [];
programKreatifState.findMany.total = 0;
programKreatifState.findMany.totalPages = 1; programKreatifState.findMany.totalPages = 1;
} }
} catch (error) { } catch (err) {
console.error("Error loading grafik berdasarkan jenis kelamin:", error); console.error("Gagal fetch program kreatif paginated:", err);
programKreatifState.findMany.data = []; programKreatifState.findMany.data = [];
programKreatifState.findMany.total = 0;
programKreatifState.findMany.totalPages = 1; programKreatifState.findMany.totalPages = 1;
} finally { } finally {
programKreatifState.findMany.loading = false; programKreatifState.findMany.loading = false;

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch"; import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
@@ -6,26 +7,14 @@ import { z } from "zod";
const templateForm = z.object({ const templateForm = z.object({
nama: z.string().min(1, "Nama minimal 1 karakter"), nama: z.string().min(1, "Nama minimal 1 karakter"),
imageId: z.string().nonempty(), icon: z.string().nonempty(),
kontakItems: z.array( kategoriId: z.array(z.string()).min(1, "Minimal pilih satu kategori"),
z.object({
nama: z.string().min(1, "Nama minimal 1 karakter"),
nomorTelepon: z.string().min(1, "Nomor Telepon minimal 1 karakter"),
imageId: z.string().nonempty(),
})
),
}); });
const defaultForm = { const defaultForm = {
nama: "", nama: "",
imageId: "", icon: "",
kontakItems: [ kategoriId: [] as string[],
{
nama: "",
nomorTelepon: "",
imageId: "",
},
],
}; };
const kontakDaruratKeamananState = proxy({ const kontakDaruratKeamananState = proxy({
@@ -61,20 +50,49 @@ const kontakDaruratKeamananState = proxy({
}, },
}, },
findMany: { findMany: {
data: null as data: null as Array<
| Prisma.KontakDaruratKeamananGetPayload<{ Prisma.KontakDaruratKeamananGetPayload<{
include: { include: {
kontakItems: true; kategori: true;
image: true; kontakItems: {
include: {
kontakItem: true;
};
}; };
}>[] };
| null, }>
async load() { > | null,
const res = await ApiFetch.api.keamanan.kontakdaruratkeamanan[ page: 1,
"find-many" totalPages: 1,
].get(); loading: false,
if (res.status === 200) { search: "",
kontakDaruratKeamananState.findMany.data = res.data?.data ?? []; load: async (page = 1, limit = 10, search = "") => {
kontakDaruratKeamananState.findMany.loading = true; // ✅ Akses langsung via nama path
kontakDaruratKeamananState.findMany.page = page;
kontakDaruratKeamananState.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.keamanan.kontakdaruratkeamanan[
"findMany"
].get({ query });
if (res.status === 200 && res.data?.success) {
kontakDaruratKeamananState.findMany.data = res.data.data ?? [];
kontakDaruratKeamananState.findMany.totalPages =
res.data.totalPages ?? 1;
} else {
kontakDaruratKeamananState.findMany.data = [];
kontakDaruratKeamananState.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch kontak darurat paginated:", err);
kontakDaruratKeamananState.findMany.data = [];
kontakDaruratKeamananState.findMany.totalPages = 1;
} finally {
kontakDaruratKeamananState.findMany.loading = false;
} }
}, },
}, },
@@ -83,10 +101,10 @@ const kontakDaruratKeamananState = proxy({
include: { include: {
kontakItems: { kontakItems: {
include: { include: {
image: true; kontakItem: true;
}; };
}; };
image: true; kategori: true;
}; };
}> | null, }> | null,
loading: false, loading: false,
@@ -168,14 +186,9 @@ const kontakDaruratKeamananState = proxy({
this.id = data.id; this.id = data.id;
this.form = { this.form = {
nama: data.nama, nama: data.nama,
imageId: data.imageId, icon: data.icon || "",
kontakItems: [ kategoriId:
{ data.kontakItems?.map((item: any) => item.kontakItemId) || [],
nama: data.kontakItems.nama,
nomorTelepon: data.kontakItems.nomorTelepon,
imageId: data.kontakItems.imageId,
},
],
}; };
return data; return data;
} else { } else {
@@ -212,14 +225,8 @@ const kontakDaruratKeamananState = proxy({
}, },
body: JSON.stringify({ body: JSON.stringify({
nama: this.form.nama, nama: this.form.nama,
imageId: this.form.imageId, icon: this.form.icon,
kontakItems: [ kategoriId: this.form.kategoriId,
{
nama: this.form.kontakItems[0].nama,
nomorTelepon: this.form.kontakItems[0].nomorTelepon,
imageId: this.form.kontakItems[0].imageId,
},
],
}), }),
} }
); );
@@ -256,4 +263,242 @@ const kontakDaruratKeamananState = proxy({
}, },
}); });
export default kontakDaruratKeamananState; const templateFormItem = z.object({
nama: z.string().min(1, "Nama minimal 1 karakter"),
nomorTelepon: z.string().min(1, "Nomor Telepon minimal 1 karakter"),
icon: z.string().nonempty(),
});
const defaultFormItem = {
nama: "",
nomorTelepon: "",
icon: "",
};
const kontakDaruratItem = proxy({
create: {
form: { ...defaultFormItem },
loading: false,
async create() {
const cek = templateFormItem.safeParse(kontakDaruratItem.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
kontakDaruratItem.create.loading = true;
const res = await ApiFetch.api.keamanan.kontakitem["create"].post(
kontakDaruratItem.create.form
);
if (res.status === 200) {
kontakDaruratItem.findMany.load();
return toast.success("success create");
}
console.log(res);
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
} finally {
kontakDaruratItem.create.loading = false;
}
},
},
findMany: {
data: null as Array<
Prisma.KontakItemGetPayload<{
omit: {
isActive: true;
};
}>
> | null,
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
kontakDaruratItem.findMany.loading = true; // ✅ Akses langsung via nama path
kontakDaruratItem.findMany.page = page;
kontakDaruratItem.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.keamanan.kontakitem["find-many"].get({
query,
});
if (res.status === 200 && res.data?.success) {
kontakDaruratItem.findMany.data = res.data.data ?? [];
kontakDaruratItem.findMany.totalPages = res.data.totalPages ?? 1;
} else {
kontakDaruratItem.findMany.data = [];
kontakDaruratItem.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch kontak darurat paginated:", err);
kontakDaruratItem.findMany.data = [];
kontakDaruratItem.findMany.totalPages = 1;
} finally {
kontakDaruratItem.findMany.loading = false;
}
},
},
findUnique: {
data: null as Prisma.KontakItemGetPayload<{
omit: {
isActive: true;
};
}> | null,
loading: false,
async load(id: string) {
try {
const res = await fetch(`/api/keamanan/kontakitem/${id}`);
if (res.ok) {
const data = await res.json();
kontakDaruratItem.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
kontakDaruratItem.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
kontakDaruratItem.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
kontakDaruratItem.delete.loading = true;
const response = await fetch(`/api/keamanan/kontakitem/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Kontak item berhasil dihapus");
await kontakDaruratItem.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus kontak item");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus kontak item");
} finally {
kontakDaruratItem.delete.loading = false;
}
},
},
update: {
id: "",
form: { ...defaultFormItem },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/keamanan/kontakitem/${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,
nomorTelepon: data.nomorTelepon,
icon: data.icon,
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading kontak darurat:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateFormItem.safeParse(kontakDaruratItem.update.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
kontakDaruratItem.update.loading = true;
const response = await fetch(`/api/keamanan/kontakitem/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
nama: this.form.nama,
nomorTelepon: this.form.nomorTelepon,
icon: this.form.icon,
}),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update kontak item");
await kontakDaruratItem.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal mengupdate kontak item");
}
} catch (error) {
console.error("Error updating kontak item:", error);
toast.error(
error instanceof Error
? error.message
: "Gagal mengupdate kontak item"
);
return false;
} finally {
kontakDaruratItem.update.loading = false;
}
},
reset() {
kontakDaruratItem.update.id = "";
kontakDaruratItem.update.form = { ...defaultFormItem };
},
},
});
const kontakDarurat = proxy({
kontakDaruratKeamananState,
kontakDaruratItem,
});
export default kontakDarurat;

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch"; import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
@@ -10,12 +11,24 @@ const templateForm = z.object({
judul: z.string().min(3, "Judul minimal 3 karakter"), judul: z.string().min(3, "Judul minimal 3 karakter"),
lokasi: z.string().min(3, "Lokasi minimal 3 karakter"), lokasi: z.string().min(3, "Lokasi minimal 3 karakter"),
tanggalWaktu: z.string().min(3, "Tanggal Waktu minimal 3 karakter"), tanggalWaktu: z.string().min(3, "Tanggal Waktu minimal 3 karakter"),
status: z.enum(["Selesai", "Proses", "Gagal"]),
penanganan: z.string(),
kronologi: z.string().optional(), kronologi: z.string().optional(),
}); });
interface FormData { interface FormData {
judul: string;
lokasi: string;
tanggalWaktu: string;
kronologi: string;
}
const defaultForm: FormData = {
judul: "",
lokasi: "",
tanggalWaktu: new Date().toISOString(),
kronologi: "",
};
interface FormEditData {
judul: string; judul: string;
lokasi: string; lokasi: string;
tanggalWaktu: string; tanggalWaktu: string;
@@ -24,15 +37,16 @@ interface FormData {
kronologi: string; kronologi: string;
} }
const defaultForm: FormData = { const editForm: FormEditData = {
judul: "", judul: "",
lokasi: "", lokasi: "",
tanggalWaktu: new Date().toISOString(), tanggalWaktu: new Date().toISOString(),
kronologi: "",
status: "Proses", status: "Proses",
penanganan: "", penanganan: "",
kronologi: "",
}; };
const laporanPublikState = proxy({ const laporanPublikState = proxy({
create: { create: {
form: { ...defaultForm }, form: { ...defaultForm },
@@ -97,13 +111,37 @@ const laporanPublikState = proxy({
include: { penanganan: true }; include: { penanganan: true };
}>[] }>[]
| null, | null,
async load() { page: 1,
const res = await ApiFetch.api.keamanan.laporanpublik["find-many"].get(); totalPages: 1,
if (res.status === 200) { loading: false,
laporanPublikState.findMany.data = res.data?.data ?? []; search: "",
} load: async (page = 1, limit = 10, search = "") => {
laporanPublikState.findMany.loading = true; // ✅ Akses langsung via nama path
laporanPublikState.findMany.page = page;
laporanPublikState.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.keamanan.laporanpublik["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
laporanPublikState.findMany.data = res.data.data ?? [];
laporanPublikState.findMany.totalPages = res.data.totalPages ?? 1;
} else {
laporanPublikState.findMany.data = [];
laporanPublikState.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch laporan publik paginated:", err);
laporanPublikState.findMany.data = [];
laporanPublikState.findMany.totalPages = 1;
} finally {
laporanPublikState.findMany.loading = false;
}
},
}, },
},
findUnique: { findUnique: {
data: null as Prisma.LaporanPublikGetPayload<{ data: null as Prisma.LaporanPublikGetPayload<{
include: { penanganan: true }; include: { penanganan: true };
@@ -160,7 +198,7 @@ const laporanPublikState = proxy({
}, },
edit: { edit: {
id: "", id: "",
form: { ...defaultForm }, form: { ...editForm },
loading: false, loading: false,
async load(id: string) { async load(id: string) {
if (!id) { if (!id) {
@@ -266,7 +304,7 @@ const laporanPublikState = proxy({
}, },
reset() { reset() {
laporanPublikState.edit.id = ""; laporanPublikState.edit.id = "";
laporanPublikState.edit.form = { ...defaultForm }; laporanPublikState.edit.form = { ...editForm };
}, },
} }
}); });

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch"; import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
@@ -5,45 +6,17 @@ import { proxy } from "valtio";
import { z } from "zod"; import { z } from "zod";
const templateForm = z.object({ const templateForm = z.object({
pencegahanKriminalitas: z.object({ judul: z.string().min(1, "Judul minimal 1 karakter"),
programKeamanan: z.object({ deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
nama: z.string().min(1, "Nama minimal 1 karakter"), deskripsiSingkat: z.string().min(1, "Deskripsi singkat minimal 1 karakter"),
deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"), linkVideo: z.string().min(1, "Link video minimal 1 karakter"),
slug: z.string().min(1, "Slug minimal 1 karakter"),
}),
tipsKeamanan: z.object({
judul: z.string().min(1, "Judul minimal 1 karakter"),
konten: z.string().min(1, "Konten minimal 1 karakter"),
slug: z.string().min(1, "Slug minimal 1 karakter"),
}),
videoKeamanan: z.object({
judul: z.string().min(1, "Judul minimal 1 karakter"),
deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
videoUrl: z.string().min(1, "Video URL minimal 1 karakter"),
slug: z.string().min(1, "Slug minimal 1 karakter"),
}),
}),
}); });
const defaultForm = { const defaultForm = {
pencegahanKriminalitas: { judul: "",
programKeamanan: { deskripsi: "",
nama: "", deskripsiSingkat: "",
deskripsi: "", linkVideo: "",
slug: "",
},
tipsKeamanan: {
judul: "",
konten: "",
slug: "",
},
videoKeamanan: {
judul: "",
deskripsi: "",
videoUrl: "",
slug: "",
},
},
}; };
const pencegahanKriminalitasState = proxy({ const pencegahanKriminalitasState = proxy({
@@ -64,7 +37,7 @@ const pencegahanKriminalitasState = proxy({
pencegahanKriminalitasState.create.loading = true; pencegahanKriminalitasState.create.loading = true;
const res = await ApiFetch.api.keamanan.pencegahankriminalitas[ const res = await ApiFetch.api.keamanan.pencegahankriminalitas[
"create" "create"
].post(pencegahanKriminalitasState.create.form.pencegahanKriminalitas); ].post(pencegahanKriminalitasState.create.form);
if (res.status === 200) { if (res.status === 200) {
pencegahanKriminalitasState.findMany.load(); pencegahanKriminalitasState.findMany.load();
return toast.success("success create"); return toast.success("success create");
@@ -81,29 +54,46 @@ const pencegahanKriminalitasState = proxy({
findMany: { findMany: {
data: null as data: null as
| Prisma.PencegahanKriminalitasGetPayload<{ | Prisma.PencegahanKriminalitasGetPayload<{
include: { omit: { isActive: true };
programKeamanan: true;
tipsKeamanan: true;
videoKeamanan: true;
};
}>[] }>[]
| null, | null,
async load() { page: 1,
const res = await ApiFetch.api.keamanan.pencegahankriminalitas[ totalPages: 1,
"find-many" loading: false,
].get(); search: "",
if (res.status === 200) { load: async (page = 1, limit = 10, search = "") => {
pencegahanKriminalitasState.findMany.data = res.data?.data ?? []; pencegahanKriminalitasState.findMany.loading = true; // ✅ Akses langsung via nama path
pencegahanKriminalitasState.findMany.page = page;
pencegahanKriminalitasState.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.keamanan.pencegahankriminalitas[
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
pencegahanKriminalitasState.findMany.data = res.data.data ?? [];
pencegahanKriminalitasState.findMany.totalPages =
res.data.totalPages ?? 1;
} else {
pencegahanKriminalitasState.findMany.data = [];
pencegahanKriminalitasState.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch pencegahan kriminalitas paginated:", err);
pencegahanKriminalitasState.findMany.data = [];
pencegahanKriminalitasState.findMany.totalPages = 1;
} finally {
pencegahanKriminalitasState.findMany.loading = false;
} }
}, },
}, },
findUnique: { findUnique: {
data: null as Prisma.PencegahanKriminalitasGetPayload<{ data: null as Prisma.PencegahanKriminalitasGetPayload<{
include: { omit: { isActive: true };
programKeamanan: true;
tipsKeamanan: true;
videoKeamanan: true;
};
}> | null, }> | null,
loading: false, loading: false,
async load(id: string) { async load(id: string) {
@@ -122,6 +112,30 @@ const pencegahanKriminalitasState = proxy({
} }
}, },
}, },
findFirst: {
data: null as Prisma.PencegahanKriminalitasGetPayload<{
omit: { isActive: true };
}> | null,
loading: false,
// findFirst.load()
async load() {
this.loading = true;
try {
const res = await ApiFetch.api.keamanan.pencegahankriminalitas["find-first"].get();
if (res.status === 200 && res.data?.success) {
this.data = res.data.data || null;
} else {
this.data = null;
}
} catch (err) {
console.error("Gagal fetch pencegahan kriminalitas terbaru:", err);
this.data = null;
} finally {
this.loading = false;
}
},
},
delete: { delete: {
loading: false, loading: false,
async byId(id: string) { async byId(id: string) {
@@ -187,24 +201,10 @@ const pencegahanKriminalitasState = proxy({
const data = result.data; const data = result.data;
pencegahanKriminalitasState.update.id = data.id; pencegahanKriminalitasState.update.id = data.id;
pencegahanKriminalitasState.update.form = { pencegahanKriminalitasState.update.form = {
pencegahanKriminalitas: { judul: data.judul,
programKeamanan: { deskripsi: data.deskripsi,
nama: data.programKeamanan.nama, deskripsiSingkat: data.deskripsiSingkat,
deskripsi: data.programKeamanan.deskripsi, linkVideo: data.linkVideo,
slug: data.programKeamanan.slug,
},
tipsKeamanan: {
judul: data.tipsKeamanan.judul,
konten: data.tipsKeamanan.konten,
slug: data.tipsKeamanan.slug,
},
videoKeamanan: {
judul: data.videoKeamanan.judul,
deskripsi: data.videoKeamanan.deskripsi,
videoUrl: data.videoKeamanan.videoUrl,
slug: data.videoKeamanan.slug,
},
},
}; };
return data; return data;
} else { } else {
@@ -240,40 +240,11 @@ const pencegahanKriminalitasState = proxy({
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify({ body: JSON.stringify({
pencegahanKriminalitas: { judul: pencegahanKriminalitasState.update.form.judul,
programKeamanan: { deskripsi: pencegahanKriminalitasState.update.form.deskripsi,
nama: pencegahanKriminalitasState.update.form deskripsiSingkat:
.pencegahanKriminalitas.programKeamanan.nama, pencegahanKriminalitasState.update.form.deskripsiSingkat,
deskripsi: linkVideo: pencegahanKriminalitasState.update.form.linkVideo,
pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.programKeamanan.deskripsi,
slug: pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.programKeamanan.slug,
},
tipsKeamanan: {
judul:
pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.tipsKeamanan.judul,
konten:
pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.tipsKeamanan.konten,
slug: pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.tipsKeamanan.slug,
},
videoKeamanan: {
judul:
pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.videoKeamanan.judul,
deskripsi:
pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.videoKeamanan.deskripsi,
videoUrl:
pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.videoKeamanan.videoUrl,
slug: pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.videoKeamanan.slug,
},
},
}), }),
} }
); );
@@ -311,4 +282,4 @@ const pencegahanKriminalitasState = proxy({
}, },
}, },
}); });
export default pencegahanKriminalitasState; export default pencegahanKriminalitasState;

View File

@@ -31,11 +31,13 @@ const templateForm = z.object({
doctorSign: z.object({ doctorSign: z.object({
content: z.string().min(1, "Content harus diisi"), content: z.string().min(1, "Content harus diisi"),
}), }),
imageId: z.string().min(1, "Image ID harus diisi"),
}); });
const defaultForm = { const defaultForm = {
title: "", title: "",
content: "", content: "",
imageId: "",
introduction: { introduction: {
content: "", content: "",
}, },
@@ -59,6 +61,7 @@ const defaultForm = {
doctorSign: { doctorSign: {
content: "", content: "",
}, },
}; };
const artikelKesehatanState = proxy({ const artikelKesehatanState = proxy({
@@ -112,30 +115,42 @@ const artikelKesehatanState = proxy({
firstaid: true; firstaid: true;
mythvsfact: true; mythvsfact: true;
doctorsign: true; doctorsign: true;
image: true;
}; };
}>[] }>[]
| null, | null,
page: 1,
totalPages: 1,
loading: false, loading: false,
async load() { search: "",
load: async (page = 1, limit = 10, search = "") => {
artikelKesehatanState.findMany.loading = true; // ✅ Akses langsung via nama path
artikelKesehatanState.findMany.page = page;
artikelKesehatanState.findMany.search = search;
try { try {
this.loading = true; const query: any = { page, limit };
const res = await (ApiFetch.api.kesehatan as any)["artikel-kesehatan"][ if (search) query.search = search;
const res = await ApiFetch.api.kesehatan["artikel-kesehatan"][
"find-many" "find-many"
].get(); ].get({ query });
if (res.status === 200) { if (res.status === 200 && res.data?.success) {
this.data = res.data?.data ?? []; artikelKesehatanState.findMany.data =
res.data.data ?? [];
artikelKesehatanState.findMany.totalPages =
res.data.totalPages ?? 1;
} else { } else {
toast.error("Gagal memuat data artikel kesehatan"); artikelKesehatanState.findMany.data = [];
artikelKesehatanState.findMany.totalPages = 1;
} }
return res;
} catch (err) { } catch (err) {
toast.error("Terjadi error saat load data"); console.error("Gagal fetch artikel kesehatan paginated:", err);
console.error("LOAD ERROR:", err); artikelKesehatanState.findMany.data = [];
throw err; artikelKesehatanState.findMany.totalPages = 1;
} finally { } finally {
this.loading = false; artikelKesehatanState.findMany.loading = false;
} }
}, },
}, },
@@ -148,6 +163,7 @@ const artikelKesehatanState = proxy({
firstaid: true; firstaid: true;
mythvsfact: true; mythvsfact: true;
doctorsign: true; doctorsign: true;
image: true;
}; };
}> | null, }> | null,
loading: false, loading: false,
@@ -202,6 +218,7 @@ const artikelKesehatanState = proxy({
doctorSign: { doctorSign: {
content: data.doctorsign.content, content: data.doctorsign.content,
}, },
imageId: data.imageId,
}; };
}, },
async submit() { async submit() {
@@ -242,6 +259,7 @@ const artikelKesehatanState = proxy({
doctorSign: { doctorSign: {
content: artikelKesehatanState.edit.form.doctorSign.content, content: artikelKesehatanState.edit.form.doctorSign.content,
}, },
imageId: artikelKesehatanState.edit.form.imageId,
}; };
const res = await fetch( const res = await fetch(
@@ -280,12 +298,9 @@ const artikelKesehatanState = proxy({
async byId(id: string) { async byId(id: string) {
try { try {
artikelKesehatanState.delete.loading = true; artikelKesehatanState.delete.loading = true;
const res = await fetch( const res = await fetch(`/api/kesehatan/artikel-kesehatan/del/${id}`, {
`/api/kesehatan/artikel-kesehatan/del/${id}`, method: "DELETE",
{ });
method: "DELETE",
}
);
const result = await res.json(); const result = await res.json();
if (res.ok && result.success) { if (res.ok && result.success) {

View File

@@ -116,27 +116,38 @@ const fasilitasKesehatan = proxy({
}; };
}>[] }>[]
| null, | null,
page: 1,
totalPages: 1,
loading: false, loading: false,
async load() { search: "",
load: async (page = 1, limit = 10, search = "") => {
fasilitasKesehatanState.fasilitasKesehatan.findMany.loading = true; // ✅ Akses langsung via nama path
fasilitasKesehatanState.fasilitasKesehatan.findMany.page = page;
fasilitasKesehatanState.fasilitasKesehatan.findMany.search = search;
try { try {
this.loading = true; const query: any = { page, limit };
const res = await (ApiFetch.api.kesehatan as any)[ if (search) query.search = search;
"fasilitas-kesehatan"
]["find-many"].get();
if (res.status === 200) { const res = await ApiFetch.api.kesehatan["fasilitas-kesehatan"][
this.data = res.data?.data ?? []; "find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
fasilitasKesehatanState.fasilitasKesehatan.findMany.data =
res.data.data ?? [];
fasilitasKesehatanState.fasilitasKesehatan.findMany.totalPages =
res.data.totalPages ?? 1;
} else { } else {
toast.error("Gagal memuat data fasilitas kesehatan"); fasilitasKesehatanState.fasilitasKesehatan.findMany.data = [];
fasilitasKesehatanState.fasilitasKesehatan.findMany.totalPages = 1;
} }
return res;
} catch (err) { } catch (err) {
toast.error("Terjadi error saat load data"); console.error("Gagal fetch fasilitas kesehatan paginated:", err);
console.error("LOAD ERROR:", err); fasilitasKesehatanState.fasilitasKesehatan.findMany.data = [];
throw err; fasilitasKesehatanState.fasilitasKesehatan.findMany.totalPages = 1;
} finally { } finally {
this.loading = false; fasilitasKesehatanState.fasilitasKesehatan.findMany.loading = false;
} }
}, },
}, },
@@ -558,7 +569,7 @@ const dokter = proxy({
const fasilitasKesehatanState = proxy({ const fasilitasKesehatanState = proxy({
fasilitasKesehatan, fasilitasKesehatan,
dokter dokter,
}); });
export default fasilitasKesehatanState; export default fasilitasKesehatanState;

View File

@@ -26,14 +26,6 @@ const templateForm = z.object({
dokumenJadwalKegiatan: z.object({ dokumenJadwalKegiatan: z.object({
content: z.string().min(1, "Content minimal 1 karakter"), content: z.string().min(1, "Content minimal 1 karakter"),
}), }),
pendaftaranJadwalKegiatan: z.object({
name: z.string().min(1, "Name minimal 1 karakter"),
tanggal: z.string().min(1, "Tanggal minimal 1 karakter"),
namaOrangtua: z.string().min(1, "Nama Orangtua minimal 1 karakter"),
nomor: z.string().min(1, "Nomor minimal 1 karakter"),
alamat: z.string().min(1, "Alamat minimal 1 karakter"),
catatan: z.string().min(1, "Catatan minimal 1 karakter"),
}),
}); });
const defaultForm = { const defaultForm = {
@@ -55,15 +47,7 @@ const defaultForm = {
}, },
dokumenJadwalKegiatan: { dokumenJadwalKegiatan: {
content: "", content: "",
}, }
pendaftaranJadwalKegiatan: {
name: "",
tanggal: "",
namaOrangtua: "",
nomor: "",
alamat: "",
catatan: "",
},
}; };
const jadwalkegiatanState = proxy({ const jadwalkegiatanState = proxy({
@@ -116,31 +100,39 @@ const jadwalkegiatanState = proxy({
deskripsijadwalkegiatan: true; deskripsijadwalkegiatan: true;
layananjadwalkegiatan: true; layananjadwalkegiatan: true;
dokumenjadwalkegiatan: true; dokumenjadwalkegiatan: true;
pendaftaranjadwalkegiatan: true;
}; };
}>[] }>[]
| null, | null,
page: 1,
totalPages: 1,
loading: false, loading: false,
async load() { search: "",
load: async (page = 1, limit = 10, search = "") => {
jadwalkegiatanState.findMany.loading = true; // ✅ Akses langsung via nama path
jadwalkegiatanState.findMany.page = page;
jadwalkegiatanState.findMany.search = search;
try { try {
this.loading = true; const query: any = { page, limit };
const res = await (ApiFetch.api.kesehatan as any)[ if (search) query.search = search;
"jadwal-kegiatan"
]["find-many"].get();
if (res.status === 200) { const res = await ApiFetch.api.kesehatan["jadwal-kegiatan"][
this.data = res.data?.data ?? []; "find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
jadwalkegiatanState.findMany.data = res.data.data ?? [];
jadwalkegiatanState.findMany.totalPages = res.data.totalPages ?? 1;
} else { } else {
toast.error("Gagal memuat data jadwal kegiatan"); jadwalkegiatanState.findMany.data = [];
jadwalkegiatanState.findMany.totalPages = 1;
} }
return res;
} catch (err) { } catch (err) {
toast.error("Terjadi error saat load data"); console.error("Gagal fetch jadwal kegiatan paginated:", err);
console.error("LOAD ERROR:", err); jadwalkegiatanState.findMany.data = [];
throw err; jadwalkegiatanState.findMany.totalPages = 1;
} finally { } finally {
this.loading = false; jadwalkegiatanState.findMany.loading = false;
} }
}, },
}, },
@@ -152,7 +144,6 @@ const jadwalkegiatanState = proxy({
layananjadwalkegiatan: true; layananjadwalkegiatan: true;
syaratketentuanjadwalkegiatan: true; syaratketentuanjadwalkegiatan: true;
dokumenjadwalkegiatan: true; dokumenjadwalkegiatan: true;
pendaftaranjadwalkegiatan: true;
}; };
}> | null, }> | null,
loading: false, loading: false,
@@ -200,15 +191,7 @@ const jadwalkegiatanState = proxy({
}, },
dokumenJadwalKegiatan: { dokumenJadwalKegiatan: {
content: data.dokumenjadwalkegiatan.content, content: data.dokumenjadwalkegiatan.content,
}, }
pendaftaranJadwalKegiatan: {
name: data.pendaftaranjadwalkegiatan.name,
tanggal: data.pendaftaranjadwalkegiatan.tanggal,
namaOrangtua: data.pendaftaranjadwalkegiatan.namaOrangtua,
nomor: data.pendaftaranjadwalkegiatan.nomor,
alamat: data.pendaftaranjadwalkegiatan.alamat,
catatan: data.pendaftaranjadwalkegiatan.catatan,
},
}; };
}, },
async submit() { async submit() {
@@ -227,29 +210,28 @@ const jadwalkegiatanState = proxy({
content: jadwalkegiatanState.edit.form.content, content: jadwalkegiatanState.edit.form.content,
informasiJadwalKegiatan: { informasiJadwalKegiatan: {
name: jadwalkegiatanState.edit.form.informasiJadwalKegiatan.name, name: jadwalkegiatanState.edit.form.informasiJadwalKegiatan.name,
tanggal: jadwalkegiatanState.edit.form.informasiJadwalKegiatan.tanggal, tanggal:
jadwalkegiatanState.edit.form.informasiJadwalKegiatan.tanggal,
waktu: jadwalkegiatanState.edit.form.informasiJadwalKegiatan.waktu, waktu: jadwalkegiatanState.edit.form.informasiJadwalKegiatan.waktu,
lokasi: jadwalkegiatanState.edit.form.informasiJadwalKegiatan.lokasi, lokasi:
jadwalkegiatanState.edit.form.informasiJadwalKegiatan.lokasi,
}, },
layananJadwalKegiatan: { layananJadwalKegiatan: {
content: jadwalkegiatanState.edit.form.layananJadwalKegiatan.content, content:
jadwalkegiatanState.edit.form.layananJadwalKegiatan.content,
}, },
deskripsiJadwalKegiatan: { deskripsiJadwalKegiatan: {
deskripsi: jadwalkegiatanState.edit.form.deskripsiJadwalKegiatan.deskripsi, deskripsi:
jadwalkegiatanState.edit.form.deskripsiJadwalKegiatan.deskripsi,
}, },
syaratKetentuanJadwalKegiatan: { syaratKetentuanJadwalKegiatan: {
content: jadwalkegiatanState.edit.form.syaratKetentuanJadwalKegiatan.content, content:
jadwalkegiatanState.edit.form.syaratKetentuanJadwalKegiatan
.content,
}, },
dokumenJadwalKegiatan: { dokumenJadwalKegiatan: {
content: jadwalkegiatanState.edit.form.dokumenJadwalKegiatan.content, content:
}, jadwalkegiatanState.edit.form.dokumenJadwalKegiatan.content,
pendaftaranJadwalKegiatan: {
name: jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.name,
tanggal: jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.tanggal,
namaOrangtua: jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.namaOrangtua,
nomor: jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.nomor,
alamat: jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.alamat,
catatan: jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.catatan,
}, },
}; };
@@ -286,7 +268,7 @@ const jadwalkegiatanState = proxy({
}, },
delete: { delete: {
loading: false, loading: false,
async byId(id: string){ async byId(id: string) {
try { try {
jadwalkegiatanState.delete.loading = true; jadwalkegiatanState.delete.loading = true;
const res = await fetch(`/api/kesehatan/jadwal-kegiatan/del/${id}`, { const res = await fetch(`/api/kesehatan/jadwal-kegiatan/del/${id}`, {
@@ -305,7 +287,7 @@ const jadwalkegiatanState = proxy({
} finally { } finally {
jadwalkegiatanState.delete.loading = false; jadwalkegiatanState.delete.loading = false;
} }
} },
}, },
}); });

View File

@@ -0,0 +1,290 @@
/* 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, "Name minimal 1 karakter"),
tanggal: z.string().min(1, "Tanggal minimal 1 karakter"),
namaOrangtua: z.string().min(1, "Nama Orangtua minimal 1 karakter"),
nomor: z.string().min(1, "Nomor minimal 1 karakter"),
alamat: z.string().min(1, "Alamat minimal 1 karakter"),
catatan: z.string().min(1, "Catatan minimal 1 karakter"),
});
const defaultForm = {
name: "",
tanggal: "",
namaOrangtua: "",
nomor: "",
alamat: "",
catatan: "",
};
const pendaftaranJadwalKegiatanState = proxy({
create: {
form: { ...defaultForm },
loading: false,
async submit() {
const cek = templateForm.safeParse(this.form);
if (!cek.success) {
const errMsg = cek.error.issues
.map((v) => `${v.path.join(".")}: ${v.message}`)
.join("\n");
toast.error(errMsg);
return null;
}
try {
this.loading = true;
const payload = { ...this.form };
const res = await (ApiFetch.api.kesehatan as any)[
"pendaftaran-jadwal-kegiatan"
].create.post(payload);
if (res.status === 200) {
toast.success("Berhasil menambahkan jadwal kegiatan");
this.resetForm();
await pendaftaranJadwalKegiatanState.findMany.load();
return res.data;
}
} catch (err: any) {
const msg = err?.message || "Terjadi kesalahan saat mengirim data";
toast.error(msg);
console.error("SUBMIT ERROR:", err);
return null;
} finally {
this.loading = false;
}
},
resetForm() {
this.form = { ...defaultForm };
},
},
findMany: {
data: null as
| Prisma.PendaftaranJadwalKegiatanGetPayload<{
omit: {
isActive: true;
};
}>[]
| null,
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
pendaftaranJadwalKegiatanState.findMany.loading = true; // ✅ Akses langsung via nama path
pendaftaranJadwalKegiatanState.findMany.page = page;
pendaftaranJadwalKegiatanState.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.kesehatan["pendaftaran-jadwal-kegiatan"][
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
pendaftaranJadwalKegiatanState.findMany.data = res.data.data ?? [];
pendaftaranJadwalKegiatanState.findMany.totalPages =
res.data.totalPages ?? 1;
} else {
pendaftaranJadwalKegiatanState.findMany.data = [];
pendaftaranJadwalKegiatanState.findMany.totalPages = 1;
}
} catch (err) {
console.error(
"Gagal fetch pendaftaran jadwal kegiatan paginated:",
err
);
pendaftaranJadwalKegiatanState.findMany.data = [];
pendaftaranJadwalKegiatanState.findMany.totalPages = 1;
} finally {
pendaftaranJadwalKegiatanState.findMany.loading = false;
}
},
},
findUnique: {
data: null as Prisma.PendaftaranJadwalKegiatanGetPayload<{
omit: {
isActive: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(
`/api/kesehatan/pendaftaran-jadwal-kegiatan/${id}`
);
if (res.ok) {
const data = await res.json();
pendaftaranJadwalKegiatanState.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
pendaftaranJadwalKegiatanState.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
pendaftaranJadwalKegiatanState.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
pendaftaranJadwalKegiatanState.delete.loading = true;
const response = await fetch(
`/api/kesehatan/pendaftaran-jadwal-kegiatan/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(
result.message || "Pendaftaran jadwal kegiatan berhasil dihapus"
);
await pendaftaranJadwalKegiatanState.findMany.load(); // refresh list
} else {
toast.error(
result?.message || "Gagal menghapus pendaftaran jadwal kegiatan"
);
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error(
"Terjadi kesalahan saat menghapus pendaftaran jadwal kegiatan"
);
} finally {
pendaftaranJadwalKegiatanState.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/kesehatan/pendaftaran-jadwal-kegiatan/${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,
tanggal: data.tanggal,
namaOrangtua: data.namaOrangtua,
nomor: data.nomor,
alamat: data.alamat,
catatan: data.catatan,
};
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading pendaftaran jadwal kegiatan:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateForm.safeParse(pendaftaranJadwalKegiatanState.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
pendaftaranJadwalKegiatanState.edit.loading = true;
const response = await fetch(
`/api/kesehatan/pendaftaran-jadwal-kegiatan/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
tanggal: this.form.tanggal,
namaOrangtua: this.form.namaOrangtua,
nomor: this.form.nomor,
alamat: this.form.alamat,
catatan: this.form.catatan,
}),
}
);
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 pendaftaran jadwal kegiatan");
await pendaftaranJadwalKegiatanState.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal update pendaftaran jadwal kegiatan");
}
} catch (error) {
console.error("Error updating pendaftaran jadwal kegiatan:", error);
toast.error(
error instanceof Error
? error.message
: "Terjadi kesalahan saat update pendaftaran jadwal kegiatan"
);
return false;
} finally {
pendaftaranJadwalKegiatanState.edit.loading = false;
}
},
reset() {
pendaftaranJadwalKegiatanState.edit.id = "";
pendaftaranJadwalKegiatanState.edit.form = { ...defaultForm };
},
},
});
export default pendaftaranJadwalKegiatanState;

View File

@@ -9,12 +9,14 @@ const templateForm = z.object({
name: z.string().min(3, "Judul minimal 3 karakter"), name: z.string().min(3, "Judul minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"), deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
imageId: z.string().nonempty(), imageId: z.string().nonempty(),
whatsapp: z.string().min(10, "Whatsapp minimal 10 karakter"),
}); });
const defaultForm = { const defaultForm = {
name: "", name: "",
deskripsi: "", deskripsi: "",
imageId: "", imageId: "",
whatsapp: "",
}; };
const kontakDarurat = proxy({ const kontakDarurat = proxy({
@@ -171,6 +173,7 @@ const kontakDarurat = proxy({
name: data.name, name: data.name,
deskripsi: data.deskripsi, deskripsi: data.deskripsi,
imageId: data.imageId, imageId: data.imageId,
whatsapp: data.whatsapp,
}; };
return data; // Return the loaded data return data; // Return the loaded data
} else { } else {
@@ -207,6 +210,7 @@ const kontakDarurat = proxy({
name: this.form.name, name: this.form.name,
deskripsi: this.form.deskripsi, deskripsi: this.form.deskripsi,
imageId: this.form.imageId, imageId: this.form.imageId,
whatsapp: this.form.whatsapp,
}), }),
} }
); );

View File

@@ -23,7 +23,12 @@ type ProgramInovasiForm = Prisma.ProgramInovasiGetPayload<{
const programInovasi = proxy({ const programInovasi = proxy({
create: { create: {
form: {} as ProgramInovasiForm, form: {
name: "",
description: "",
imageId: "",
link: ""
} as ProgramInovasiForm,
loading: false, loading: false,
async create() { async create() {
// Ensure all required fields are non-null // Ensure all required fields are non-null
@@ -383,16 +388,15 @@ const pejabatDesa = proxy({
this.error = null; this.error = null;
try { try {
const response = await fetch( // Ensure ID is properly encoded in the URL
`/api/landingpage/pejabatdesa/${this.id}`, const url = new URL(`/api/landingpage/pejabatdesa/${encodeURIComponent(this.id)}`, window.location.origin);
{ const response = await fetch(url.toString(), {
method: "PUT", method: "PUT",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify(this.form), body: JSON.stringify(this.form),
} });
);
if (!response.ok) { if (!response.ok) {
const errorData = await response.json().catch(() => ({})); const errorData = await response.json().catch(() => ({}));

View File

@@ -72,7 +72,7 @@ const sdgsDesa = proxy({
].get({ ].get({
query, query,
}); });
if (res.status === 200 && res.data?.success) { if (res.status === 200 && res.data?.success) {
sdgsDesa.findMany.data = res.data.data || []; sdgsDesa.findMany.data = res.data.data || [];
sdgsDesa.findMany.total = res.data.total || 0; sdgsDesa.findMany.total = res.data.total || 0;
@@ -94,7 +94,7 @@ const sdgsDesa = proxy({
}, },
}, },
findUnique: { findUnique: {
data: null as Prisma.SDGSDesaGetPayload<{ data: null as Prisma.SdgsDesaGetPayload<{
include: { include: {
image: true; image: true;
}; };

View File

@@ -354,14 +354,39 @@ const kategoriKegiatan = proxy({
id: string; id: string;
nama: string; nama: string;
}> | null, }> | null,
async load() { page: 1,
const res = await ApiFetch.api.lingkungan.kategorikegiatan[ totalPages: 1,
"find-many" loading: false,
].get(); search: "",
if (res.status === 200) { load: async (page = 1, limit = 10, search = "") => {
kategoriKegiatan.findMany.data = res.data?.data ?? []; kategoriKegiatan.findMany.loading = true; // ✅ Akses langsung via nama path
} kategoriKegiatan.findMany.page = page;
}, kategoriKegiatan.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res =
await ApiFetch.api.lingkungan.kategorikegiatan[
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
kategoriKegiatan.findMany.data = res.data.data ?? [];
kategoriKegiatan.findMany.totalPages = res.data.totalPages ?? 1;
} else {
kategoriKegiatan.findMany.data = [];
kategoriKegiatan.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch kategori kegiatan paginated:", err);
kategoriKegiatan.findMany.data = [];
kategoriKegiatan.findMany.totalPages = 1;
} finally {
kategoriKegiatan.findMany.loading = false;
}
},
}, },
findUnique: { findUnique: {
data: null as Prisma.KategoriKegiatanGetPayload<{ data: null as Prisma.KategoriKegiatanGetPayload<{

View File

@@ -112,7 +112,32 @@ const statepermohonanInformasiPublik = proxy({
statepermohonanInformasiPublik.findMany.data = res.data?.data ?? []; statepermohonanInformasiPublik.findMany.data = res.data?.data ?? [];
} }
} }
} },
findUnique: {
data: null as Prisma.PermohonanInformasiPublikGetPayload<{
include: {
jenisInformasiDiminta: true,
caraMemperolehInformasi: true,
caraMemperolehSalinanInformasi: true,
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/ppid/permohonaninformasipublik/${id}`);
if (res.ok) {
const data = await res.json();
statepermohonanInformasiPublik.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch program inovasi:", res.statusText);
statepermohonanInformasiPublik.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching program inovasi:", error);
statepermohonanInformasiPublik.findUnique.data = null;
}
},
},
}) })
const statepermohonanInformasiPublikForm = proxy({ const statepermohonanInformasiPublikForm = proxy({

View File

@@ -57,7 +57,29 @@ const permohonanKeberatanInformasi = proxy({
permohonanKeberatanInformasi.findMany.data = res.data?.data ?? []; permohonanKeberatanInformasi.findMany.data = res.data?.data ?? [];
} }
} }
} },
findUnique: {
data: null as Prisma.FormulirPermohonanKeberatanGetPayload<{
omit: {
isActive: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/ppid/permohonankeberataninformasipublik/${id}`);
if (res.ok) {
const data = await res.json();
permohonanKeberatanInformasi.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch permohonan keberatan informasi:", res.statusText);
permohonanKeberatanInformasi.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching permohonan keberatan informasi:", error);
permohonanKeberatanInformasi.findUnique.data = null;
}
},
}
}); });
export default permohonanKeberatanInformasi; export default permohonanKeberatanInformasi;

View File

@@ -352,17 +352,19 @@ const posisiOrganisasi = proxy({
totalPages: 1, totalPages: 1,
loading: false, loading: false,
search: "", search: "",
load: async (page = 1, limit = 10, search = "") => { load: async (page = 1, limit?: number, search = "") => {
posisiOrganisasi.findMany.loading = true; // ✅ Akses langsung via nama path const appliedLimit = limit ?? 10;
posisiOrganisasi.findMany.page = page; posisiOrganisasi.findMany.page = page;
posisiOrganisasi.findMany.search = search; posisiOrganisasi.findMany.search = search;
try { try {
const query: any = { page, limit }; const query: any = { page, limit: appliedLimit };
if (search) query.search = search; if (search) query.search = search;
const res = await ApiFetch.api.ppid.strukturppid.posisiorganisasi["find-many"].get({ query }); const res = await ApiFetch.api.ppid.strukturppid.posisiorganisasi[
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) { if (res.status === 200 && res.data?.success) {
posisiOrganisasi.findMany.data = res.data.data ?? []; posisiOrganisasi.findMany.data = res.data.data ?? [];
posisiOrganisasi.findMany.totalPages = res.data.totalPages ?? 1; posisiOrganisasi.findMany.totalPages = res.data.totalPages ?? 1;
@@ -379,7 +381,44 @@ const posisiOrganisasi = proxy({
} }
}, },
}, },
findManyAll: {
data: [] as Array<{
id: string;
nama: string;
deskripsi: string | null;
hierarki: number;
}>,
loading: false,
search: "",
load: async (search = "") => {
// Change to arrow function
posisiOrganisasi.findManyAll.loading = true; // Use the full path to access the property
posisiOrganisasi.findManyAll.search = search;
try {
const query: any = { search };
if (search) query.search = search;
const res = await ApiFetch.api.ppid.strukturppid.posisiorganisasi[
"find-many-all"
].get({
query,
});
if (res.status === 200 && res.data?.success) {
posisiOrganisasi.findManyAll.data = res.data.data || [];
} else {
console.error("Failed to load posisiOrganisasi:", res.data?.message);
posisiOrganisasi.findManyAll.data = [];
}
} catch (error) {
console.error("Error loading posisiOrganisasi:", error);
posisiOrganisasi.findManyAll.data = [];
} finally {
posisiOrganisasi.findManyAll.loading = false;
}
},
},
delete: { delete: {
loading: false, loading: false,
async byId(id: string) { async byId(id: string) {
@@ -569,6 +608,31 @@ const pegawai = proxy({
}, },
}, },
nonActive: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
pegawai.nonActive.loading = true;
const res = await fetch(`/api/ppid/strukturppid/pegawai/non-active/${id}`, {
method: "DELETE", // biasanya nonActive pakai PATCH
});
const json = await res.json();
if (res.ok) {
toast.success(json.message ?? "Pegawai berhasil dinonaktifkan");
await pegawai.findMany.load(); // refresh data
} else {
toast.error(json.message ?? "Gagal menonaktifkan pegawai");
}
} catch (error) {
console.error("Gagal nonActive:", error);
toast.error("Terjadi kesalahan saat menonaktifkan pegawai");
} finally {
pegawai.nonActive.loading = false;
}
},
},
edit: { edit: {
id: "", id: "",
form: { ...pegawaiDefaultForm }, form: { ...pegawaiDefaultForm },

View File

@@ -90,6 +90,32 @@ const userState = proxy({
} }
}, },
}, },
updateActive: {
loading: false,
async submit(id: string, isActive: boolean) {
this.loading = true;
try {
const res = await fetch(`/api/user/updt`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id, isActive }),
});
const data = await res.json();
if (res.status === 200 && data.success) {
toast.success(data.message);
userState.findMany.load(userState.findMany.page, 10, userState.findMany.search);
} else {
toast.error(data.message || "Gagal update status user");
}
} catch (e) {
console.error(e);
toast.error("Gagal update status user");
} finally {
this.loading = false;
}
},
},
}); });
const templateRole = z.object({ const templateRole = z.object({

View File

@@ -0,0 +1,111 @@
'use client'
import { apiFetchLogin } from '@/app/admin/auth/_lib/api_fetch_auth';
import colors from '@/con/colors';
import { Box, Button, Center, Flex, Image, Paper, Stack, Text, Title } from '@mantine/core';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { PhoneInput } from "react-international-phone";
import "react-international-phone/style.css";
import { toast } from 'react-toastify';
function Login() {
const router = useRouter()
const [phone, setPhone] = useState("")
const [isError, setError] = useState(false)
const [loading, setLoading] = useState(false)
async function onLogin() {
const nomor = phone.substring(1);
if (nomor.length <= 4) return setError(true)
try {
setLoading(true);
const response = await apiFetchLogin({ nomor: nomor })
if (response && response.success) {
localStorage.setItem("hipmi_auth_code_id", response.kodeId);
toast.success(response.message);
router.push("/validasi", { scroll: false });
} else {
setLoading(false);
toast.error(response?.message);
}
} catch (error) {
setLoading(false)
console.log("Error Login", error)
toast.error("Terjadi kesalahan saat login")
}
}
return (
<Stack pos={"relative"} bg={colors.Bg}>
<Box px={{ base: 'md', md: 100 }} pb={50}>
<Stack align='center' justify='center' h={"100vh"}>
<Paper p={'xl'} radius={'md'} bg={colors['white-trans-1']}>
<Stack align='center' gap={"lg"}>
<Box>
<Title ta={"center"} order={2} fw={'bold'} c={colors['blue-button']}>
Login
</Title>
<Center>
<Image loading="lazy" src={"/darmasaba-icon.png"} alt="" w={80} />
</Center>
</Box>
<Box>
{/* <Box mb={10}>
<Text c={colors['blue-button']} ta={"center"} fz={"sm"} fw={'bold'}>Masuk Untuk Akses Admin</Text>
<TextInput
label='Username'
placeholder='Username'
value={username}
onChange={(e) => setUsername(e.target.value)}
required
/>
</Box> */}
<PhoneInput
countrySelectorStyleProps={{
buttonStyle: {
backgroundColor: colors['blue-button'],
},
}}
inputStyle={{ width: "100%"}}
defaultCountry="id"
onChange={(val) => {
setPhone(val);
}}
/>
{isError ? (
toast.error("Masukan nomor telepon anda")
) : (
""
)}
<Box py={20} >
<Button
fullWidth
bg={colors['blue-button']}
radius={'xl'}
onClick={onLogin}
loading={loading ? true : false}
>Masuk
</Button>
</Box>
<Flex justify={'center'} align={'center'}>
<Text>Belum punya akun? </Text>
<Button variant='transparent' component={Link} href={'/registrasi'}>
<Text c={colors['blue-button']} fw={'bold'}>Registrasi</Text>
</Button>
</Flex>
</Box>
</Stack>
</Paper>
</Stack>
</Box>
</Stack>
);
}
export default Login;

View File

@@ -0,0 +1,121 @@
/* eslint-disable @typescript-eslint/no-unused-expressions */
'use client'
import { apiFetchRegister } from '@/app/admin/auth/_lib/api_fetch_auth';
import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto';
import colors from '@/con/colors';
import { Box, Button, Center, Checkbox, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { PhoneInput } from "react-international-phone";
import "react-international-phone/style.css";
import { toast } from 'react-toastify';
function Registrasi() {
const [phone, setPhone] = useState("")
const router = useRouter()
const [value, setValue] = useState("")
const [isValue, setIsValue] = useState(false);
const [loading, setLoading] = useState(false);
async function onRegistarsi() {
if (value.length < 5) {
toast.error("Username minimal 5 karakter!");
return;
}
if (value.includes(" ")) {
toast.error("Username tidak boleh ada spasi!");
return;
}
if (!phone) {
toast.error("Nomor telepon wajib diisi!");
return;
}
try {
setLoading(true);
const respone = await apiFetchRegister({ nomor: phone, username: value });
if (respone.success) {
router.push("/login", { scroll: false });
toast.success(respone.message);
} else {
setLoading(false);
toast.error(respone.message);
}
} catch (error) {
setLoading(false);
console.log("Error Registrasi", error);
}
}
return (
<Stack pos={"relative"} bg={colors.Bg} gap={"22"} py={"xl"} h={"100vh"}>
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box px={{ base: 'md', md: 100 }}>
<Stack justify='center' align='center' h={"80vh"}>
<Paper p={'xl'} radius={'md'} bg={colors['white-trans-1']}>
<Stack align='center'>
<Title order={2} fw={'bold'} c={colors['blue-button']}>
Registrasi
</Title>
<Center>
<Image loading="lazy" src={"/darmasaba-icon.png"} alt="" w={80} />
</Center>
<Box>
<TextInput placeholder='Username'
label='Username'
maxLength={50}
error={
value.length > 0 && value.length < 5
? "Minimal 5 karakter !"
: value.includes(" ")
? "Tidak boleh ada spasi"
: isValue
? "Masukan username anda"
: ""
}
onChange={(val) => {
val.currentTarget.value.length > 0 ? setIsValue(false) : "";
setValue(val.currentTarget.value);
}}
required
/>
<Box py={10}>
<Text fz={"sm"} >Nomor Telepon</Text>
<PhoneInput
countrySelectorStyleProps={{
buttonStyle: {
backgroundColor: colors['blue-button'],
},
}}
inputStyle={{ width: "100%" }}
defaultCountry="id"
onChange={(val) => {
setPhone(val);
}}
/>
</Box>
<Box pb={10}>
<Checkbox
label="Saya menyetujui syarat dan ketentuan yang berlaku"
/>
</Box>
<Box pb={20} >
<Button fullWidth bg={colors['blue-button']} radius={'xl'} onClick={onRegistarsi} loading={loading ? true : false}>Daftar</Button>
</Box>
</Box>
</Stack>
</Paper>
</Stack>
</Box>
</Stack>
);
}
export default Registrasi;

View File

@@ -0,0 +1,38 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, PinInput, Stack, Text, Title } from '@mantine/core';
import { useRouter } from 'next/navigation';
function Validasi() {
const router = useRouter()
return (
<Stack pos={"relative"} bg={colors.Bg}>
<Box px={{ base: 'md', md: 100 }} pb={50}>
<Stack align='center' justify='center' h={"100vh"}>
<Paper p={'xl'} radius={'md'} bg={colors['white-trans-1']}>
<Stack align='center' gap={"lg"}>
<Box>
<Title ta={"center"} order={2} fw={'bold'} c={colors['blue-button']}>
Kode Verifikasi
</Title>
</Box>
<Box>
<Box mb={10}>
<Text c={colors['blue-button']} ta={"center"} fz={"sm"} fw={'bold'}>Masukkan Kode Verifikasi</Text>
<PinInput type={/^[0-9]*$/} inputType="tel" inputMode="numeric" />
</Box>
<Box py={20} >
<Button onClick={() => router.push("/admin/landing-page/profile/program-inovasi")}>
Page
</Button>
</Box>
</Box>
</Stack>
</Paper>
</Stack>
</Box>
</Stack>
);
}
export default Validasi;

View File

@@ -1,10 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core'; import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { IconFileText, IconBuildingStore, IconSparkles, IconUsers } from '@tabler/icons-react'; import { IconFileText, IconBuildingStore, IconSparkles, IconUsers, IconUsersPlus } from '@tabler/icons-react';
function LayoutTabsLayanan({ children }: { children: React.ReactNode }) { function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
const router = useRouter() const router = useRouter()
@@ -37,6 +37,13 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
href: "/admin/desa/layanan/pelayanan_penduduk_non_permanent", href: "/admin/desa/layanan/pelayanan_penduduk_non_permanent",
icon: <IconUsers size={18} stroke={1.8} />, icon: <IconUsers size={18} stroke={1.8} />,
tooltip: "Pendataan penduduk non-permanent" tooltip: "Pendataan penduduk non-permanent"
},
{
label: "Ajukan Permohonan",
value: "ajukanpermohonan",
href: "/admin/desa/layanan/ajukan_permohonan",
icon: <IconUsersPlus size={18} stroke={1.8} />,
tooltip: "Ajukan permohonan"
} }
]; ];
@@ -62,43 +69,50 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
<Stack gap="lg"> <Stack gap="lg">
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Layanan</Title> <Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Layanan</Title>
<Tabs <Tabs
color={colors['blue-button']} color={colors["blue-button"]}
variant='pills' variant="pills"
value={activeTab} value={activeTab}
onChange={handleTabChange} onChange={handleTabChange}
radius="lg" radius="lg"
keepMounted={false} keepMounted={false}
> >
<TabsList {/* ✅ Scroll horizontal wrapper */}
p="sm" <ScrollArea type="auto" offsetScrollbars>
style={{ <TabsList
background: "linear-gradient(135deg, #e7ebf7, #f9faff)", p="sm"
borderRadius: "1rem", style={{
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)", background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
}} borderRadius: "1rem",
> boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
{tabs.map((tab, i) => ( display: "flex",
<Tooltip flexWrap: "nowrap",
key={i} gap: "0.5rem",
label={tab.tooltip} paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
position="bottom" }}
withArrow >
transitionProps={{ transition: 'pop', duration: 200 }} {tabs.map((tab, i) => (
> <Tooltip
<TabsTab key={i}
value={tab.value} label={tab.tooltip}
leftSection={tab.icon} position="bottom"
style={{ withArrow
fontWeight: 600, transitionProps={{ transition: 'pop', duration: 200 }}
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
> >
{tab.label} <TabsTab
</TabsTab> value={tab.value}
</Tooltip> leftSection={tab.icon}
))} style={{
</TabsList> fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
</ScrollArea>
{tabs.map((tab, i) => ( {tabs.map((tab, i) => (
<TabsPanel <TabsPanel

View File

@@ -1,7 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core'; import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { IconNews, IconCategory } from '@tabler/icons-react'; import { IconNews, IconCategory } from '@tabler/icons-react';
@@ -49,43 +49,50 @@ function LayoutTabsBerita({ children }: { children: React.ReactNode }) {
<Stack gap="lg"> <Stack gap="lg">
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Berita Desa</Title> <Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Berita Desa</Title>
<Tabs <Tabs
color={colors['blue-button']} color={colors["blue-button"]}
variant="pills" variant="pills"
value={activeTab} value={activeTab}
onChange={handleTabChange} onChange={handleTabChange}
radius="lg" radius="lg"
keepMounted={false} keepMounted={false}
> >
<TabsList {/* ✅ Scroll horizontal wrapper */}
p="sm" <ScrollArea type="auto" offsetScrollbars>
style={{ <TabsList
background: "linear-gradient(135deg, #e7ebf7, #f9faff)", p="sm"
borderRadius: "1rem", style={{
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)", background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
}} borderRadius: "1rem",
> boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
{tabs.map((tab, i) => ( display: "flex",
<Tooltip flexWrap: "nowrap",
key={i} gap: "0.5rem",
label={tab.tooltip} paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
position="bottom" }}
withArrow >
transitionProps={{ transition: 'pop', duration: 200 }} {tabs.map((tab, i) => (
> <Tooltip
<TabsTab key={i}
value={tab.value} label={tab.tooltip}
leftSection={tab.icon} position="bottom"
style={{ withArrow
fontWeight: 600, transitionProps={{ transition: 'pop', duration: 200 }}
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
> >
{tab.label} <TabsTab
</TabsTab> value={tab.value}
</Tooltip> leftSection={tab.icon}
))} style={{
</TabsList> fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
</ScrollArea>
{tabs.map((tab, i) => ( {tabs.map((tab, i) => (
<TabsPanel <TabsPanel

View File

@@ -1,5 +1,6 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita'; import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { import {
@@ -10,7 +11,7 @@ import {
Stack, Stack,
TextInput, TextInput,
Title, Title,
Tooltip Tooltip,
} from '@mantine/core'; } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
@@ -24,7 +25,7 @@ function EditKategoriBerita() {
const params = useParams(); const params = useParams();
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: editState.update.form.name || '', name: '',
}); });
useEffect(() => { useEffect(() => {
@@ -48,8 +49,16 @@ function EditKategoriBerita() {
loadKategori(); loadKategori();
}, [params?.id]); }, [params?.id]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData((prev) => ({
...prev,
[e.target.name]: e.target.value,
}));
};
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
// update global state hanya saat submit
editState.update.form = { editState.update.form = {
...editState.update.form, ...editState.update.form,
name: formData.name, name: formData.name,
@@ -94,10 +103,11 @@ function EditKategoriBerita() {
> >
<Stack gap="md"> <Stack gap="md">
<TextInput <TextInput
name="name"
label="Nama Kategori Berita" label="Nama Kategori Berita"
placeholder="Masukkan nama kategori berita" placeholder="Masukkan nama kategori berita"
value={formData.name} value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={handleChange}
required required
/> />

View File

@@ -7,10 +7,9 @@ import {
Group, Group,
Paper, Paper,
Stack, Stack,
Text,
TextInput, TextInput,
Title, Title,
Tooltip, Tooltip
} from '@mantine/core'; } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
@@ -62,9 +61,9 @@ function CreateKategoriBerita() {
> >
<Stack gap="md"> <Stack gap="md">
<TextInput <TextInput
label={<Text fw="bold" fz="sm">Nama Kategori Berita</Text>} label="Nama Kategori Berita"
placeholder="Masukkan nama kategori berita" placeholder="Masukkan nama kategori berita"
value={createState.create.form.name || ''} defaultValue={createState.create.form.name || ''}
onChange={(e) => (createState.create.form.name = e.target.value)} onChange={(e) => (createState.create.form.name = e.target.value)}
required required
/> />

View File

@@ -19,7 +19,12 @@ import {
Tooltip, Tooltip,
} from "@mantine/core"; } from "@mantine/core";
import { Dropzone } from "@mantine/dropzone"; import { Dropzone } from "@mantine/dropzone";
import { IconArrowBack, IconPhoto, IconUpload, IconX } from "@tabler/icons-react"; import {
IconArrowBack,
IconPhoto,
IconUpload,
IconX,
} from "@tabler/icons-react";
import { useParams, useRouter } from "next/navigation"; import { useParams, useRouter } from "next/navigation";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
@@ -33,16 +38,17 @@ function EditBerita() {
const [previewImage, setPreviewImage] = useState<string | null>(null); const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
judul: beritaState.berita.edit.form.judul || "", judul: "",
deskripsi: beritaState.berita.edit.form.deskripsi || "", deskripsi: "",
kategoriBeritaId: beritaState.berita.edit.form.kategoriBeritaId || "", kategoriBeritaId: "",
content: beritaState.berita.edit.form.content || "", content: "",
imageId: beritaState.berita.edit.form.imageId || "", imageId: "",
}); });
// Load berita by id saat pertama kali // Load kategori + berita
useEffect(() => { useEffect(() => {
beritaState.kategoriBerita.findMany.load(); beritaState.kategoriBerita.findMany.load();
const loadBerita = async () => { const loadBerita = async () => {
const id = params?.id as string; const id = params?.id as string;
if (!id) return; if (!id) return;
@@ -71,8 +77,13 @@ function EditBerita() {
loadBerita(); loadBerita();
}, [params?.id]); }, [params?.id]);
const handleChange = (field: string, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
// Update global state hanya sekali di sini
beritaState.berita.edit.form = { beritaState.berita.edit.form = {
...beritaState.berita.edit.form, ...beritaState.berita.edit.form,
...formData, ...formData,
@@ -103,6 +114,7 @@ function EditBerita() {
return ( return (
<Box px={{ base: "sm", md: "lg" }} py="md"> <Box px={{ base: "sm", md: "lg" }} py="md">
{/* Header */}
<Group mb="md"> <Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button <Button
@@ -119,6 +131,7 @@ function EditBerita() {
</Title> </Title>
</Group> </Group>
{/* Form */}
<Paper <Paper
w={{ base: "100%", md: "50%" }} w={{ base: "100%", md: "50%" }}
bg={colors["white-1"]} bg={colors["white-1"]}
@@ -132,22 +145,41 @@ function EditBerita() {
label="Judul" label="Judul"
placeholder="Masukkan judul" placeholder="Masukkan judul"
value={formData.judul} value={formData.judul}
onChange={(e) => onChange={(e) => handleChange("judul", e.target.value)}
setFormData({ ...formData, judul: e.target.value })
}
required required
/> />
<TextInput <Select
label="Deskripsi" value={formData.kategoriBeritaId}
placeholder="Masukkan deskripsi" onChange={(val) => handleChange("kategoriBeritaId", val || "")}
value={formData.deskripsi} label="Kategori"
onChange={(e) => placeholder="Pilih kategori"
setFormData({ ...formData, deskripsi: e.target.value }) data={
beritaState.kategoriBerita.findMany.data?.map((v) => ({
value: v.id,
label: v.name,
})) || []
} }
clearable
searchable
required required
error={!formData.kategoriBeritaId ? "Pilih kategori" : undefined}
/> />
<Box>
<Text fz="sm" fw="bold">
Deskripsi Singkat
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) =>
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }))
}
/>
</Box>
{/* Upload Gambar */}
<Box> <Box>
<Text fw="bold" fz="sm" mb={6}> <Text fw="bold" fz="sm" mb={6}>
Gambar Berita Gambar Berita
@@ -160,7 +192,9 @@ function EditBerita() {
setPreviewImage(URL.createObjectURL(selectedFile)); setPreviewImage(URL.createObjectURL(selectedFile));
} }
}} }}
onReject={() => toast.error("File tidak valid, gunakan format gambar")} onReject={() =>
toast.error("File tidak valid, gunakan format gambar")
}
maxSize={5 * 1024 ** 2} maxSize={5 * 1024 ** 2}
accept={{ "image/*": [] }} accept={{ "image/*": [] }}
radius="md" radius="md"
@@ -168,7 +202,11 @@ function EditBerita() {
> >
<Group justify="center" gap="xl" mih={180}> <Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept> <Dropzone.Accept>
<IconUpload size={48} color={colors["blue-button"]} stroke={1.5} /> <IconUpload
size={48}
color={colors["blue-button"]}
stroke={1.5}
/>
</Dropzone.Accept> </Dropzone.Accept>
<Dropzone.Reject> <Dropzone.Reject>
<IconX size={48} color="red" stroke={1.5} /> <IconX size={48} color="red" stroke={1.5} />
@@ -198,43 +236,26 @@ function EditBerita() {
objectFit: "contain", objectFit: "contain",
border: `1px solid ${colors["blue-button"]}`, border: `1px solid ${colors["blue-button"]}`,
}} }}
loading="lazy"
/> />
</Box> </Box>
)} )}
</Box> </Box>
{/* Konten */}
<Box> <Box>
<Text fz="sm" fw="bold"> <Text fz="sm" fw="bold">
Konten Konten
</Text> </Text>
<EditEditor <EditEditor
value={formData.content} value={formData.content}
onChange={(htmlContent) => { onChange={(htmlContent) =>
setFormData((prev) => ({ ...prev, content: htmlContent })); setFormData((prev) => ({ ...prev, content: htmlContent }))
beritaState.berita.edit.form.content = htmlContent; }
}}
/> />
</Box> </Box>
<Select {/* Action */}
value={formData.kategoriBeritaId}
onChange={(val) =>
setFormData({ ...formData, kategoriBeritaId: val || "" })
}
label="Kategori"
placeholder="Pilih kategori"
data={
beritaState.kategoriBerita.findMany.data?.map((v) => ({
value: v.id,
label: v.name,
})) || []
}
clearable
searchable
required
error={!formData.kategoriBeritaId ? "Pilih kategori" : undefined}
/>
<Group justify="right"> <Group justify="right">
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}

View File

@@ -80,7 +80,7 @@ function DetailBerita() {
<Box> <Box>
<Text fz="lg" fw="bold">Deskripsi</Text> <Text fz="lg" fw="bold">Deskripsi</Text>
<Text fz="md" c="dimmed">{data.deskripsi || '-'}</Text> <Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }} />
</Box> </Box>
<Box> <Box>
@@ -93,6 +93,7 @@ function DetailBerita() {
h={200} h={200}
radius="md" radius="md"
fit="cover" fit="cover"
loading='lazy'
/> />
) : ( ) : (
<Text fz="sm" c="dimmed">Tidak ada gambar</Text> <Text fz="sm" c="dimmed">Tidak ada gambar</Text>

View File

@@ -100,7 +100,7 @@ export default function CreateBerita() {
<TextInput <TextInput
label="Judul" label="Judul"
placeholder="Masukkan judul berita" placeholder="Masukkan judul berita"
value={beritaState.berita.create.form.judul} defaultValue={beritaState.berita.create.form.judul}
onChange={(e) => (beritaState.berita.create.form.judul = e.target.value)} onChange={(e) => (beritaState.berita.create.form.judul = e.target.value)}
required required
/> />
@@ -112,7 +112,7 @@ export default function CreateBerita() {
label: item.name, label: item.name,
value: item.id, value: item.id,
}))} }))}
value={beritaState.berita.create.form.kategoriBeritaId || null} defaultValue={beritaState.berita.create.form.kategoriBeritaId || null}
onChange={(val: string | null) => { onChange={(val: string | null) => {
if (val) { if (val) {
const selected = beritaState.kategoriBerita.findMany.data?.find( const selected = beritaState.kategoriBerita.findMany.data?.find(
@@ -131,12 +131,17 @@ export default function CreateBerita() {
required required
/> />
<TextInput <Box>
label="Deskripsi Singkat" <Text fz="sm" fw="bold" mb={6}>
placeholder="Masukkan deskripsi berita" Deskripsi Singkat
value={beritaState.berita.create.form.deskripsi} </Text>
onChange={(e) => (beritaState.berita.create.form.deskripsi = e.target.value)} <CreateEditor
/> value={beritaState.berita.create.form.deskripsi}
onChange={(htmlContent) => {
beritaState.berita.create.form.deskripsi = htmlContent;
}}
/>
</Box>
<Box> <Box>
<Text fw="bold" fz="sm" mb={6}> <Text fw="bold" fz="sm" mb={6}>
@@ -183,6 +188,7 @@ export default function CreateBerita() {
objectFit: 'contain', objectFit: 'contain',
border: '1px solid #ddd', border: '1px solid #ddd',
}} }}
loading="lazy"
/> />
</Box> </Box>
)} )}

View File

@@ -5,7 +5,6 @@ import {
Button, Button,
Center, Center,
Group, Group,
Image,
Pagination, Pagination,
Paper, Paper,
Skeleton, Skeleton,
@@ -18,7 +17,7 @@ import {
TableTr, TableTr,
Text, Text,
Title, Title,
Tooltip, Tooltip
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; import { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
@@ -87,7 +86,6 @@ function ListBerita({ search }: { search: string }) {
<TableTr> <TableTr>
<TableTh style={{ width: '30%' }}>Judul</TableTh> <TableTh style={{ width: '30%' }}>Judul</TableTh>
<TableTh style={{ width: '20%' }}>Kategori</TableTh> <TableTh style={{ width: '20%' }}>Kategori</TableTh>
<TableTh style={{ width: '25%' }}>Gambar</TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh> <TableTh style={{ width: '15%' }}>Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
@@ -96,28 +94,17 @@ function ListBerita({ search }: { search: string }) {
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd style={{ width: '30%' }}> <TableTd style={{ width: '30%' }}>
<Text fw={500} truncate="end" lineClamp={1}> <Box w={150}>
{item.judul} <Text fw={500} truncate="end" lineClamp={1}>
</Text> {item.judul}
</Text>
</Box>
</TableTd> </TableTd>
<TableTd style={{ width: '20%' }}> <TableTd style={{ width: '20%' }}>
<Text fz="sm" c="dimmed"> <Text fz="sm" c="dimmed">
{item.kategoriBerita?.name || '-'} {item.kategoriBerita?.name || '-'}
</Text> </Text>
</TableTd> </TableTd>
<TableTd style={{ width: '25%' }}>
<Box
w={80}
h={80}
style={{ borderRadius: 8, overflow: 'hidden' }}
>
{item.image?.link ? (
<Image src={item.image.link} alt="gambar" fit="cover" />
) : (
<Box bg={colors['blue-button']} w="100%" h="100%" />
)}
</Box>
</TableTd>
<TableTd style={{ width: '15%' }}> <TableTd style={{ width: '15%' }}>
<Button <Button
variant="light" variant="light"

View File

@@ -110,7 +110,7 @@ export default function ListImage() {
radius="md" radius="md"
onClick={() => { onClick={() => {
stateFileStorage stateFileStorage
.del({ name: v.name }) .del({ id: v.id })
.finally(() => toast("Foto berhasil dihapus")); .finally(() => toast("Foto berhasil dihapus"));
}} }}
> >
@@ -131,6 +131,7 @@ export default function ListImage() {
h={120} h={120}
fit="contain" fit="contain"
opacity={0.7} opacity={0.7}
loading="lazy"
/> />
<Text c="dimmed" ta="center"> <Text c="dimmed" ta="center">
Belum ada foto yang tersedia Belum ada foto yang tersedia
@@ -143,14 +144,15 @@ export default function ListImage() {
<Flex justify="center"> <Flex justify="center">
<Pagination <Pagination
total={total} total={total}
value={stateFileStorage.page} // Changed from page to value
size="md" size="md"
radius="md" radius="md"
withEdges withEdges
onChange={(page) => { onChange={(page) => {
stateFileStorage.page = page; stateFileStorage.load({ page });
stateFileStorage.load();
}} }}
/> />
</Flex> </Flex>
)} )}
</Stack> </Stack>

View File

@@ -1,7 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core'; import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { IconPhoto, IconVideo } from '@tabler/icons-react'; import { IconPhoto, IconVideo } from '@tabler/icons-react';
@@ -48,43 +48,50 @@ function LayoutTabsGallery({ children }: { children: React.ReactNode }) {
<Stack gap="lg"> <Stack gap="lg">
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Gallery</Title> <Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Gallery</Title>
<Tabs <Tabs
color={colors['blue-button']} color={colors["blue-button"]}
variant='pills' variant="pills"
value={activeTab} value={activeTab}
onChange={handleTabChange} onChange={handleTabChange}
radius="lg" radius="lg"
keepMounted={false} keepMounted={false}
> >
<TabsList {/* ✅ Scroll horizontal wrapper */}
p="sm" <ScrollArea type="auto" offsetScrollbars>
style={{ <TabsList
background: "linear-gradient(135deg, #e7ebf7, #f9faff)", p="sm"
borderRadius: "1rem", style={{
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)", background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
}} borderRadius: "1rem",
> boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
{tabs.map((tab, i) => ( display: "flex",
<Tooltip flexWrap: "nowrap",
key={i} gap: "0.5rem",
label={tab.tooltip} paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
position="bottom" }}
withArrow >
transitionProps={{ transition: 'pop', duration: 200 }} {tabs.map((tab, i) => (
> <Tooltip
<TabsTab key={i}
value={tab.value} label={tab.tooltip}
leftSection={tab.icon} position="bottom"
style={{ withArrow
fontWeight: 600, transitionProps={{ transition: 'pop', duration: 200 }}
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
> >
{tab.label} <TabsTab
</TabsTab> value={tab.value}
</Tooltip> leftSection={tab.icon}
))} style={{
</TabsList> fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
</ScrollArea>
{tabs.map((tab, i) => ( {tabs.map((tab, i) => (
<TabsPanel <TabsPanel

View File

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

View File

@@ -15,7 +15,7 @@ import {
} from '@mantine/core'; } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState, useCallback } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { convertYoutubeUrlToEmbed } from '../../../lib/youtube-utils'; import { convertYoutubeUrlToEmbed } from '../../../lib/youtube-utils';
@@ -31,17 +31,19 @@ function EditVideo() {
linkVideo: '', linkVideo: '',
}); });
// load data video sekali saat id ada
useEffect(() => { useEffect(() => {
const loadVideo = async () => { const loadVideo = async () => {
const id = params?.id as string; const id = params?.id as string;
if (!id) return; if (!id) return;
try { try {
const data = await videoState.update.load(id); const data = await videoState.update.load(id);
if (data) { if (data) {
setFormData({ setFormData({
name: data.name || '', name: data.name ?? '',
deskripsi: data.deskripsi || '', deskripsi: data.deskripsi ?? '',
linkVideo: data.linkVideo || '', linkVideo: data.linkVideo ?? '',
}); });
} }
} catch (error) { } catch (error) {
@@ -49,10 +51,16 @@ function EditVideo() {
toast.error('Gagal memuat data video'); toast.error('Gagal memuat data video');
} }
}; };
loadVideo(); loadVideo();
}, [params?.id]); }, [params?.id]);
const embedLink = convertYoutubeUrlToEmbed(formData.linkVideo); const handleChange = useCallback(
(field: keyof typeof formData, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
},
[]
);
const handleSubmit = async () => { const handleSubmit = async () => {
const converted = convertYoutubeUrlToEmbed(formData.linkVideo); const converted = convertYoutubeUrlToEmbed(formData.linkVideo);
@@ -63,7 +71,6 @@ function EditVideo() {
try { try {
videoState.update.form = { videoState.update.form = {
...videoState.update.form,
name: formData.name, name: formData.name,
deskripsi: formData.deskripsi, deskripsi: formData.deskripsi,
linkVideo: formData.linkVideo, linkVideo: formData.linkVideo,
@@ -77,11 +84,18 @@ function EditVideo() {
} }
}; };
const embedLink = convertYoutubeUrlToEmbed(formData.linkVideo);
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md"> <Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md"> <Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
</Tooltip> </Tooltip>
@@ -103,7 +117,7 @@ function EditVideo() {
label="Judul Video" label="Judul Video"
placeholder="Masukkan judul video" placeholder="Masukkan judul video"
value={formData.name} value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) => handleChange('name', e.currentTarget.value)}
required required
/> />
@@ -112,7 +126,7 @@ function EditVideo() {
label="Link Video YouTube" label="Link Video YouTube"
placeholder="https://www.youtube.com/watch?v=abc123" placeholder="https://www.youtube.com/watch?v=abc123"
value={formData.linkVideo} value={formData.linkVideo}
onChange={(e) => setFormData({ ...formData, linkVideo: e.currentTarget.value })} onChange={(e) => handleChange('linkVideo', e.currentTarget.value)}
required required
/> />
{embedLink && ( {embedLink && (
@@ -135,7 +149,7 @@ function EditVideo() {
</Title> </Title>
<EditEditor <EditEditor
value={formData.deskripsi} value={formData.deskripsi}
onChange={(val) => setFormData({ ...formData, deskripsi: val })} onChange={(val) => handleChange('deskripsi', val)}
/> />
</Box> </Box>

View File

@@ -102,6 +102,7 @@ function DetailVideo() {
fz="md" fz="md"
c="dimmed" c="dimmed"
dangerouslySetInnerHTML={{ __html: data.deskripsi }} dangerouslySetInnerHTML={{ __html: data.deskripsi }}
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
/> />
) : ( ) : (
<Text fz="sm" c="dimmed">Tidak ada deskripsi</Text> <Text fz="sm" c="dimmed">Tidak ada deskripsi</Text>

View File

@@ -80,7 +80,7 @@ function CreateVideo() {
<TextInput <TextInput
label="Judul Video" label="Judul Video"
placeholder="Masukkan judul video" placeholder="Masukkan judul video"
value={videoState.create.form.name} defaultValue={videoState.create.form.name}
onChange={(e) => { onChange={(e) => {
videoState.create.form.name = e.currentTarget.value; videoState.create.form.name = e.currentTarget.value;
}} }}
@@ -91,7 +91,7 @@ function CreateVideo() {
<TextInput <TextInput
label="Link Video YouTube" label="Link Video YouTube"
placeholder="https://www.youtube.com/watch?v=abc123" placeholder="https://www.youtube.com/watch?v=abc123"
value={link} defaultValue={link}
onChange={(e) => setLink(e.currentTarget.value)} onChange={(e) => setLink(e.currentTarget.value)}
required required
/> />

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