Compare commits

...

18 Commits

Author SHA1 Message Date
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
646 changed files with 25418 additions and 15439 deletions

3
.gitignore vendored
View File

@@ -41,6 +41,9 @@ next-env.d.ts
# uploads
/uploads
# download
/download
# cache
/cache

BIN
bun.lockb

Binary file not shown.

View File

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

View File

@@ -3,7 +3,7 @@
"version": "0.1.5",
"private": true,
"scripts": {
"dev": "bun --bun next dev",
"dev": "bun --bun next dev --hostname 0.0.0.0",
"build": "bun --bun next build",
"start": "bun --bun next start"
},
@@ -39,19 +39,23 @@
"@tiptap/pm": "^2.11.7",
"@tiptap/react": "^2.11.7",
"@tiptap/starter-kit": "^2.11.7",
"@types/adm-zip": "^0.5.7",
"@types/bun": "^1.2.2",
"@types/leaflet": "^1.9.20",
"@types/lodash": "^4.17.16",
"add": "^2.0.6",
"adm-zip": "^0.5.16",
"animate.css": "^4.1.1",
"bcryptjs": "^3.0.2",
"bun": "^1.2.2",
"chart.js": "^4.4.8",
"classnames": "^2.5.1",
"colors": "^1.4.0",
"dayjs": "^1.11.13",
"elysia": "^1.3.5",
"embla-carousel-autoplay": "^8.5.2",
"embla-carousel-react": "^7.1.0",
"extract-zip": "^2.0.1",
"form-data": "^4.0.2",
"framer-motion": "^12.23.5",
"get-port": "^7.1.0",
@@ -80,9 +84,11 @@
"react-transition-group": "^4.4.5",
"readdirp": "^4.1.1",
"recharts": "^2.15.3",
"sharp": "^0.34.3",
"swr": "^2.3.2",
"uuid": "^11.1.0",
"valtio": "^2.1.3",
"zlib": "^1.0.5",
"zod": "^3.24.3"
},
"devDependencies": {

View File

@@ -1,51 +1,99 @@
[
{
"month": "Jan",
"year": 2025,
"totalUnemployment": 160,
"educatedUnemployment": 95,
"uneducatedUnemployment": 65,
"percentageChange": null
},
{
"month": "Feb",
"year": 2025,
"totalUnemployment": 155,
"educatedUnemployment": 90,
"uneducatedUnemployment": 65,
"percentageChange": -3.1
},
{
"month": "Mar",
"year": 2025,
"totalUnemployment": 150,
"educatedUnemployment": 88,
"uneducatedUnemployment": 62,
"percentageChange": -3.2
},
{
"month": "Apr",
"year": 2025,
"totalUnemployment": 148,
"educatedUnemployment": 85,
"uneducatedUnemployment": 63,
"percentageChange": -1.3
},
{
"month": "Mei",
"year": 2025,
"totalUnemployment": 145,
"educatedUnemployment": 82,
"uneducatedUnemployment": 63,
"percentageChange": -2.0
},
{
"month": "Jun",
"year": 2025,
"totalUnemployment": 140,
"educatedUnemployment": 80,
"uneducatedUnemployment": 60,
"percentageChange": -3.4
}
]
{
"month": "Jan",
"year": 2025,
"totalUnemployment": 160,
"educatedUnemployment": 95,
"uneducatedUnemployment": 65,
"percentageChange": 0.0
},
{
"month": "Feb",
"year": 2025,
"totalUnemployment": 158,
"educatedUnemployment": 93,
"uneducatedUnemployment": 65,
"percentageChange": -1.25
},
{
"month": "Mar",
"year": 2025,
"totalUnemployment": 155,
"educatedUnemployment": 91,
"uneducatedUnemployment": 64,
"percentageChange": -1.90
},
{
"month": "Apr",
"year": 2025,
"totalUnemployment": 152,
"educatedUnemployment": 89,
"uneducatedUnemployment": 63,
"percentageChange": -1.94
},
{
"month": "Mei",
"year": 2025,
"totalUnemployment": 150,
"educatedUnemployment": 88,
"uneducatedUnemployment": 62,
"percentageChange": -1.32
},
{
"month": "Jun",
"year": 2025,
"totalUnemployment": 148,
"educatedUnemployment": 87,
"uneducatedUnemployment": 61,
"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",
"name": "Telephone Desa Darmasaba",
"iconUrl": "081239580000"
"iconUrl": "081239580000",
"imageId": "cmff3nv180003vn6h5jvedidq"
},
{
"id": "cmds8z7u20005vnbegyyvnbk0",
"name": "Email Desa Darmasaba",
"iconUrl": "desadarmasaba@badungkab.go.id"
"iconUrl": "desadarmasaba@badungkab.go.id",
"imageId": "cmff3ll130001vn6hkhls3f5y"
},
{
"id": "cmds9023u0008vnbe3oxmhwyf",
"name": "Desa Darmasaba",
"iconUrl": "https://www.youtube.com/channel/UCtPw9WOQO7d2HIKzKgel4Xg"
"iconUrl": "https://www.youtube.com/channel/UCtPw9WOQO7d2HIKzKgel4Xg",
"imageId": "cmff3joae0000vn6h8sgs0ilg"
},
{
"id": "cmds90oul000bvnbe2bqkptoi",
"name": "Pemerintah Desa Darmasaba",
"iconUrl": "https://www.facebook.com/DarmasabaDesaku"
"iconUrl": "https://www.facebook.com/DarmasabaDesaku",
"imageId": "cmff3mtat0002vn6hs8vyyhdd"
},
{
"id": "cmds91i4e000evnbe8gtf1gub",
"name": "ddarmasaba",
"iconUrl": "https://www.instagram.com/ddarmasaba/"
"iconUrl": "https://www.instagram.com/ddarmasaba/",
"imageId": "cmff3oouh0004vn6hd94brzv9"
},
{
"id": "cmds92de5000hvnbemlu6sq5x",
"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",
"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",
"name": "Dmangan",
"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",
"name": "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",
"name": "Bares",
"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",
"name": "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",
"name": "PDKT",
"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",
"name": "GM",
"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",
"name": "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": "1",
"id": "role-1",
"name": "ADMIN DESA",
"description": "Administrator Desa",
"permissions": ["manage_users", "manage_content", "view_reports"],
"isActive": true,
"createdAt": "2025-09-01T00:00:00.000Z",
"updatedAt": "2025-09-01T00:00:00.000Z"
"isActive": true
},
{
"id": "2",
"id": "role-2",
"name": "ADMIN KESEHATAN",
"description": "Administrator Bidang Kesehatan",
"permissions": ["manage_health_data", "view_reports"],
"isActive": true,
"createdAt": "2025-09-01T00:00:00.000Z",
"updatedAt": "2025-09-01T00:00:00.000Z"
"isActive": true
},
{
"id": "3",
"id": "role-3",
"name": "ADMIN SEKOLAH",
"description": "Administrator Sekolah",
"permissions": ["manage_school_data", "view_reports"],
"isActive": true,
"createdAt": "2025-09-01T00:00:00.000Z",
"updatedAt": "2025-09-01T00:00:00.000Z"
"isActive": true
}
]

View File

@@ -1,32 +1,23 @@
[
{
"id": "1",
"nama": "Admin Desa",
"nomor": "089647037426",
"roleId": "1",
"isActive": true,
"lastLogin": "2025-08-31T10:00:00.000Z",
"createdAt": "2025-09-01T00:00:00.000Z",
"updatedAt": "2025-09-01T00:00:00.000Z"
},
{
"id": "2",
"nama": "Admin Kesehatan",
"nomor": "082339004198",
"roleId": "2",
"isActive": true,
"lastLogin": null,
"createdAt": "2025-09-01T00:00:00.000Z",
"updatedAt": "2025-09-01T00:00:00.000Z"
},
{
"id": "3",
"nama": "Admin Sekolah",
"nomor": "085237157222",
"roleId": "3",
"isActive": true,
"lastLogin": null,
"createdAt": "2025-09-01T00:00:00.000Z",
"updatedAt": "2025-09-01T00:00:00.000Z"
}
]
{
"id": "user-1",
"nama": "Admin Desa",
"nomor": "089647037426",
"roleId": "role-1",
"isActive": true
},
{
"id": "user-2",
"nama": "Admin Kesehatan",
"nomor": "082339004198",
"roleId": "role-2",
"isActive": true
},
{
"id": "user-3",
"nama": "Admin Sekolah",
"nomor": "085237157222",
"roleId": "role-3",
"isActive": true
}
]

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")
PelayananSuratKeteranganImage2 PelayananSuratKeterangan[] @relation("PelayananSuratKeteranganImage2")
PasarDesa PasarDesa[]
KontakDaruratKeamanan KontakDaruratKeamanan[]
KontakItem KontakItem[]
Pegawai Pegawai[]
DesaDigital DesaDigital[]
InfoTekno InfoTekno[]
@@ -92,7 +90,7 @@ model FileStorage {
PejabatDesa PejabatDesa[]
MediaSosial MediaSosial[]
DesaAntiKorupsi DesaAntiKorupsi[]
SDGSDesa SDGSDesa[]
SDGSDesa SdgsDesa[]
APBDesImage APBDes[] @relation("APBDesImage")
APBDesFile APBDes[] @relation("APBDesFile")
PrestasiDesa PrestasiDesa[]
@@ -101,6 +99,8 @@ model FileStorage {
PerbekelDariMasaKeMasa PerbekelDariMasaKeMasa[]
MitraKolaborasi MitraKolaborasi[]
ArtikelKesehatan ArtikelKesehatan[]
}
//========================================= MENU LANDING PAGE ========================================= //
@@ -168,9 +168,9 @@ model KategoriDesaAntiKorupsi {
}
//========================================= SDGS Desa ========================================= //
model SDGSDesa {
model SdgsDesa {
id String @id @default(cuid())
name String @unique
name String
jumlah String
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
@@ -183,7 +183,7 @@ model SDGSDesa {
//========================================= APBDes ========================================= //
model APBDes {
id String @id @default(cuid())
name String @unique
name String
jumlah String
image FileStorage? @relation("APBDesImage", fields: [imageId], references: [id])
imageId String?
@@ -198,7 +198,7 @@ model APBDes {
//========================================= PRESTASI DESA ========================================= //
model PrestasiDesa {
id String @id @default(cuid())
name String @unique
name String
deskripsi String @db.Text
kategori KategoriPrestasiDesa @relation(fields: [kategoriId], references: [id])
kategoriId String
@@ -672,17 +672,18 @@ model GalleryVideo {
// ========================================= LAYANAN DESA ========================================= //
model PelayananSuratKeterangan {
id String @id @default(cuid())
name String
deskripsi String @db.Text
image FileStorage? @relation("PelayananSuratKeteranganImage", fields: [imageId], references: [id])
imageId String?
image2 FileStorage? @relation("PelayananSuratKeteranganImage2", fields: [image2Id], references: [id])
image2Id String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
id String @id @default(cuid())
name String
deskripsi String @db.Text
image FileStorage? @relation("PelayananSuratKeteranganImage", fields: [imageId], references: [id])
imageId String?
image2 FileStorage? @relation("PelayananSuratKeteranganImage2", fields: [image2Id], references: [id])
image2Id String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
AjukanPermohonan AjukanPermohonan[]
}
model PelayananTelunjukSaktiDesa {
@@ -717,6 +718,20 @@ model PelayananPendudukNonPermanen {
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 ========================================= //
model Penghargaan {
id String @id @default(cuid())
@@ -835,8 +850,8 @@ model JadwalKegiatan {
syaratKetentuanJadwalKegiatanId String
dokumenjadwalkegiatan DokumenJadwalKegiatan @relation(fields: [dokumenJadwalKegiatanId], references: [id])
dokumenJadwalKegiatanId String
pendaftaranjadwalkegiatan PendaftaranJadwalKegiatan @relation(fields: [pendaftaranJadwalKegiatanId], references: [id])
pendaftaranJadwalKegiatanId String
pendaftaranjadwalkegiatan PendaftaranJadwalKegiatan? @relation(fields: [pendaftaranJadwalKegiatanId], references: [id])
pendaftaranJadwalKegiatanId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
@@ -972,8 +987,10 @@ model ArtikelKesehatan {
id String @id @default(cuid())
title String
content String
introduction Introduction @relation(fields: [introductionId], references: [id])
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
introductionId String
introduction Introduction @relation(fields: [introductionId], references: [id])
symptom Symptom @relation(fields: [symptomId], references: [id])
symptomId String
prevention Prevention @relation(fields: [preventionId], references: [id])
@@ -1218,8 +1235,7 @@ model LayananPolsek {
model KontakDaruratKeamanan {
id String @id @default(uuid())
nama String
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
icon String
kategori KontakItem @relation(fields: [kategoriId], references: [id])
kategoriId String
kontakItems KontakDaruratToItem[]
@@ -1233,8 +1249,7 @@ model KontakItem {
id String @id @default(uuid())
nama String
nomorTelepon String
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
icon String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
@@ -1254,48 +1269,15 @@ model KontakDaruratToItem {
// ========================================= PENCEGAHAN KRIMINALITAS ========================================= //
model PencegahanKriminalitas {
id String @id @default(cuid())
programKeamanan ProgramKeamanan @relation(fields: [programKeamananId], references: [id])
programKeamananId String
tipsKeamanan TipsKeamanan @relation(fields: [tipsKeamananId], references: [id])
tipsKeamananId String
videoKeamanan VideoKeamanan @relation(fields: [videoKeamananId], references: [id])
videoKeamananId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
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[]
id String @id @default(cuid())
judul String
deskripsi String
deskripsiSingkat String
linkVideo String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= LAPORAN PUBLIK ========================================= //
@@ -1304,11 +1286,13 @@ model LaporanPublik {
judul String
lokasi String
tanggalWaktu DateTime
status StatusLaporan
status StatusLaporan @default(Proses)
penanganan PenangananLaporanPublik[]
kronologi String? // Optional, bisa diisi detail kronologi
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model PenangananLaporanPublik {
@@ -1416,6 +1400,9 @@ model PosisiOrganisasi {
pegawai Pegawai[]
strukturOrganisasi StrukturOrganisasi[] // Relasi balik
StrukturOrganisasiPPID StrukturOrganisasiPPID[]
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("posisi_organisasi")
}
@@ -1484,7 +1471,7 @@ model ProgramKemiskinan {
id String @id @default(uuid())
nama String
deskripsi String
ikonUrl String?
icon String
isActive Boolean @default(true)
statistikId String? @unique
statistik StatistikKemiskinan? @relation(fields: [statistikId], references: [id])
@@ -1566,7 +1553,7 @@ model DataDemografiPekerjaan {
model DetailDataPengangguran {
id String @id @default(uuid()) @db.Uuid
month String @db.VarChar(20)
year DateTime
year Int
totalUnemployment Int
educatedUnemployment Int
uneducatedUnemployment Int

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import prisma from "@/lib/prisma";
import profilePejabatDesa from "./data/landing-page/profile/profile.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 dasarHukumPPID from "./data/ppid/dasar-hukum-ppid/dasarhukumPPID.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 umurResponden from "./data/ppid/ikm/umur-responden/umur-responden.json";
import categoryPengumuman from "./data/category-pengumuman.json";
@@ -54,63 +55,73 @@ import programUnggulan from "./data/pendidikan/program-pendidikan-anak/program-u
import tujuanProgram from "./data/pendidikan/program-pendidikan-anak/tujuan-program.json";
import roles from "./data/user/roles.json";
import users from "./data/user/users.json";
import fileStorage from "./data/file-storage.json";
import seedAssets from "./seed_assets";
import { safeSeedUnique } from "./safeseedUnique";
(async () => {
// =========== USER & ROLE ===========
// In your seed.ts
// =========== ROLES ===========
console.log("🔄 Seeding roles...");
for (const r of roles) {
await prisma.role.upsert({
where: { id: r.id },
update: {
// =========== ROLES ===========
console.log("🔄 Seeding roles...");
for (const r of roles) {
await safeSeedUnique("role", { id: r.id }, {
name: r.name,
description: r.description,
permissions: r.permissions,
isActive: r.isActive,
},
create: {
id: r.id,
name: r.name,
description: r.description,
permissions: r.permissions,
isActive: r.isActive,
},
});
}
console.log("✅ Roles seeded");
// =========== USERS ===========
console.log("🔄 Seeding users...");
for (const u of users) {
// First verify the role exists
const roleExists = await prisma.role.findUnique({
where: { id: u.roleId }
});
if (!roleExists) {
console.error(`❌ Role with id ${u.roleId} not found for user ${u.nama}`);
continue;
});
}
await prisma.user.upsert({
where: { id: u.id },
update: {
console.log("✅ Roles seeded");
// =========== USERS ===========
console.log("🔄 Seeding users...");
for (const u of users) {
// First verify the role exists
const roleExists = await prisma.role.findUnique({
where: { id: u.roleId },
});
if (!roleExists) {
console.error(`❌ Role with id ${u.roleId} not found for user ${u.nama}`);
continue;
}
await safeSeedUnique("user", { id: u.id }, {
username: u.nama,
nomor: u.nomor,
roleId: u.roleId,
isActive: u.isActive,
},
create: {
id: u.id,
username: u.nama,
nomor: u.nomor,
roleId: u.roleId,
isActive: u.isActive,
},
});
}
console.log("✅ Users seeded");
isActive: u.isActive,
});
}
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 ===========
// =========== SUBMENU PROFILE ===========
// =========== PROFILE PEJABAT DESA ===========
@@ -120,11 +131,13 @@ console.log("✅ Users seeded");
update: {
name: p.name,
position: p.position,
imageId: p.imageId,
},
create: {
id: p.id,
name: p.name,
position: p.position,
imageId: p.imageId,
},
});
}
@@ -134,18 +147,35 @@ console.log("✅ Users seeded");
// =========== PROGRAM INOVASI ===========
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({
where: { id: p.id },
update: {
name: p.name,
description: p.description,
link: p.link,
imageId: p.imageId,
},
create: {
id: p.id,
name: p.name,
description: p.description,
link: p.link,
imageId: p.imageId,
},
});
}
@@ -158,11 +188,13 @@ console.log("✅ Users seeded");
update: {
name: p.name,
iconUrl: p.iconUrl,
imageId: p.imageId,
},
create: {
id: p.id,
name: p.name,
iconUrl: p.iconUrl,
imageId: p.imageId,
},
});
}
@@ -308,16 +340,14 @@ console.log("✅ Users seeded");
// =========== SDGSDesa ===========
for (const l of sdgsDesa) {
await prisma.sDGSDesa.upsert({
where: {
name: l.name,
jumlah: l.jumlah,
},
await prisma.sdgsDesa.upsert({
where: { id: l.id },
update: {
name: l.name,
jumlah: l.jumlah,
},
create: {
id: l.id,
name: l.name,
jumlah: l.jumlah,
},
@@ -330,8 +360,7 @@ console.log("✅ Users seeded");
for (const l of apbdes) {
await prisma.aPBDes.upsert({
where: {
name: l.name,
jumlah: l.jumlah,
id: l.id,
},
update: {
name: l.name,
@@ -558,29 +587,29 @@ console.log("✅ Users seeded");
}
console.log("dasar hukum PPID success ...");
// =========== SUBMENU DAFTAR INFORMASI PUBLIK PPID ===========
for (const v of daftarInformasiPublik) {
// Convert string date to Date object
const tanggal = new Date(v.tanggal);
await prisma.daftarInformasiPublik.upsert({
where: {
id: v.id,
},
update: {
jenisInformasi: v.jenisInformasi,
deskripsi: v.deskripsi,
tanggal: tanggal,
},
create: {
id: v.id,
jenisInformasi: v.jenisInformasi,
deskripsi: v.deskripsi,
tanggal: tanggal,
},
});
}
console.log("daftar informasi publik PPID success ...");
// =========== SUBMENU DAFTAR INFORMASI PUBLIK PPID ===========
for (const v of daftarInformasiPublik) {
// Convert string date to Date object
const tanggal = new Date(v.tanggal);
await prisma.daftarInformasiPublik.upsert({
where: {
id: v.id,
},
update: {
jenisInformasi: v.jenisInformasi,
deskripsi: v.deskripsi,
tanggal: tanggal,
},
create: {
id: v.id,
jenisInformasi: v.jenisInformasi,
deskripsi: v.deskripsi,
tanggal: tanggal,
},
});
}
console.log("daftar informasi publik PPID success ...");
for (const l of pelayananPerizinanBerusaha) {
await prisma.pelayananPerizinanBerusaha.upsert({
@@ -849,12 +878,9 @@ console.log("✅ Users seeded");
console.log("hubungan organisasi success ...");
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({
where: {
month_year: { month: d.month, year: yearAsDate },
month_year: { month: d.month, year: d.year },
},
update: {
totalUnemployment: d.totalUnemployment,
@@ -864,7 +890,7 @@ console.log("✅ Users seeded");
},
create: {
month: d.month,
year: yearAsDate,
year: d.year,
totalUnemployment: d.totalUnemployment,
educatedUnemployment: d.educatedUnemployment,
uneducatedUnemployment: d.uneducatedUnemployment,
@@ -1127,6 +1153,10 @@ console.log("✅ Users seeded");
console.log(
"✅ fasilitas bimbingan belajar desa seeded (editable later via UI)"
);
// seed assets
await seedAssets();
})()
.then(() => prisma.$disconnect())
.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.

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}>
<Flex>
<Image
loading="lazy"
src={images["darmasaba-icon"]}
alt="darmasaba"
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 {
IconAlertTriangle,
IconAmbulance,
IconBuilding,
IconCash,
IconChartLine,
IconChristmasTreeFilled,
IconClipboardTextFilled,
IconDroplet,
IconFiretruck,
IconHome,
IconHomeEco,
IconHospital,
IconLeaf,
IconRecycle,
IconScale,
IconSchool,
IconShieldFilled,
IconShoppingCart,
IconTent,
IconTrashFilled,
IconTree,
@@ -32,13 +40,23 @@ const iconMap = {
scale: { label: 'Scale', icon: IconScale },
clipboard: { label: 'Clipboard', icon: IconClipboardTextFilled },
trash: { label: 'Trash', icon: IconTrashFilled },
lingkunganSehat: {label: 'Lingkungan Sehat', icon: IconHomeEco},
sumberOksigen: {label: 'Sumber Oksigen', icon: IconChristmasTreeFilled},
ekonomiBerkelanjutan: {label: 'Ekonomi Berkelanjutan', icon: IconTrendingUp},
mencegahBencana: {label: 'Mencegah Bencana', icon: IconShieldFilled},
rumah: {label: 'Rumah', icon: IconHome},
pohon: {label: 'Pohon', icon: IconTree},
air: {label: 'Air', icon: IconDroplet}
lingkunganSehat: { label: 'Lingkungan Sehat', icon: IconHomeEco },
sumberOksigen: { label: 'Sumber Oksigen', icon: IconChristmasTreeFilled },
ekonomiBerkelanjutan: { label: 'Ekonomi Berkelanjutan', icon: IconTrendingUp },
mencegahBencana: { label: 'Mencegah Bencana', icon: IconShieldFilled },
rumah: { label: 'Rumah', icon: IconHome },
pohon: { label: 'Pohon', icon: IconTree },
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 {
IconAmbulance,
IconCash,
IconChartLine,
IconChristmasTreeFilled,
IconClipboardTextFilled,
IconDroplet,
IconFiretruck,
IconHome,
IconHomeEco,
IconHospital,
IconLeaf,
IconRecycle,
IconScale,
IconSchool,
IconShieldFilled,
IconShoppingCart,
IconTent,
IconTrashFilled,
IconTree,
IconTrendingUp,
IconTrophy,
IconTruckFilled,
IconBuilding,
IconAlertTriangle
} from '@tabler/icons-react';
const iconMap = {
@@ -36,7 +44,17 @@ const iconMap = {
mencegahBencana: {label: 'Mencegah Bencana', icon: IconShieldFilled},
rumah: {label: 'Rumah', icon: IconHome},
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;

View File

@@ -71,6 +71,22 @@ const pelayananPendudukNonPermanenForm = {
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({
create: {
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: {
data: null as Prisma.PelayananSuratKeteranganGetPayload<{
include: {
@@ -769,11 +809,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({
suratKeterangan,
pelayananPerizinanBerusaha,
pelayananTelunjukSaktiDesa,
pelayananPendudukNonPermanen,
ajukanPermohonan,
});
export default stateLayananDesa;

View File

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

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
@@ -71,12 +72,37 @@ const demografiPekerjaan = proxy({
omit: { isActive: true };
}>[]
| null,
async load() {
const res = await ApiFetch.api.ekonomi.demografipekerjaan[
"find-many"
].get();
if (res.status === 200) {
demografiPekerjaan.findMany.data = res.data?.data ?? [];
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
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 { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
@@ -69,16 +70,37 @@ const jumlahPendudukMiskin = proxy({
select: { id: true; year: true; totalPoorPopulation: true };
}>[]
| null,
loading: false,
async load() {
const res = await ApiFetch.api.ekonomi.jumlahpendudukmiskin[
"find-many"
].get();
if (res.status === 200) {
jumlahPendudukMiskin.findMany.data = res.data?.data ?? [];
}
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
jumlahPendudukMiskin.findMany.loading = true; // ✅ Akses langsung via nama path
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: {
data: null as Prisma.GrafikJumlahPendudukMiskinGetPayload<{
select: { id: true; year: true; totalPoorPopulation: true };

View File

@@ -15,7 +15,8 @@ const templateJumlahPengngguran = z.object({
uneducatedUnemployment: z
.number()
.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 = {
@@ -29,7 +30,7 @@ type JumlahPengangguran = {
const jumlahPengangguranForm: JumlahPengangguran = {
month: "",
year: 0,
year: new Date().getFullYear(), // Default to current year
totalUnemployment: 0,
educatedUnemployment: 0,
uneducatedUnemployment: 0,
@@ -60,13 +61,21 @@ const jumlahPengangguran = proxy({
form: jumlahPengangguranForm,
loading: false,
async create() {
const cek = templateJumlahPengngguran.safeParse(
jumlahPengangguran.create.form
);
// Ensure all number fields are actual numbers
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) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
.map((v) => `${v.path.join(".")} (${v.message})`)
.join("\n")}]`;
toast.error(err);
return null;
}
@@ -78,7 +87,7 @@ const jumlahPengangguran = proxy({
].post(jumlahPengangguran.create.form);
if (res.status === 200) {
const id = res.data?.data?.id;
const id = res.data?.id;
if (id) {
toast.success("Success create");
jumlahPengangguran.create.form = { ...jumlahPengangguranForm };
@@ -103,16 +112,40 @@ const jumlahPengangguran = proxy({
omit: { isActive: true };
}>[]
| null,
async load() {
const res =
await ApiFetch.api.ekonomi.jumlahpengangguran.detaildatapengangguran[
"find-many"
].get();
if (res.status === 200) {
jumlahPengangguran.findMany.data = res.data?.data ?? [];
}
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
jumlahPengangguran.findMany.loading = true; // ✅ Akses langsung via nama path
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: {
data: null as Prisma.DetailDataPengangguranGetPayload<{

View File

@@ -8,7 +8,7 @@ import { z } from "zod";
const templateForm = z.object({
nama: z.string().min(1, "Nama minimal 1 karakter"),
deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
ikonUrl: z.string().optional(),
icon: z.string().min(1, "Icon minimal 1 karakter"),
statistik: z.object({
tahun: z.string().min(1, "Tahun minimal 1 karakter"),
jumlah: z.string().min(1, "Jumlah minimal 1 karakter"),
@@ -18,7 +18,7 @@ const templateForm = z.object({
const defaultForm = {
nama: "",
deskripsi: "",
ikonUrl: "",
icon: "",
statistik: {
tahun: "",
jumlah: "",
@@ -148,7 +148,7 @@ const programKemiskinanState = proxy({
this.form = {
nama: data.nama,
deskripsi: data.deskripsi,
ikonUrl: data.ikonUrl || "",
icon: data.icon,
statistik: {
tahun: data.statistik.tahun,
jumlah: data.statistik.jumlah,
@@ -189,7 +189,7 @@ const programKemiskinanState = proxy({
body: JSON.stringify({
nama: this.form.nama,
deskripsi: this.form.deskripsi,
ikonUrl: this.form.ikonUrl,
icon: this.form.icon,
statistik: {
tahun: this.form.statistik.tahun,
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 { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
@@ -76,13 +77,37 @@ const grafikSektorUnggulan = proxy({
};
}>[]
| null,
page: 1,
totalPages: 1,
loading: false,
async load() {
const res = await ApiFetch.api.ekonomi.sektourunggulandesa[
"find-many"
].get();
if (res.status === 200) {
grafikSektorUnggulan.findMany.data = res.data?.data ?? [];
search: "",
load: async (page = 1, limit = 10, search = "") => {
grafikSektorUnggulan.findMany.loading = true; // ✅ Akses langsung via nama path
grafikSektorUnggulan.findMany.page = page;
grafikSektorUnggulan.findMany.search = search;
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;
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 {
const res = await ApiFetch.api.ekonomi["struktur-organisasi"][
"posisi-organisasi"
]["find-many"].get();
if (res.status === 200) {
// The API now returns the id field, so we can use it directly
this.data = res.data?.data ?? [];
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.ekonomi["struktur-organisasi"]["posisi-organisasi"]["find-many"].get({ query });
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) {
console.error("Find many error:", error);
this.data = [];
} catch (err) {
console.error("Gagal fetch posisi organisasi paginated:", err);
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 { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
@@ -75,16 +76,37 @@ const grafikBerdasarkanUsiaKerjaNganggur = proxy({
omit: { isActive: true };
}>[]
| null,
loading: false,
async load() {
const res = await ApiFetch.api.ekonomi.grafikusiakerjayangmenganggur[
"find-many"
].get();
if (res.status === 200) {
grafikBerdasarkanUsiaKerjaNganggur.findMany.data = res.data?.data ?? [];
}
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
grafikBerdasarkanUsiaKerjaNganggur.findMany.loading = true; // ✅ Akses langsung via nama path
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: {
data: null as Prisma.GrafikMenganggurBerdasarkanUsiaGetPayload<{
omit: { isActive: true };
@@ -259,15 +281,36 @@ const grafikBerdasarkanPendidikan = proxy({
omit: { isActive: true };
}>[]
| null,
loading: false,
async load() {
const res = await ApiFetch.api.ekonomi.grafikmenganggurberdasarkanpendidikan[
"find-many"
].get();
if (res.status === 200) {
grafikBerdasarkanPendidikan.findMany.data = res.data?.data ?? [];
}
},
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
grafikBerdasarkanPendidikan.findMany.loading = true; // ✅ Akses langsung via nama path
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: {
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 { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
@@ -61,10 +62,37 @@ const ajukanIdeInovatifState = proxy({
};
}>[]
| null,
async load() {
const res = await ApiFetch.api.inovasi.ajukanideinovatif["find-many"].get();
if (res.status === 200) {
ajukanIdeInovatifState.findMany.data = res.data?.data ?? [];
page: 1,
totalPages: 1,
loading: false,
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 {
ajukanIdeInovatifState.delete.loading = true;
const response = await fetch(`/api/inovasi/ajukanideinovatif/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const response = await fetch(
`/api/inovasi/ajukanideinovatif/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok) {
toast.success(result.message || "Ajukan Ide Inovatif berhasil dihapus");
toast.success(
result.message || "Ajukan Ide Inovatif berhasil dihapus"
);
await ajukanIdeInovatifState.findMany.load();
} else {
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 { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
@@ -54,19 +55,20 @@ const administrasiOnline = proxy({
},
findMany: {
data: null as Array<
Prisma.AdministrasiOnlineGetPayload<{
include: {
jenisLayanan: true;
};
}>
> | null,
Prisma.AdministrasiOnlineGetPayload<{
include: {
jenisLayanan: true;
};
}>
> | null,
page: 1,
totalPages: 1,
loading: false,
async load(page = 1, limit = 10) {
search: "",
async load(page = 1, limit = 10, search = "") {
administrasiOnline.findMany.loading = true;
administrasiOnline.findMany.page = page;
administrasiOnline.findMany.search = search;
try {
const res =
await ApiFetch.api.inovasi.layananonlinedesa.administrasionline[
@@ -75,6 +77,7 @@ const administrasiOnline = proxy({
query: {
page,
limit,
search,
},
});
@@ -91,10 +94,10 @@ const administrasiOnline = proxy({
},
findUnique: {
data: null as Prisma.AdministrasiOnlineGetPayload<{
include: {
jenisLayanan: true;
};
}> | null,
include: {
jenisLayanan: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(
@@ -199,13 +202,37 @@ const jenisLayanan = proxy({
nama: string;
deskripsi: string;
}> | null,
async load() {
const res =
await ApiFetch.api.inovasi.layananonlinedesa.administrasionline.jenislayanan[
"find-many"
].get();
if (res.status === 200) {
jenisLayanan.findMany.data = res.data?.data ?? [];
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
jenisLayanan.findMany.loading = true; // ✅ Akses langsung via nama path
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"),
judulPengaduan: z.string().min(1, "Judul pengaduan minimal 1 karakter"),
lokasiKejadian: z.string().min(1, "Lokasi kejadian minimal 1 karakter"),
deskripsiPengaduan: z.string().min(1, "Deskripsi pengaduan minimal 1 karakter"),
deskripsiPengaduan: z
.string()
.min(1, "Deskripsi pengaduan minimal 1 karakter"),
jenisPengaduanId: z.string().min(1, "Jenis pengaduan minimal 1 karakter"),
imageId: z.string().min(1, "Image minimal 1 karakter"),
});
@@ -455,37 +484,42 @@ const pengaduanMasyarakat = proxy({
},
findMany: {
data: null as Array<
Prisma.PengaduanMasyarakatGetPayload<{
include: {
jenisPengaduan: true;
image: true;
};
}>
> | null,
Prisma.PengaduanMasyarakatGetPayload<{
include: {
jenisPengaduan: true;
image: true;
};
}>
> | null,
page: 1,
totalPages: 1,
loading: false,
async load(page = 1, limit = 10) {
pengaduanMasyarakat.findMany.loading = true;
search: "",
load: async (page = 1, limit = 10, search = "") => {
pengaduanMasyarakat.findMany.loading = true; // ✅ Akses langsung via nama path
pengaduanMasyarakat.findMany.page = page;
pengaduanMasyarakat.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res =
await ApiFetch.api.inovasi.layananonlinedesa.pengaduanmasyarakat[
"find-many"
].get({
query: {
page,
limit,
},
});
].get({ query });
if (res.status === 200 && res.data?.success) {
pengaduanMasyarakat.findMany.data = res.data.data ?? [];
pengaduanMasyarakat.findMany.totalPages = res.data.totalPages ?? 1;
} else {
pengaduanMasyarakat.findMany.data = [];
pengaduanMasyarakat.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch pengaduan masyarakat paginated:", err);
pengaduanMasyarakat.findMany.data = [];
pengaduanMasyarakat.findMany.totalPages = 1;
} finally {
pengaduanMasyarakat.findMany.loading = false;
}
@@ -493,11 +527,11 @@ const pengaduanMasyarakat = proxy({
},
findUnique: {
data: null as Prisma.PengaduanMasyarakatGetPayload<{
include: {
jenisPengaduan: true;
image: true;
};
}> | null,
include: {
jenisPengaduan: true;
image: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(
@@ -507,7 +541,10 @@ const pengaduanMasyarakat = proxy({
const data = await res.json();
pengaduanMasyarakat.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch pengaduan masyarakat:", res.statusText);
console.error(
"Failed to fetch pengaduan masyarakat:",
res.statusText
);
pengaduanMasyarakat.findUnique.data = null;
}
} catch (error) {
@@ -542,7 +579,9 @@ const pengaduanMasyarakat = proxy({
);
await pengaduanMasyarakat.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus pengaduan masyarakat");
toast.error(
result?.message || "Gagal menghapus pengaduan masyarakat"
);
}
} catch (error) {
console.error("Gagal delete:", error);
@@ -567,7 +606,9 @@ const jenisPengaduan = proxy({
form: { ...defaultJenisPengaduanForm },
loading: false,
async create() {
const cek = templateJenisPengaduanForm.safeParse(jenisPengaduan.create.form);
const cek = templateJenisPengaduanForm.safeParse(
jenisPengaduan.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
@@ -598,13 +639,37 @@ const jenisPengaduan = proxy({
id: string;
nama: string;
}> | null,
async load() {
const res =
await ApiFetch.api.inovasi.layananonlinedesa.pengaduanmasyarakat.jenispengaduan[
"find-many"
].get();
if (res.status === 200) {
jenisPengaduan.findMany.data = res.data?.data ?? [];
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
jenisPengaduan.findMany.loading = true; // ✅ Akses langsung via nama path
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;
this.id = data.id;
this.form = {
nama: data.nama
nama: data.nama,
};
return data;
} else {
@@ -709,7 +774,9 @@ const jenisPengaduan = proxy({
},
async update() {
const cek = templateJenisPengaduanForm.safeParse(jenisPengaduan.edit.form);
const cek = templateJenisPengaduanForm.safeParse(
jenisPengaduan.edit.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
@@ -759,7 +826,9 @@ const jenisPengaduan = proxy({
await jenisPengaduan.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal mengupdate jenis pengaduan");
throw new Error(
result.message || "Gagal mengupdate jenis pengaduan"
);
}
} catch (error) {
// If JSON parsing fails, try to get the response text for better error messages
@@ -792,7 +861,6 @@ const jenisPengaduan = proxy({
},
});
const layananonlineDesa = proxy({
administrasiOnline,
jenisLayanan,

View File

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

View File

@@ -7,13 +7,13 @@ import { z } from "zod";
const templateForm = z.object({
nama: z.string().min(1, "Nama minimal 1 karakter"),
imageId: z.string().nonempty(),
icon: z.string().nonempty(),
kategoriId: z.array(z.string()).min(1, "Minimal pilih satu kategori"),
});
const defaultForm = {
nama: "",
imageId: "",
icon: "",
kategoriId: [] as string[],
};
@@ -54,7 +54,6 @@ const kontakDaruratKeamananState = proxy({
Prisma.KontakDaruratKeamananGetPayload<{
include: {
kategori: true;
image: true;
kontakItems: {
include: {
kontakItem: true;
@@ -102,14 +101,9 @@ const kontakDaruratKeamananState = proxy({
include: {
kontakItems: {
include: {
kontakItem: {
include: {
image: true;
}
};
kontakItem: true;
};
};
image: true;
kategori: true;
};
}> | null,
@@ -192,8 +186,9 @@ const kontakDaruratKeamananState = proxy({
this.id = data.id;
this.form = {
nama: data.nama,
imageId: data.imageId || '',
kategoriId: data.kontakItems?.map((item: any) => item.kontakItemId) || []
icon: data.icon || "",
kategoriId:
data.kontakItems?.map((item: any) => item.kontakItemId) || [],
};
return data;
} else {
@@ -230,7 +225,7 @@ const kontakDaruratKeamananState = proxy({
},
body: JSON.stringify({
nama: this.form.nama,
imageId: this.form.imageId,
icon: this.form.icon,
kategoriId: this.form.kategoriId,
}),
}
@@ -271,13 +266,13 @@ const kontakDaruratKeamananState = proxy({
const templateFormItem = 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(),
icon: z.string().nonempty(),
});
const defaultFormItem = {
nama: "",
nomorTelepon: "",
imageId: "",
icon: "",
};
const kontakDaruratItem = proxy({
@@ -285,9 +280,7 @@ const kontakDaruratItem = proxy({
form: { ...defaultFormItem },
loading: false,
async create() {
const cek = templateFormItem.safeParse(
kontakDaruratItem.create.form
);
const cek = templateFormItem.safeParse(kontakDaruratItem.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
@@ -296,9 +289,9 @@ const kontakDaruratItem = proxy({
}
try {
kontakDaruratItem.create.loading = true;
const res = await ApiFetch.api.keamanan.kontakitem[
"create"
].post(kontakDaruratItem.create.form);
const res = await ApiFetch.api.keamanan.kontakitem["create"].post(
kontakDaruratItem.create.form
);
if (res.status === 200) {
kontakDaruratItem.findMany.load();
return toast.success("success create");
@@ -315,8 +308,8 @@ const kontakDaruratItem = proxy({
findMany: {
data: null as Array<
Prisma.KontakItemGetPayload<{
include: {
image: true;
omit: {
isActive: true;
};
}>
> | null,
@@ -333,14 +326,13 @@ const kontakDaruratItem = proxy({
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.keamanan.kontakitem[
"find-many"
].get({ query });
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;
kontakDaruratItem.findMany.totalPages = res.data.totalPages ?? 1;
} else {
kontakDaruratItem.findMany.data = [];
kontakDaruratItem.findMany.totalPages = 1;
@@ -356,9 +348,8 @@ const kontakDaruratItem = proxy({
},
findUnique: {
data: null as Prisma.KontakItemGetPayload<{
include: {
kategori: true;
image: true;
omit: {
isActive: true;
};
}> | null,
loading: false,
@@ -384,15 +375,12 @@ const kontakDaruratItem = proxy({
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 response = await fetch(`/api/keamanan/kontakitem/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
@@ -422,15 +410,12 @@ const kontakDaruratItem = proxy({
}
try {
const response = await fetch(
`/api/keamanan/kontakitem/${id}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
}
);
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}`);
}
@@ -441,7 +426,7 @@ const kontakDaruratItem = proxy({
this.form = {
nama: data.nama,
nomorTelepon: data.nomorTelepon,
imageId: data.imageId,
icon: data.icon,
};
return data;
} else {
@@ -457,9 +442,7 @@ const kontakDaruratItem = proxy({
},
async update() {
const cek = templateFormItem.safeParse(
kontakDaruratItem.update.form
);
const cek = templateFormItem.safeParse(kontakDaruratItem.update.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
@@ -469,20 +452,17 @@ const kontakDaruratItem = proxy({
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,
imageId: this.form.imageId,
}),
}
);
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(
@@ -514,7 +494,7 @@ const kontakDaruratItem = proxy({
kontakDaruratItem.update.form = { ...defaultFormItem };
},
},
})
});
const kontakDarurat = proxy({
kontakDaruratKeamananState,

View File

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

View File

@@ -6,45 +6,17 @@ import { proxy } from "valtio";
import { z } from "zod";
const templateForm = z.object({
pencegahanKriminalitas: z.object({
programKeamanan: z.object({
nama: z.string().min(1, "Nama minimal 1 karakter"),
deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
slug: z.string().min(1, "Slug minimal 1 karakter"),
}),
tipsKeamanan: z.object({
judul: z.string().min(1, "Judul minimal 1 karakter"),
konten: z.string().min(1, "Konten minimal 1 karakter"),
slug: z.string().min(1, "Slug minimal 1 karakter"),
}),
videoKeamanan: z.object({
judul: z.string().min(1, "Judul minimal 1 karakter"),
deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
videoUrl: z.string().min(1, "Video URL minimal 1 karakter"),
slug: z.string().min(1, "Slug minimal 1 karakter"),
}),
}),
judul: z.string().min(1, "Judul minimal 1 karakter"),
deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
deskripsiSingkat: z.string().min(1, "Deskripsi singkat minimal 1 karakter"),
linkVideo: z.string().min(1, "Link video minimal 1 karakter"),
});
const defaultForm = {
pencegahanKriminalitas: {
programKeamanan: {
nama: "",
deskripsi: "",
slug: "",
},
tipsKeamanan: {
judul: "",
konten: "",
slug: "",
},
videoKeamanan: {
judul: "",
deskripsi: "",
videoUrl: "",
slug: "",
},
},
judul: "",
deskripsi: "",
deskripsiSingkat: "",
linkVideo: "",
};
const pencegahanKriminalitasState = proxy({
@@ -65,7 +37,7 @@ const pencegahanKriminalitasState = proxy({
pencegahanKriminalitasState.create.loading = true;
const res = await ApiFetch.api.keamanan.pencegahankriminalitas[
"create"
].post(pencegahanKriminalitasState.create.form.pencegahanKriminalitas);
].post(pencegahanKriminalitasState.create.form);
if (res.status === 200) {
pencegahanKriminalitasState.findMany.load();
return toast.success("success create");
@@ -82,11 +54,7 @@ const pencegahanKriminalitasState = proxy({
findMany: {
data: null as
| Prisma.PencegahanKriminalitasGetPayload<{
include: {
programKeamanan: true;
tipsKeamanan: true;
videoKeamanan: true;
};
omit: { isActive: true };
}>[]
| null,
page: 1,
@@ -125,11 +93,7 @@ const pencegahanKriminalitasState = proxy({
},
findUnique: {
data: null as Prisma.PencegahanKriminalitasGetPayload<{
include: {
programKeamanan: true;
tipsKeamanan: true;
videoKeamanan: true;
};
omit: { isActive: true };
}> | null,
loading: false,
async load(id: string) {
@@ -148,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: {
loading: false,
async byId(id: string) {
@@ -213,24 +201,10 @@ const pencegahanKriminalitasState = proxy({
const data = result.data;
pencegahanKriminalitasState.update.id = data.id;
pencegahanKriminalitasState.update.form = {
pencegahanKriminalitas: {
programKeamanan: {
nama: data.programKeamanan.nama,
deskripsi: data.programKeamanan.deskripsi,
slug: data.programKeamanan.slug,
},
tipsKeamanan: {
judul: data.tipsKeamanan.judul,
konten: data.tipsKeamanan.konten,
slug: data.tipsKeamanan.slug,
},
videoKeamanan: {
judul: data.videoKeamanan.judul,
deskripsi: data.videoKeamanan.deskripsi,
videoUrl: data.videoKeamanan.videoUrl,
slug: data.videoKeamanan.slug,
},
},
judul: data.judul,
deskripsi: data.deskripsi,
deskripsiSingkat: data.deskripsiSingkat,
linkVideo: data.linkVideo,
};
return data;
} else {
@@ -266,40 +240,11 @@ const pencegahanKriminalitasState = proxy({
"Content-Type": "application/json",
},
body: JSON.stringify({
pencegahanKriminalitas: {
programKeamanan: {
nama: pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.programKeamanan.nama,
deskripsi:
pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.programKeamanan.deskripsi,
slug: pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.programKeamanan.slug,
},
tipsKeamanan: {
judul:
pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.tipsKeamanan.judul,
konten:
pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.tipsKeamanan.konten,
slug: pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.tipsKeamanan.slug,
},
videoKeamanan: {
judul:
pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.videoKeamanan.judul,
deskripsi:
pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.videoKeamanan.deskripsi,
videoUrl:
pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.videoKeamanan.videoUrl,
slug: pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.videoKeamanan.slug,
},
},
judul: pencegahanKriminalitasState.update.form.judul,
deskripsi: pencegahanKriminalitasState.update.form.deskripsi,
deskripsiSingkat:
pencegahanKriminalitasState.update.form.deskripsiSingkat,
linkVideo: pencegahanKriminalitasState.update.form.linkVideo,
}),
}
);

View File

@@ -31,11 +31,13 @@ const templateForm = z.object({
doctorSign: z.object({
content: z.string().min(1, "Content harus diisi"),
}),
imageId: z.string().min(1, "Image ID harus diisi"),
});
const defaultForm = {
title: "",
content: "",
imageId: "",
introduction: {
content: "",
},
@@ -59,6 +61,7 @@ const defaultForm = {
doctorSign: {
content: "",
},
};
const artikelKesehatanState = proxy({
@@ -112,6 +115,7 @@ const artikelKesehatanState = proxy({
firstaid: true;
mythvsfact: true;
doctorsign: true;
image: true;
};
}>[]
| null,
@@ -159,6 +163,7 @@ const artikelKesehatanState = proxy({
firstaid: true;
mythvsfact: true;
doctorsign: true;
image: true;
};
}> | null,
loading: false,
@@ -213,6 +218,7 @@ const artikelKesehatanState = proxy({
doctorSign: {
content: data.doctorsign.content,
},
imageId: data.imageId,
};
},
async submit() {
@@ -253,6 +259,7 @@ const artikelKesehatanState = proxy({
doctorSign: {
content: artikelKesehatanState.edit.form.doctorSign.content,
},
imageId: artikelKesehatanState.edit.form.imageId,
};
const res = await fetch(

View File

@@ -26,14 +26,6 @@ const templateForm = z.object({
dokumenJadwalKegiatan: z.object({
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 = {
@@ -55,15 +47,7 @@ const defaultForm = {
},
dokumenJadwalKegiatan: {
content: "",
},
pendaftaranJadwalKegiatan: {
name: "",
tanggal: "",
namaOrangtua: "",
nomor: "",
alamat: "",
catatan: "",
},
}
};
const jadwalkegiatanState = proxy({
@@ -116,7 +100,6 @@ const jadwalkegiatanState = proxy({
deskripsijadwalkegiatan: true;
layananjadwalkegiatan: true;
dokumenjadwalkegiatan: true;
pendaftaranjadwalkegiatan: true;
};
}>[]
| null,
@@ -161,7 +144,6 @@ const jadwalkegiatanState = proxy({
layananjadwalkegiatan: true;
syaratketentuanjadwalkegiatan: true;
dokumenjadwalkegiatan: true;
pendaftaranjadwalkegiatan: true;
};
}> | null,
loading: false,
@@ -209,15 +191,7 @@ const jadwalkegiatanState = proxy({
},
dokumenJadwalKegiatan: {
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() {
@@ -259,20 +233,6 @@ const jadwalkegiatanState = proxy({
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,
},
};
const res = await fetch(

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

@@ -23,7 +23,12 @@ type ProgramInovasiForm = Prisma.ProgramInovasiGetPayload<{
const programInovasi = proxy({
create: {
form: {} as ProgramInovasiForm,
form: {
name: "",
description: "",
imageId: "",
link: ""
} as ProgramInovasiForm,
loading: false,
async create() {
// Ensure all required fields are non-null

View File

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

View File

@@ -354,14 +354,39 @@ const kategoriKegiatan = proxy({
id: string;
nama: string;
}> | null,
async load() {
const res = await ApiFetch.api.lingkungan.kategorikegiatan[
"find-many"
].get();
if (res.status === 200) {
kategoriKegiatan.findMany.data = res.data?.data ?? [];
}
},
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
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: {
data: null as Prisma.KategoriKegiatanGetPayload<{

View File

@@ -352,17 +352,19 @@ const posisiOrganisasi = proxy({
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
posisiOrganisasi.findMany.loading = true; // ✅ Akses langsung via nama path
load: async (page = 1, limit?: number, search = "") => {
const appliedLimit = limit ?? 10;
posisiOrganisasi.findMany.page = page;
posisiOrganisasi.findMany.search = search;
try {
const query: any = { page, limit };
const query: any = { page, limit: appliedLimit };
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) {
posisiOrganisasi.findMany.data = res.data.data ?? [];
posisiOrganisasi.findMany.totalPages = res.data.totalPages ?? 1;

View File

@@ -51,7 +51,7 @@ function Login() {
Login
</Title>
<Center>
<Image src={"/darmasaba-icon.png"} alt="" w={80} />
<Image loading="lazy" src={"/darmasaba-icon.png"} alt="" w={80} />
</Center>
</Box>
<Box>

View File

@@ -63,7 +63,7 @@ function Registrasi() {
Registrasi
</Title>
<Center>
<Image src={"/darmasaba-icon.png"} alt="" w={80} />
<Image loading="lazy" src={"/darmasaba-icon.png"} alt="" w={80} />
</Center>
<Box>
<TextInput placeholder='Username'

View File

@@ -1,10 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
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 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 }) {
const router = useRouter()
@@ -37,6 +37,13 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
href: "/admin/desa/layanan/pelayanan_penduduk_non_permanent",
icon: <IconUsers size={18} stroke={1.8} />,
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">
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Layanan</Title>
<Tabs
color={colors['blue-button']}
variant='pills'
color={colors["blue-button"]}
variant="pills"
value={activeTab}
onChange={handleTabChange}
radius="lg"
keepMounted={false}
>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
}}
>
{tabs.map((tab, i) => (
<Tooltip
key={i}
label={tab.tooltip}
position="bottom"
withArrow
transitionProps={{ transition: 'pop', duration: 200 }}
>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
{/* ✅ Scroll horizontal wrapper */}
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
}}
>
{tabs.map((tab, i) => (
<Tooltip
key={i}
label={tab.tooltip}
position="bottom"
withArrow
transitionProps={{ transition: 'pop', duration: 200 }}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
</ScrollArea>
{tabs.map((tab, i) => (
<TabsPanel

View File

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

View File

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

View File

@@ -7,10 +7,9 @@ import {
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
Tooltip
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
@@ -62,9 +61,9 @@ function CreateKategoriBerita() {
>
<Stack gap="md">
<TextInput
label={<Text fw="bold" fz="sm">Nama Kategori Berita</Text>}
label="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)}
required
/>

View File

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

View File

@@ -80,7 +80,7 @@ function DetailBerita() {
<Box>
<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" }}>{data.deskripsi || '-'}</Text>
</Box>
<Box>
@@ -93,6 +93,7 @@ function DetailBerita() {
h={200}
radius="md"
fit="cover"
loading='lazy'
/>
) : (
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>

View File

@@ -100,7 +100,7 @@ export default function CreateBerita() {
<TextInput
label="Judul"
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)}
required
/>
@@ -112,7 +112,7 @@ export default function CreateBerita() {
label: item.name,
value: item.id,
}))}
value={beritaState.berita.create.form.kategoriBeritaId || null}
defaultValue={beritaState.berita.create.form.kategoriBeritaId || null}
onChange={(val: string | null) => {
if (val) {
const selected = beritaState.kategoriBerita.findMany.data?.find(
@@ -133,8 +133,9 @@ export default function CreateBerita() {
<TextInput
label="Deskripsi Singkat"
required
placeholder="Masukkan deskripsi berita"
value={beritaState.berita.create.form.deskripsi}
defaultValue={beritaState.berita.create.form.deskripsi}
onChange={(e) => (beritaState.berita.create.form.deskripsi = e.target.value)}
/>
@@ -183,6 +184,7 @@ export default function CreateBerita() {
objectFit: 'contain',
border: '1px solid #ddd',
}}
loading="lazy"
/>
</Box>
)}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,178 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors';
import {
Box,
Button,
Group,
Paper,
Select,
Stack,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditAjukanPermohonan() {
const router = useRouter();
const params = useParams();
const stateAjukan = useProxy(stateLayananDesa.ajukanPermohonan);
// State lokal form
const [formData, setFormData] = useState({
nama: '',
nik: '',
alamat: '',
nomorKk: '',
kategoriId: '',
});
// Load data awal
useEffect(() => {
stateLayananDesa.suratKeterangan.findManyAll.load();
const loadAjukan = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await stateAjukan.edit.load(id);
if (data) {
setFormData({
nama: data.nama || '',
nik: data.nik || '',
alamat: data.alamat || '',
nomorKk: data.nomorKk || '',
kategoriId: data.kategoriId || '',
});
}
} catch (error) {
console.error('Error loading ajukan:', error);
toast.error('Gagal memuat data ajukan');
}
};
loadAjukan();
}, [params?.id]);
// Handler untuk input controlled
const handleChange = (field: string, value: string) => {
setFormData((prev) => ({
...prev,
[field]: value,
}));
};
const handleSubmit = async () => {
try {
stateAjukan.edit.form = {
...stateAjukan.edit.form,
...formData,
};
toast.success('Ajukan berhasil diperbarui!');
router.push('/admin/desa/layanan/ajukan_permohonan');
} catch (error) {
console.error('Error updating ajukan:', error);
toast.error('Terjadi kesalahan saat memperbarui ajukan');
}
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Back Button */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Ajukan Permohonan
</Title>
</Group>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
label="Nama"
placeholder="Masukkan nama"
value={formData.nama}
onChange={(e) => handleChange('nama', e.target.value)}
required
/>
<TextInput
type="number"
label="NIK"
placeholder="Masukkan NIK"
value={formData.nik}
onChange={(e) => handleChange('nik', e.target.value)}
required
/>
<TextInput
label="Alamat"
placeholder="Masukkan alamat"
value={formData.alamat}
onChange={(e) => handleChange('alamat', e.target.value)}
required
/>
<TextInput
type="number"
label="Nomor KK"
placeholder="Masukkan nomor KK"
value={formData.nomorKk}
onChange={(e) => handleChange('nomorKk', e.target.value)}
required
/>
<Select
label="Kategori"
placeholder="Pilih kategori"
data={stateLayananDesa.suratKeterangan.findManyAll.data?.map((item) => ({
label: item.name,
value: item.id,
}))}
value={formData.kategoriId || null}
onChange={(val) => handleChange('kategoriId', val || '')}
searchable
clearable
nothingFoundMessage="Tidak ditemukan"
required
/>
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditAjukanPermohonan;

View File

@@ -0,0 +1,172 @@
'use client'
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors';
import {
Box,
Button,
Group,
Paper,
Skeleton,
Stack,
Text,
Tooltip
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
function DetailAjukanPermohonan() {
const ajukanPermohonanState = useProxy(stateLayananDesa.ajukanPermohonan);
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const params = useParams();
const router = useRouter();
useShallowEffect(() => {
ajukanPermohonanState.findUnique.load(params?.id as string);
}, []);
const handleHapus = () => {
if (selectedId) {
ajukanPermohonanState.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
router.push('/admin/desa/layanan/ajukan_permohonan');
}
};
if (!ajukanPermohonanState.findUnique.data) {
return (
<Stack py={10}>
<Skeleton height={500} radius="md" />
</Stack>
);
}
const data = ajukanPermohonanState.findUnique.data;
return (
<Box py={10}>
{/* Tombol Kembali */}
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
mb={15}
>
Kembali
</Button>
<Paper
withBorder
w={{ base: '100%', md: '60%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Surat Keterangan
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz="lg" fw="bold">
Nama
</Text>
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }}>
{data?.nama || '-'}
</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">
NIK
</Text>
<Text fz="md" c="dimmed">
{data?.nik || '-'}
</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">
Alamat
</Text>
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }}>
{data?.alamat || '-'}
</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">
Nomor KK
</Text>
<Text fz="md" c="dimmed">
{data?.nomorKk || '-'}
</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">
Kategori
</Text>
<Text fz="md" c="dimmed">
{data?.kategori.name || '-'}
</Text>
</Box>
<Group gap="sm">
<Tooltip label="Hapus Surat" withArrow position="top">
<Button
color="red"
onClick={() => {
setSelectedId(data.id);
setModalHapus(true);
}}
variant="light"
radius="md"
size="md"
disabled={ajukanPermohonanState.delete.loading}
>
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Surat" withArrow position="top">
<Button
color="green"
onClick={() =>
router.push(
`/admin/desa/layanan/ajukan_permohonan/${data.id}/edit`
)
}
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Tooltip>
</Group>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah Anda yakin ingin menghapus ajukan permohonan ini?"
/>
</Box>
);
}
export default DetailAjukanPermohonan;

View File

@@ -0,0 +1,155 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import {
Box,
Button,
Center,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title
} from '@mantine/core';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import stateLayananDesa from '../../../_state/desa/layananDesa';
function AjukanPermohonan() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Pelayanan Ajukan Permohonan'
placeholder='Cari nama atau deskripsi...'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListAjukanPermohonan search={search} />
</Box>
);
}
function ListAjukanPermohonan({ search }: { search: string }) {
const AjukanPermohonanState = useProxy(stateLayananDesa.ajukanPermohonan);
const router = useRouter();
const {
data,
page,
totalPages,
loading,
load,
} = AjukanPermohonanState.findMany;
useEffect(() => {
load(page, 10, search);
}, [page, search]);
// Loading state
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={600} radius="md" />
</Stack>
);
}
return (
<Box py={10}>
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
<Title order={4}>List Ajukan Permohonan</Title>
<Box style={{ overflowX: "auto" }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '30%' }}>Nama</TableTh>
<TableTh style={{ width: '45%' }}>Alamat</TableTh>
<TableTh style={{ width: '15%' }}>NIK</TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{data.length > 0 ? (
data.map((item) => (
<TableTr key={item.id}>
<TableTd style={{ width: '30%' }}>
<Box w={200}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.nama}
</Text>
</Box>
</TableTd>
<TableTd style={{ width: '45%' }}>
<Box w={200}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.alamat}
</Text>
</Box>
</TableTd>
<TableTd style={{ width: '45%' }}>
<Box w={200}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.nik}
</Text>
</Box>
</TableTd>
<TableTd style={{ width: '15%' }}>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() =>
router.push(`/admin/desa/layanan/ajukan_permohonan/${item.id}`)
}
>
Detail
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={3}>
<Center py={20}>
<Text color="dimmed">Tidak ada data ajukan permohonan yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10, search);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
</Box>
);
}
export default AjukanPermohonan;

View File

@@ -1,9 +1,20 @@
'use client'
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
@@ -12,17 +23,22 @@ import { useProxy } from 'valtio/utils';
function EditPelayananPendudukNonPermanent() {
const router = useRouter();
const params = useParams()
const statePendudukNonPermanent = useProxy(stateLayananDesa.pelayananPendudukNonPermanen)
const [formData, setFormData] = useState({
name: statePendudukNonPermanent.findById.data?.name || '',
deskripsi: statePendudukNonPermanent.findById.data?.deskripsi || '',
})
const params = useParams();
const statePendudukNonPermanent = useProxy(
stateLayananDesa.pelayananPendudukNonPermanen
);
const [formData, setFormData] = useState({
name: '',
deskripsi: '',
});
// Load data sekali dari backend
useEffect(() => {
const loadPelayananPerizinan = async () => {
const loadData = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await statePendudukNonPermanent.update.load(id);
if (data) {
@@ -32,27 +48,48 @@ function EditPelayananPendudukNonPermanent() {
});
}
} catch (error) {
console.error("Error loading pelayanan perizinan berusaha:", error);
toast.error("Gagal memuat data pelayanan perizinan berusaha");
console.error('Error loading data:', error);
toast.error('Gagal memuat data pelayanan penduduk non permanent');
}
};
loadPelayananPerizinan();
loadData();
}, [params?.id]);
const handleChange =
(field: keyof typeof formData) =>
(value: string) => {
setFormData((prev) => ({
...prev,
[field]: value,
}));
};
const handleSubmit = async () => {
if (statePendudukNonPermanent.findById.data) {
statePendudukNonPermanent.findById.data.name = formData.name;
statePendudukNonPermanent.findById.data.deskripsi = formData.deskripsi;
statePendudukNonPermanent.update.update(statePendudukNonPermanent.findById.data)
}
router.push('/admin/desa/layanan/pelayanan_penduduk_non_permanent')
}
if (!statePendudukNonPermanent.findById.data) return;
// Update global state hanya di submit
const updated = {
...statePendudukNonPermanent.findById.data,
name: formData.name,
deskripsi: formData.deskripsi,
};
await statePendudukNonPermanent.update.update(updated);
router.push('/admin/desa/layanan/pelayanan_penduduk_non_permanent');
};
return (
<Box>
<Stack gap="xs">
<Group mb="md">
<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} />
</Button>
</Tooltip>
@@ -62,7 +99,7 @@ function EditPelayananPendudukNonPermanent() {
</Group>
<Paper
w={{ base: "100%", md: "50%" }}
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="md"
radius="md"
@@ -77,22 +114,18 @@ function EditPelayananPendudukNonPermanent() {
label="Judul"
placeholder="Masukkan judul"
value={formData.name}
onChange={(e) =>
setFormData({ ...formData, name: e.target.value })
}
onChange={(e) => handleChange('name')(e.target.value)}
required
/>
{/* Posisi Field */}
{/* Deskripsi Field */}
<Box>
<Text fz="sm" fw="bold">
Deskripsi
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }));
}}
onChange={handleChange('deskripsi')}
/>
</Box>
@@ -104,7 +137,9 @@ function EditPelayananPendudukNonPermanent() {
loading={statePendudukNonPermanent.update.loading}
disabled={!formData.name}
>
{statePendudukNonPermanent.update.loading ? 'Menyimpan...' : 'Simpan Perubahan'}
{statePendudukNonPermanent.update.loading
? 'Menyimpan...'
: 'Simpan Perubahan'}
</Button>
<Button

View File

@@ -91,6 +91,7 @@ function PelayananPendudukNonPermanent() {
ta="justify"
fz={{ base: '1rem', md: '1.2rem' }}
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
style={{wordBreak: "break-word", whiteSpace: "normal"}}
/>
</Box>
</Box>

View File

@@ -1,9 +1,18 @@
'use client'
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, TextInput, Title, Tooltip } from '@mantine/core';
import {
Box,
Button,
Group,
Paper,
Stack,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
@@ -12,15 +21,18 @@ import { useProxy } from 'valtio/utils';
function EditPelayananPerizinanBerusaha() {
const router = useRouter();
const params = useParams()
const statePerizinanBerusaha = useProxy(stateLayananDesa.pelayananPerizinanBerusaha)
const params = useParams();
const statePerizinanBerusaha = useProxy(
stateLayananDesa.pelayananPerizinanBerusaha
);
const [formData, setFormData] = useState({
name: statePerizinanBerusaha.findById.data?.name || '',
deskripsi: statePerizinanBerusaha.findById.data?.deskripsi || '',
link: statePerizinanBerusaha.findById.data?.link || '',
})
name: '',
deskripsi: '',
link: '',
});
// load data pertama kali
useEffect(() => {
const loadPelayananPerizinan = async () => {
const id = params?.id as string;
@@ -35,22 +47,35 @@ function EditPelayananPerizinanBerusaha() {
});
}
} catch (error) {
console.error("Error loading pelayanan perizinan berusaha:", error);
toast.error("Gagal memuat data pelayanan perizinan berusaha");
console.error('Error loading pelayanan perizinan berusaha:', error);
toast.error('Gagal memuat data pelayanan perizinan berusaha');
}
};
loadPelayananPerizinan();
}, [params?.id]);
const handleChange =
(field: keyof typeof formData) =>
(value: string) => {
setFormData((prev) => ({
...prev,
[field]: value,
}));
};
const handleSubmit = async () => {
const { name, deskripsi, link } = formData;
if (statePerizinanBerusaha.findById.data) {
statePerizinanBerusaha.findById.data.name = formData.name;
statePerizinanBerusaha.findById.data.deskripsi = formData.deskripsi;
statePerizinanBerusaha.findById.data.link = formData.link;
statePerizinanBerusaha.update.update(statePerizinanBerusaha.findById.data)
const updatedData = {
...statePerizinanBerusaha.findById.data,
name,
deskripsi,
link,
};
await statePerizinanBerusaha.update.update(updatedData);
router.push('/admin/desa/layanan/pelayanan_perizinan_berusaha');
}
router.push('/admin/desa/layanan/pelayanan_perizinan_berusaha')
}
};
return (
<Box>
@@ -58,7 +83,12 @@ function EditPelayananPerizinanBerusaha() {
{/* Header Section */}
<Group mb="md">
<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} />
</Button>
</Tooltip>
@@ -69,7 +99,7 @@ function EditPelayananPerizinanBerusaha() {
{/* Form Section */}
<Paper
w={{ base: "100%", md: "50%" }}
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="md"
radius="md"
@@ -84,7 +114,7 @@ function EditPelayananPerizinanBerusaha() {
label="Judul"
placeholder="Masukkan judul"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
onChange={(e) => handleChange('name')(e.target.value)}
required
/>
@@ -93,7 +123,7 @@ function EditPelayananPerizinanBerusaha() {
label="Link"
placeholder="Masukkan link terkait"
value={formData.link}
onChange={(e) => setFormData({ ...formData, link: e.target.value })}
onChange={(e) => handleChange('link')(e.target.value)}
/>
{/* Deskripsi Field */}
@@ -101,7 +131,7 @@ function EditPelayananPerizinanBerusaha() {
<Title order={6}>Deskripsi</Title>
<EditEditor
value={formData.deskripsi}
onChange={(val) => setFormData({ ...formData, deskripsi: val })}
onChange={handleChange('deskripsi')}
/>
</Box>
@@ -113,7 +143,9 @@ function EditPelayananPerizinanBerusaha() {
loading={statePerizinanBerusaha.update.loading}
disabled={!formData.name}
>
{statePerizinanBerusaha.update.loading ? 'Menyimpan...' : 'Simpan Perubahan'}
{statePerizinanBerusaha.update.loading
? 'Menyimpan...'
: 'Simpan Perubahan'}
</Button>
<Button

View File

@@ -101,6 +101,7 @@ function PerizinanBerusaha() {
ta="justify"
fz={{ base: '1rem', md: '1.2rem' }}
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
style={{wordBreak: "break-word", whiteSpace: "normal"}}
/>
<Text

View File

@@ -1,5 +1,4 @@
'use client'
/* eslint-disable react-hooks/exhaustive-deps */
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors';
@@ -19,7 +18,7 @@ import {
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useEffect, useState, useCallback } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
@@ -28,17 +27,23 @@ function EditSuratKeterangan() {
const params = useParams();
const stateSurat = useProxy(stateLayananDesa.suratKeterangan);
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [previewImage2, setPreviewImage2] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [file2, setFile2] = useState<File | null>(null);
// state lokal untuk form
const [formData, setFormData] = useState({
name: stateSurat.edit.form.name,
deskripsi: stateSurat.edit.form.deskripsi,
imageId: stateSurat.edit.form.imageId,
image2Id: stateSurat.edit.form.image2Id,
name: '',
deskripsi: '',
imageId: '',
image2Id: '',
});
// state file upload
const [file, setFile] = useState<File | null>(null);
const [file2, setFile2] = useState<File | null>(null);
// state preview gambar
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [previewImage2, setPreviewImage2] = useState<string | null>(null);
// load data awal
useEffect(() => {
const loadSurat = async () => {
const id = params?.id as string;
@@ -64,15 +69,15 @@ function EditSuratKeterangan() {
};
loadSurat();
}, [params?.id]);
}, [params?.id, stateSurat.edit]);
const handleSubmit = async () => {
// handler untuk submit
const handleSubmit = useCallback(async () => {
try {
stateSurat.edit.form = {
...stateSurat.edit.form,
...formData,
};
// update form global hanya saat submit
stateSurat.edit.form = { ...stateSurat.edit.form, ...formData };
// upload file 1
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = res.data?.data;
@@ -80,6 +85,7 @@ function EditSuratKeterangan() {
stateSurat.edit.form.imageId = uploaded.id;
}
// upload file 2
if (file2) {
const res = await ApiFetch.api.fileStorage.create.post({ file: file2, name: file2.name });
const uploaded = res.data?.data;
@@ -94,7 +100,7 @@ function EditSuratKeterangan() {
console.error('Error updating surat:', error);
toast.error('Terjadi kesalahan saat memperbarui surat');
}
};
}, [formData, file, file2, router, stateSurat.edit]);
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
@@ -119,28 +125,32 @@ function EditSuratKeterangan() {
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Input nama */}
<TextInput
label="Nama Surat Keterangan"
placeholder="Masukkan nama surat keterangan"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
onChange={(e) => setFormData((prev) => ({ ...prev, name: e.target.value }))}
required
/>
{/* Input deskripsi */}
<Box>
<Text fz="sm" fw="bold" mb={6}>
Konten
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => setFormData({ ...formData, deskripsi: htmlContent })}
onChange={(htmlContent) =>
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }))
}
/>
</Box>
{/* Upload Gambar 1 */}
<Box>
<Text fw="bold" fz="sm" mb={6}>
Gambar Konten Pelayanan
Gambar Konten Pelayanan
</Text>
<Dropzone
onDrop={(files) => {
@@ -188,6 +198,7 @@ function EditSuratKeterangan() {
objectFit: 'contain',
border: `1px solid ${colors['blue-button']}`,
}}
loading="lazy"
/>
</Box>
)}
@@ -244,6 +255,7 @@ function EditSuratKeterangan() {
objectFit: 'contain',
border: `1px solid ${colors['blue-button']}`,
}}
loading="lazy"
/>
</Box>
)}

View File

@@ -95,6 +95,7 @@ function DetailSuratKeterangan() {
dangerouslySetInnerHTML={{
__html: data?.deskripsi || '-',
}}
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
/>
</Box>
@@ -110,6 +111,7 @@ function DetailSuratKeterangan() {
h={200}
radius="md"
fit="cover"
loading="lazy"
/>
) : (
<Text fz="sm" c="dimmed">
@@ -130,6 +132,7 @@ function DetailSuratKeterangan() {
h={200}
radius="md"
fit="cover"
loading="lazy"
/>
) : (
<Text fz="sm" c="dimmed">

View File

@@ -106,9 +106,9 @@ function CreateSuratKeterangan() {
<Stack gap="md">
{/* Nama Surat */}
<TextInput
value={stateSurat.create.form.name}
defaultValue={stateSurat.create.form.name}
onChange={(val) => (stateSurat.create.form.name = val.target.value)}
label={<Text fz="sm" fw="bold">Nama Surat Keterangan</Text>}
label="Nama Surat Keterangan"
placeholder="Masukkan nama surat keterangan"
required
/>
@@ -170,6 +170,7 @@ function CreateSuratKeterangan() {
alt="Preview Gambar Utama"
radius="md"
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
loading="lazy"
/>
</Box>
)}
@@ -219,6 +220,7 @@ function CreateSuratKeterangan() {
alt="Preview Gambar Tambahan"
radius="md"
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
loading="lazy"
/>
</Box>
) : (

View File

@@ -20,7 +20,7 @@ import {
Title,
Tooltip
} from '@mantine/core';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useMemo, useState } from 'react';
import { useProxy } from 'valtio/utils';
@@ -119,19 +119,22 @@ function ListSuratKeterangan({ search }: { search: string }) {
<Box w={200}>
<Text truncate="end" lineClamp={1} fz="sm" c="dimmed"
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
style={{wordBreak: "break-word", whiteSpace: "normal"}}
/>
</Box>
</TableTd>
<TableTd style={{ width: '15%' }}>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() =>
router.push(`/admin/desa/layanan/pelayanan_surat_keterangan/${item.id}`)
}
>
<IconDeviceImac size={20} />
<Text ml={5}>Detail</Text>
Detail
</Button>
</TableTd>
</TableTr>

View File

@@ -15,7 +15,7 @@ import {
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useEffect, useState, useCallback } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
@@ -26,22 +26,24 @@ function EditPelayananTelunjukSakti() {
const params = useParams();
const [formData, setFormData] = useState({
name: stateTelunjukDesa.edit.form.name,
deskripsi: stateTelunjukDesa.edit.form.deskripsi,
link: stateTelunjukDesa.edit.form.link,
name: '',
deskripsi: '',
link: '',
});
// Load data awal hanya sekali (pas ada id)
useEffect(() => {
const loadPelayananTelunjukSakti = async () => {
const loadData = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await stateTelunjukDesa.edit.load(id);
if (data) {
setFormData({
name: data.name || '',
deskripsi: data.deskripsi || '',
link: data.link || '',
name: data.name ?? '',
deskripsi: data.deskripsi ?? '',
link: data.link ?? '',
});
}
} catch (error) {
@@ -49,9 +51,19 @@ function EditPelayananTelunjukSakti() {
toast.error('Gagal memuat data pelayanan telunjuk sakti');
}
};
loadPelayananTelunjukSakti();
loadData();
}, [params?.id]);
// Handler input controlled
const handleChange = useCallback(
(field: keyof typeof formData, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
},
[]
);
// Submit: update global state hanya saat simpan
const handleSubmit = async () => {
try {
stateTelunjukDesa.edit.form = {
@@ -95,7 +107,7 @@ function EditPelayananTelunjukSakti() {
label="Nama Pelayanan"
placeholder="Masukkan nama pelayanan"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
onChange={(e) => handleChange('name', e.target.value)}
required
/>
@@ -106,7 +118,7 @@ function EditPelayananTelunjukSakti() {
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => setFormData({ ...formData, deskripsi: htmlContent })}
onChange={(htmlContent) => handleChange('deskripsi', htmlContent)}
/>
</Box>
@@ -115,7 +127,7 @@ function EditPelayananTelunjukSakti() {
label="Link"
placeholder="Masukkan link terkait"
value={formData.link}
onChange={(e) => setFormData({ ...formData, link: e.target.value })}
onChange={(e) => handleChange('link', e.target.value)}
/>
{/* Tombol Simpan */}

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