Compare commits

...

14 Commits

545 changed files with 29975 additions and 16971 deletions

2
.gitignore vendored
View File

@@ -48,3 +48,5 @@ next-env.d.ts
.env.* .env.*
*.tar.gz

BIN
bun.lockb

Binary file not shown.

View File

@@ -3,11 +3,9 @@
"version": "0.1.5", "version": "0.1.5",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev --turbopack", "dev": "bun --bun next dev",
"build": "next build", "build": "bun --bun next build",
"start": "next start", "start": "bun --bun next start"
"lint": "next lint",
"prisma:seed": "bun run prisma/seed.ts"
}, },
"prisma": { "prisma": {
"seed": "bun run prisma/seed.ts" "seed": "bun run prisma/seed.ts"
@@ -75,15 +73,18 @@
"prisma": "^6.3.1", "prisma": "^6.3.1",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-international-phone": "^4.6.0",
"react-leaflet": "^5.0.0", "react-leaflet": "^5.0.0",
"react-simple-toasts": "^6.1.0", "react-simple-toasts": "^6.1.0",
"react-toastify": "^11.0.5", "react-toastify": "^11.0.5",
"react-transition-group": "^4.4.5", "react-transition-group": "^4.4.5",
"readdirp": "^4.1.1", "readdirp": "^4.1.1",
"recharts": "^2.15.3", "recharts": "^2.15.3",
"sharp": "^0.34.3",
"swr": "^2.3.2", "swr": "^2.3.2",
"uuid": "^11.1.0", "uuid": "^11.1.0",
"valtio": "^2.1.3", "valtio": "^2.1.3",
"zlib": "^1.0.5",
"zod": "^3.24.3" "zod": "^3.24.3"
}, },
"devDependencies": { "devDependencies": {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -81,8 +81,6 @@ model FileStorage {
PelayananSuratKeteranganImage PelayananSuratKeterangan[] @relation("PelayananSuratKeteranganImage") PelayananSuratKeteranganImage PelayananSuratKeterangan[] @relation("PelayananSuratKeteranganImage")
PelayananSuratKeteranganImage2 PelayananSuratKeterangan[] @relation("PelayananSuratKeteranganImage2") PelayananSuratKeteranganImage2 PelayananSuratKeterangan[] @relation("PelayananSuratKeteranganImage2")
PasarDesa PasarDesa[] PasarDesa PasarDesa[]
KontakDaruratKeamanan KontakDaruratKeamanan[]
KontakItem KontakItem[]
Pegawai Pegawai[] Pegawai Pegawai[]
DesaDigital DesaDigital[] DesaDigital DesaDigital[]
InfoTekno InfoTekno[] InfoTekno InfoTekno[]
@@ -92,7 +90,7 @@ model FileStorage {
PejabatDesa PejabatDesa[] PejabatDesa PejabatDesa[]
MediaSosial MediaSosial[] MediaSosial MediaSosial[]
DesaAntiKorupsi DesaAntiKorupsi[] DesaAntiKorupsi DesaAntiKorupsi[]
SDGSDesa SDGSDesa[] SDGSDesa SdgsDesa[]
APBDesImage APBDes[] @relation("APBDesImage") APBDesImage APBDes[] @relation("APBDesImage")
APBDesFile APBDes[] @relation("APBDesFile") APBDesFile APBDes[] @relation("APBDesFile")
PrestasiDesa PrestasiDesa[] PrestasiDesa PrestasiDesa[]
@@ -101,6 +99,8 @@ model FileStorage {
PerbekelDariMasaKeMasa PerbekelDariMasaKeMasa[] PerbekelDariMasaKeMasa PerbekelDariMasaKeMasa[]
MitraKolaborasi MitraKolaborasi[] MitraKolaborasi MitraKolaborasi[]
ArtikelKesehatan ArtikelKesehatan[]
} }
//========================================= MENU LANDING PAGE ========================================= // //========================================= MENU LANDING PAGE ========================================= //
@@ -168,9 +168,9 @@ model KategoriDesaAntiKorupsi {
} }
//========================================= SDGS Desa ========================================= // //========================================= SDGS Desa ========================================= //
model SDGSDesa { model SdgsDesa {
id String @id @default(cuid()) id String @id @default(cuid())
name String @unique name String
jumlah String jumlah String
image FileStorage? @relation(fields: [imageId], references: [id]) image FileStorage? @relation(fields: [imageId], references: [id])
imageId String? imageId String?
@@ -183,7 +183,7 @@ model SDGSDesa {
//========================================= APBDes ========================================= // //========================================= APBDes ========================================= //
model APBDes { model APBDes {
id String @id @default(cuid()) id String @id @default(cuid())
name String @unique name String
jumlah String jumlah String
image FileStorage? @relation("APBDesImage", fields: [imageId], references: [id]) image FileStorage? @relation("APBDesImage", fields: [imageId], references: [id])
imageId String? imageId String?
@@ -198,7 +198,7 @@ model APBDes {
//========================================= PRESTASI DESA ========================================= // //========================================= PRESTASI DESA ========================================= //
model PrestasiDesa { model PrestasiDesa {
id String @id @default(cuid()) id String @id @default(cuid())
name String @unique name String
deskripsi String @db.Text deskripsi String @db.Text
kategori KategoriPrestasiDesa @relation(fields: [kategoriId], references: [id]) kategori KategoriPrestasiDesa @relation(fields: [kategoriId], references: [id])
kategoriId String kategoriId String
@@ -972,8 +972,10 @@ model ArtikelKesehatan {
id String @id @default(cuid()) id String @id @default(cuid())
title String title String
content String content String
introduction Introduction @relation(fields: [introductionId], references: [id]) image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
introductionId String introductionId String
introduction Introduction @relation(fields: [introductionId], references: [id])
symptom Symptom @relation(fields: [symptomId], references: [id]) symptom Symptom @relation(fields: [symptomId], references: [id])
symptomId String symptomId String
prevention Prevention @relation(fields: [preventionId], references: [id]) prevention Prevention @relation(fields: [preventionId], references: [id])
@@ -1216,71 +1218,51 @@ model LayananPolsek {
// ========================================= KONTAK DARURAT ========================================= // // ========================================= KONTAK DARURAT ========================================= //
model KontakDaruratKeamanan { model KontakDaruratKeamanan {
id String @id @default(uuid()) id String @id @default(uuid())
nama String // contoh: "Layanan Darurat", "Fasilitas Kesehatan" nama String
image FileStorage? @relation(fields: [imageId], references: [id]) icon String
imageId String? kategori KontakItem @relation(fields: [kategoriId], references: [id])
kontakItems KontakItem[] kategoriId String
createdAt DateTime @default(now()) kontakItems KontakDaruratToItem[]
updatedAt DateTime @updatedAt createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
isActive Boolean @default(true)
} }
model KontakItem { model KontakItem {
id String @id @default(uuid()) id String @id @default(uuid())
nama String // contoh: "Polisi", "Ambulans", "Puskesmas Darmasaba" nama String
nomorTelepon String nomorTelepon String
image FileStorage? @relation(fields: [imageId], references: [id]) icon String
imageId String? createdAt DateTime @default(now())
kategori KontakDaruratKeamanan @relation(fields: [kategoriId], references: [id]) updatedAt DateTime @updatedAt
kategoriId String deletedAt DateTime @default(now())
createdAt DateTime @default(now()) isActive Boolean @default(true)
updatedAt DateTime @updatedAt KontakDaruratToItem KontakDaruratToItem[]
KontakDaruratKeamanan KontakDaruratKeamanan[]
}
model KontakDaruratToItem {
id String @id @default(uuid())
kontakDaruratId String
kontakItemId String
kontakDarurat KontakDaruratKeamanan @relation(fields: [kontakDaruratId], references: [id])
kontakItem KontakItem @relation(fields: [kontakItemId], references: [id])
createdAt DateTime @default(now())
} }
// ========================================= PENCEGAHAN KRIMINALITAS ========================================= // // ========================================= PENCEGAHAN KRIMINALITAS ========================================= //
model PencegahanKriminalitas { model PencegahanKriminalitas {
id String @id @default(cuid()) id String @id @default(cuid())
programKeamanan ProgramKeamanan @relation(fields: [programKeamananId], references: [id]) judul String
programKeamananId String deskripsi String
tipsKeamanan TipsKeamanan @relation(fields: [tipsKeamananId], references: [id]) deskripsiSingkat String
tipsKeamananId String linkVideo String
videoKeamanan VideoKeamanan @relation(fields: [videoKeamananId], references: [id]) createdAt DateTime @default(now())
videoKeamananId String updatedAt DateTime @updatedAt
createdAt DateTime @default(now()) deletedAt DateTime @default(now())
updatedAt DateTime @updatedAt isActive Boolean @default(true)
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model ProgramKeamanan {
id String @id @default(cuid())
nama String // contoh: "Ronda Malam"
deskripsi String? // jika mau tambahkan info detail
slug String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
PencegahanKriminalitas PencegahanKriminalitas[]
}
model TipsKeamanan {
id String @id @default(cuid())
judul String
konten String
slug String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
PencegahanKriminalitas PencegahanKriminalitas[]
}
model VideoKeamanan {
id String @id @default(cuid())
judul String
deskripsi String?
videoUrl String // link youtube atau embed url
slug String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
PencegahanKriminalitas PencegahanKriminalitas[]
} }
// ========================================= LAPORAN PUBLIK ========================================= // // ========================================= LAPORAN PUBLIK ========================================= //
@@ -1289,11 +1271,13 @@ model LaporanPublik {
judul String judul String
lokasi String lokasi String
tanggalWaktu DateTime tanggalWaktu DateTime
status StatusLaporan status StatusLaporan @default(Proses)
penanganan PenangananLaporanPublik[] penanganan PenangananLaporanPublik[]
kronologi String? // Optional, bisa diisi detail kronologi kronologi String? // Optional, bisa diisi detail kronologi
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
} }
model PenangananLaporanPublik { model PenangananLaporanPublik {
@@ -1401,6 +1385,9 @@ model PosisiOrganisasi {
pegawai Pegawai[] pegawai Pegawai[]
strukturOrganisasi StrukturOrganisasi[] // Relasi balik strukturOrganisasi StrukturOrganisasi[] // Relasi balik
StrukturOrganisasiPPID StrukturOrganisasiPPID[] StrukturOrganisasiPPID StrukturOrganisasiPPID[]
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("posisi_organisasi") @@map("posisi_organisasi")
} }
@@ -1469,7 +1456,7 @@ model ProgramKemiskinan {
id String @id @default(uuid()) id String @id @default(uuid())
nama String nama String
deskripsi String deskripsi String
ikonUrl String? icon String
isActive Boolean @default(true) isActive Boolean @default(true)
statistikId String? @unique statistikId String? @unique
statistik StatistikKemiskinan? @relation(fields: [statistikId], references: [id]) statistik StatistikKemiskinan? @relation(fields: [statistikId], references: [id])
@@ -1551,7 +1538,7 @@ model DataDemografiPekerjaan {
model DetailDataPengangguran { model DetailDataPengangguran {
id String @id @default(uuid()) @db.Uuid id String @id @default(uuid()) @db.Uuid
month String @db.VarChar(20) month String @db.VarChar(20)
year DateTime year Int
totalUnemployment Int totalUnemployment Int
educatedUnemployment Int educatedUnemployment Int
uneducatedUnemployment Int uneducatedUnemployment Int
@@ -2106,18 +2093,18 @@ model KategoriBuku {
// ========================================= USER ========================================= // // ========================================= USER ========================================= //
model User { model User {
id String @id @default(cuid()) id String @id @default(cuid())
username String username String
nomor String @unique nomor String @unique
role Role @relation(fields: [roleId], references: [id]) role Role @relation(fields: [roleId], references: [id])
roleId String @default("1") roleId String @default("1")
instansi String? instansi String?
UserSession UserSession? // Nama instansi (Puskesmas, Sekolah, dll) UserSession UserSession? // Nama instansi (Puskesmas, Sekolah, dll)
isActive Boolean @default(true) isActive Boolean @default(true)
lastLogin DateTime? lastLogin DateTime?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
deletedAt DateTime? deletedAt DateTime?
} }
model Role { model Role {

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import profilePejabatDesa from "./data/landing-page/profile/profile.json"; import profilePejabatDesa from "./data/landing-page/profile/profile.json";
import programInovasi from "./data/landing-page/profile/programInovasi.json"; import programInovasi from "./data/landing-page/profile/programInovasi.json";
@@ -15,7 +16,7 @@ import posisiOrganisasiPPID from "./data/ppid/struktur-ppid/posisi-organisasi-PP
import visiMisiPPID from "./data/ppid/visi-misi-ppid/visimisiPPID.json"; import visiMisiPPID from "./data/ppid/visi-misi-ppid/visimisiPPID.json";
import dasarHukumPPID from "./data/ppid/dasar-hukum-ppid/dasarhukumPPID.json"; import dasarHukumPPID from "./data/ppid/dasar-hukum-ppid/dasarhukumPPID.json";
import jenisKelamin from "./data/ppid/ikm/jenis-kelamin/jenis-kelamin.json"; import jenisKelamin from "./data/ppid/ikm/jenis-kelamin/jenis-kelamin.json";
import daftarInformasiPublik from "./data/ppid/daftar-informasi-publik-desa-darmasaba/daftarInformasi.json" import daftarInformasiPublik from "./data/ppid/daftar-informasi-publik-desa-darmasaba/daftarInformasi.json";
import pilihanRatingResponden from "./data/ppid/ikm/pilihan-rating-responden/rating-responden.json"; import pilihanRatingResponden from "./data/ppid/ikm/pilihan-rating-responden/rating-responden.json";
import umurResponden from "./data/ppid/ikm/umur-responden/umur-responden.json"; import umurResponden from "./data/ppid/ikm/umur-responden/umur-responden.json";
import categoryPengumuman from "./data/category-pengumuman.json"; import categoryPengumuman from "./data/category-pengumuman.json";
@@ -54,63 +55,90 @@ import programUnggulan from "./data/pendidikan/program-pendidikan-anak/program-u
import tujuanProgram from "./data/pendidikan/program-pendidikan-anak/tujuan-program.json"; import tujuanProgram from "./data/pendidikan/program-pendidikan-anak/tujuan-program.json";
import roles from "./data/user/roles.json"; import roles from "./data/user/roles.json";
import users from "./data/user/users.json"; import users from "./data/user/users.json";
import fileStorage from "./data/file-storage.json";
(async () => { (async () => {
// =========== USER & ROLE =========== // =========== USER & ROLE ===========
// In your seed.ts // In your seed.ts
// =========== ROLES =========== // =========== ROLES ===========
console.log("🔄 Seeding roles..."); console.log("🔄 Seeding roles...");
for (const r of roles) { for (const r of roles) {
await prisma.role.upsert({ await prisma.role.upsert({
where: { id: r.id }, where: { id: r.id },
update: { update: {
name: r.name, name: r.name,
description: r.description, description: r.description,
permissions: r.permissions, permissions: r.permissions,
isActive: r.isActive, isActive: r.isActive,
}, },
create: { create: {
id: r.id, id: r.id,
name: r.name, name: r.name,
description: r.description, description: r.description,
permissions: r.permissions, permissions: r.permissions,
isActive: r.isActive, 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;
} }
console.log("✅ Roles seeded");
await prisma.user.upsert({ // =========== USERS ===========
where: { id: u.id }, console.log("🔄 Seeding users...");
update: { for (const u of users) {
username: u.nama, // First verify the role exists
nomor: u.nomor, const roleExists = await prisma.role.findUnique({
roleId: u.roleId, where: { id: u.roleId },
isActive: u.isActive, });
},
create: { if (!roleExists) {
id: u.id, console.error(`❌ Role with id ${u.roleId} not found for user ${u.nama}`);
username: u.nama, continue;
nomor: u.nomor, }
roleId: u.roleId,
isActive: u.isActive, await prisma.user.upsert({
}, where: { id: u.id },
}); update: {
} username: u.nama,
console.log("✅ Users seeded"); 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");
// =========== FILE STORAGE ===========
console.log("🔄 Seeding file storage...");
for (const f of fileStorage) {
await prisma.fileStorage.upsert({
where: { id: f.id },
update: {
name: f.name,
realName: f.realName,
path: f.path,
mimeType: f.mimeType,
link: f.link,
category: f.category,
},
create: {
id: f.id,
name: f.name,
realName: f.realName,
path: f.path,
mimeType: f.mimeType,
link: f.link,
category: f.category,
},
});
}
console.log("✅ File storage seeded");
// =========== LANDING PAGE =========== // =========== LANDING PAGE ===========
// =========== SUBMENU PROFILE =========== // =========== SUBMENU PROFILE ===========
// =========== PROFILE PEJABAT DESA =========== // =========== PROFILE PEJABAT DESA ===========
@@ -120,11 +148,13 @@ console.log("✅ Users seeded");
update: { update: {
name: p.name, name: p.name,
position: p.position, position: p.position,
imageId: p.imageId,
}, },
create: { create: {
id: p.id, id: p.id,
name: p.name, name: p.name,
position: p.position, position: p.position,
imageId: p.imageId,
}, },
}); });
} }
@@ -134,18 +164,35 @@ console.log("✅ Users seeded");
// =========== PROGRAM INOVASI =========== // =========== PROGRAM INOVASI ===========
for (const p of programInovasi) { for (const p of programInovasi) {
let imageId: string | null = null;
if (p.imageId) {
const imageExists = await prisma.fileStorage.findUnique({
where: { id: p.imageId },
});
if (imageExists) {
imageId = p.imageId;
} else {
console.warn(
`⚠️ imageId ${p.imageId} tidak ditemukan untuk ProgramInovasi ${p.name}`
);
}
}
await prisma.programInovasi.upsert({ await prisma.programInovasi.upsert({
where: { id: p.id }, where: { id: p.id },
update: { update: {
name: p.name, name: p.name,
description: p.description, description: p.description,
link: p.link, link: p.link,
imageId: p.imageId,
}, },
create: { create: {
id: p.id, id: p.id,
name: p.name, name: p.name,
description: p.description, description: p.description,
link: p.link, link: p.link,
imageId: p.imageId,
}, },
}); });
} }
@@ -158,11 +205,13 @@ console.log("✅ Users seeded");
update: { update: {
name: p.name, name: p.name,
iconUrl: p.iconUrl, iconUrl: p.iconUrl,
imageId: p.imageId,
}, },
create: { create: {
id: p.id, id: p.id,
name: p.name, name: p.name,
iconUrl: p.iconUrl, iconUrl: p.iconUrl,
imageId: p.imageId,
}, },
}); });
} }
@@ -308,11 +357,8 @@ console.log("✅ Users seeded");
// =========== SDGSDesa =========== // =========== SDGSDesa ===========
for (const l of sdgsDesa) { for (const l of sdgsDesa) {
await prisma.sDGSDesa.upsert({ await prisma.sdgsDesa.upsert({
where: { where: { id: l.id },
name: l.name,
jumlah: l.jumlah,
},
update: { update: {
name: l.name, name: l.name,
jumlah: l.jumlah, jumlah: l.jumlah,
@@ -330,8 +376,7 @@ console.log("✅ Users seeded");
for (const l of apbdes) { for (const l of apbdes) {
await prisma.aPBDes.upsert({ await prisma.aPBDes.upsert({
where: { where: {
name: l.name, id: l.id,
jumlah: l.jumlah,
}, },
update: { update: {
name: l.name, name: l.name,
@@ -558,29 +603,29 @@ console.log("✅ Users seeded");
} }
console.log("dasar hukum PPID success ..."); console.log("dasar hukum PPID success ...");
// =========== SUBMENU DAFTAR INFORMASI PUBLIK PPID =========== // =========== SUBMENU DAFTAR INFORMASI PUBLIK PPID ===========
for (const v of daftarInformasiPublik) { for (const v of daftarInformasiPublik) {
// Convert string date to Date object // Convert string date to Date object
const tanggal = new Date(v.tanggal); const tanggal = new Date(v.tanggal);
await prisma.daftarInformasiPublik.upsert({ await prisma.daftarInformasiPublik.upsert({
where: { where: {
id: v.id, id: v.id,
}, },
update: { update: {
jenisInformasi: v.jenisInformasi, jenisInformasi: v.jenisInformasi,
deskripsi: v.deskripsi, deskripsi: v.deskripsi,
tanggal: tanggal, tanggal: tanggal,
}, },
create: { create: {
id: v.id, id: v.id,
jenisInformasi: v.jenisInformasi, jenisInformasi: v.jenisInformasi,
deskripsi: v.deskripsi, deskripsi: v.deskripsi,
tanggal: tanggal, tanggal: tanggal,
}, },
}); });
} }
console.log("daftar informasi publik PPID success ..."); console.log("daftar informasi publik PPID success ...");
for (const l of pelayananPerizinanBerusaha) { for (const l of pelayananPerizinanBerusaha) {
await prisma.pelayananPerizinanBerusaha.upsert({ await prisma.pelayananPerizinanBerusaha.upsert({
@@ -849,12 +894,9 @@ console.log("✅ Users seeded");
console.log("hubungan organisasi success ..."); console.log("hubungan organisasi success ...");
for (const d of detailDataPengangguran) { for (const d of detailDataPengangguran) {
// Convert the year to a Date object (using January 1st of the year as the date)
const yearAsDate = new Date(d.year, 0, 1);
await prisma.detailDataPengangguran.upsert({ await prisma.detailDataPengangguran.upsert({
where: { where: {
month_year: { month: d.month, year: yearAsDate }, month_year: { month: d.month, year: d.year },
}, },
update: { update: {
totalUnemployment: d.totalUnemployment, totalUnemployment: d.totalUnemployment,
@@ -864,7 +906,7 @@ console.log("✅ Users seeded");
}, },
create: { create: {
month: d.month, month: d.month,
year: yearAsDate, year: d.year,
totalUnemployment: d.totalUnemployment, totalUnemployment: d.totalUnemployment,
educatedUnemployment: d.educatedUnemployment, educatedUnemployment: d.educatedUnemployment,
uneducatedUnemployment: d.uneducatedUnemployment, uneducatedUnemployment: d.uneducatedUnemployment,

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.

After

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

After

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -120,27 +120,36 @@ const jadwalkegiatanState = proxy({
}; };
}>[] }>[]
| null, | null,
page: 1,
totalPages: 1,
loading: false, loading: false,
async load() { search: "",
load: async (page = 1, limit = 10, search = "") => {
jadwalkegiatanState.findMany.loading = true; // ✅ Akses langsung via nama path
jadwalkegiatanState.findMany.page = page;
jadwalkegiatanState.findMany.search = search;
try { try {
this.loading = true; const query: any = { page, limit };
const res = await (ApiFetch.api.kesehatan as any)[ if (search) query.search = search;
"jadwal-kegiatan"
]["find-many"].get();
if (res.status === 200) { const res = await ApiFetch.api.kesehatan["jadwal-kegiatan"][
this.data = res.data?.data ?? []; "find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
jadwalkegiatanState.findMany.data = res.data.data ?? [];
jadwalkegiatanState.findMany.totalPages = res.data.totalPages ?? 1;
} else { } else {
toast.error("Gagal memuat data jadwal kegiatan"); jadwalkegiatanState.findMany.data = [];
jadwalkegiatanState.findMany.totalPages = 1;
} }
return res;
} catch (err) { } catch (err) {
toast.error("Terjadi error saat load data"); console.error("Gagal fetch jadwal kegiatan paginated:", err);
console.error("LOAD ERROR:", err); jadwalkegiatanState.findMany.data = [];
throw err; jadwalkegiatanState.findMany.totalPages = 1;
} finally { } finally {
this.loading = false; jadwalkegiatanState.findMany.loading = false;
} }
}, },
}, },
@@ -227,29 +236,42 @@ const jadwalkegiatanState = proxy({
content: jadwalkegiatanState.edit.form.content, content: jadwalkegiatanState.edit.form.content,
informasiJadwalKegiatan: { informasiJadwalKegiatan: {
name: jadwalkegiatanState.edit.form.informasiJadwalKegiatan.name, name: jadwalkegiatanState.edit.form.informasiJadwalKegiatan.name,
tanggal: jadwalkegiatanState.edit.form.informasiJadwalKegiatan.tanggal, tanggal:
jadwalkegiatanState.edit.form.informasiJadwalKegiatan.tanggal,
waktu: jadwalkegiatanState.edit.form.informasiJadwalKegiatan.waktu, waktu: jadwalkegiatanState.edit.form.informasiJadwalKegiatan.waktu,
lokasi: jadwalkegiatanState.edit.form.informasiJadwalKegiatan.lokasi, lokasi:
jadwalkegiatanState.edit.form.informasiJadwalKegiatan.lokasi,
}, },
layananJadwalKegiatan: { layananJadwalKegiatan: {
content: jadwalkegiatanState.edit.form.layananJadwalKegiatan.content, content:
jadwalkegiatanState.edit.form.layananJadwalKegiatan.content,
}, },
deskripsiJadwalKegiatan: { deskripsiJadwalKegiatan: {
deskripsi: jadwalkegiatanState.edit.form.deskripsiJadwalKegiatan.deskripsi, deskripsi:
jadwalkegiatanState.edit.form.deskripsiJadwalKegiatan.deskripsi,
}, },
syaratKetentuanJadwalKegiatan: { syaratKetentuanJadwalKegiatan: {
content: jadwalkegiatanState.edit.form.syaratKetentuanJadwalKegiatan.content, content:
jadwalkegiatanState.edit.form.syaratKetentuanJadwalKegiatan
.content,
}, },
dokumenJadwalKegiatan: { dokumenJadwalKegiatan: {
content: jadwalkegiatanState.edit.form.dokumenJadwalKegiatan.content, content:
jadwalkegiatanState.edit.form.dokumenJadwalKegiatan.content,
}, },
pendaftaranJadwalKegiatan: { pendaftaranJadwalKegiatan: {
name: jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.name, name: jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.name,
tanggal: jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.tanggal, tanggal:
namaOrangtua: jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.namaOrangtua, jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.tanggal,
nomor: jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.nomor, namaOrangtua:
alamat: jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.alamat, jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan
catatan: jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.catatan, .namaOrangtua,
nomor:
jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.nomor,
alamat:
jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.alamat,
catatan:
jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.catatan,
}, },
}; };
@@ -286,7 +308,7 @@ const jadwalkegiatanState = proxy({
}, },
delete: { delete: {
loading: false, loading: false,
async byId(id: string){ async byId(id: string) {
try { try {
jadwalkegiatanState.delete.loading = true; jadwalkegiatanState.delete.loading = true;
const res = await fetch(`/api/kesehatan/jadwal-kegiatan/del/${id}`, { const res = await fetch(`/api/kesehatan/jadwal-kegiatan/del/${id}`, {
@@ -305,7 +327,7 @@ const jadwalkegiatanState = proxy({
} finally { } finally {
jadwalkegiatanState.delete.loading = false; jadwalkegiatanState.delete.loading = false;
} }
} },
}, },
}); });

View File

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

View File

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

View File

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

View File

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

View File

@@ -352,17 +352,19 @@ const posisiOrganisasi = proxy({
totalPages: 1, totalPages: 1,
loading: false, loading: false,
search: "", search: "",
load: async (page = 1, limit = 10, search = "") => { load: async (page = 1, limit?: number, search = "") => {
posisiOrganisasi.findMany.loading = true; // ✅ Akses langsung via nama path const appliedLimit = limit ?? 10;
posisiOrganisasi.findMany.page = page; posisiOrganisasi.findMany.page = page;
posisiOrganisasi.findMany.search = search; posisiOrganisasi.findMany.search = search;
try { try {
const query: any = { page, limit }; const query: any = { page, limit: appliedLimit };
if (search) query.search = search; if (search) query.search = search;
const res = await ApiFetch.api.ppid.strukturppid.posisiorganisasi["find-many"].get({ query }); const res = await ApiFetch.api.ppid.strukturppid.posisiorganisasi[
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) { if (res.status === 200 && res.data?.success) {
posisiOrganisasi.findMany.data = res.data.data ?? []; posisiOrganisasi.findMany.data = res.data.data ?? [];
posisiOrganisasi.findMany.totalPages = res.data.totalPages ?? 1; posisiOrganisasi.findMany.totalPages = res.data.totalPages ?? 1;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -198,6 +198,7 @@ function EditBerita() {
objectFit: "contain", objectFit: "contain",
border: `1px solid ${colors["blue-button"]}`, border: `1px solid ${colors["blue-button"]}`,
}} }}
loading="lazy"
/> />
</Box> </Box>
)} )}

View File

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

View File

@@ -183,6 +183,7 @@ export default function CreateBerita() {
objectFit: 'contain', objectFit: 'contain',
border: '1px solid #ddd', border: '1px solid #ddd',
}} }}
loading="lazy"
/> />
</Box> </Box>
)} )}

View File

@@ -96,9 +96,11 @@ function ListBerita({ search }: { search: string }) {
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd style={{ width: '30%' }}> <TableTd style={{ width: '30%' }}>
<Text fw={500} truncate="end" lineClamp={1}> <Box w={200}>
{item.judul} <Text fw={500} truncate="end" lineClamp={1}>
</Text> {item.judul}
</Text>
</Box>
</TableTd> </TableTd>
<TableTd style={{ width: '20%' }}> <TableTd style={{ width: '20%' }}>
<Text fz="sm" c="dimmed"> <Text fz="sm" c="dimmed">
@@ -112,7 +114,7 @@ function ListBerita({ search }: { search: string }) {
style={{ borderRadius: 8, overflow: 'hidden' }} style={{ borderRadius: 8, overflow: 'hidden' }}
> >
{item.image?.link ? ( {item.image?.link ? (
<Image src={item.image.link} alt="gambar" fit="cover" /> <Image loading='lazy' src={item.image.link} alt="gambar" fit="cover" />
) : ( ) : (
<Box bg={colors['blue-button']} w="100%" h="100%" /> <Box bg={colors['blue-button']} w="100%" h="100%" />
)} )}

View File

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

View File

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

View File

@@ -140,7 +140,7 @@ function EditSuratKeterangan() {
{/* Upload Gambar 1 */} {/* Upload Gambar 1 */}
<Box> <Box>
<Text fw="bold" fz="sm" mb={6}> <Text fw="bold" fz="sm" mb={6}>
Gambar 1 Gambar Konten Pelayanan
</Text> </Text>
<Dropzone <Dropzone
onDrop={(files) => { onDrop={(files) => {
@@ -188,6 +188,7 @@ function EditSuratKeterangan() {
objectFit: 'contain', objectFit: 'contain',
border: `1px solid ${colors['blue-button']}`, border: `1px solid ${colors['blue-button']}`,
}} }}
loading="lazy"
/> />
</Box> </Box>
)} )}
@@ -196,7 +197,7 @@ function EditSuratKeterangan() {
{/* Upload Gambar 2 */} {/* Upload Gambar 2 */}
<Box> <Box>
<Text fw="bold" fz="sm" mb={6}> <Text fw="bold" fz="sm" mb={6}>
Gambar 2 Gambar Alur Pelayanan Surat
</Text> </Text>
<Dropzone <Dropzone
onDrop={(files) => { onDrop={(files) => {
@@ -244,6 +245,7 @@ function EditSuratKeterangan() {
objectFit: 'contain', objectFit: 'contain',
border: `1px solid ${colors['blue-button']}`, border: `1px solid ${colors['blue-button']}`,
}} }}
loading="lazy"
/> />
</Box> </Box>
)} )}

View File

@@ -100,7 +100,7 @@ function DetailSuratKeterangan() {
<Box> <Box>
<Text fz="lg" fw="bold"> <Text fz="lg" fw="bold">
Gambar Gambar Konten Pelayanan
</Text> </Text>
{data?.image?.link ? ( {data?.image?.link ? (
<Image <Image
@@ -110,6 +110,7 @@ function DetailSuratKeterangan() {
h={200} h={200}
radius="md" radius="md"
fit="cover" fit="cover"
loading="lazy"
/> />
) : ( ) : (
<Text fz="sm" c="dimmed"> <Text fz="sm" c="dimmed">
@@ -120,7 +121,7 @@ function DetailSuratKeterangan() {
<Box> <Box>
<Text fz="lg" fw="bold"> <Text fz="lg" fw="bold">
Gambar 2 Gambar Alur Pelayanan Surat
</Text> </Text>
{data?.image2?.link ? ( {data?.image2?.link ? (
<Image <Image
@@ -130,6 +131,7 @@ function DetailSuratKeterangan() {
h={200} h={200}
radius="md" radius="md"
fit="cover" fit="cover"
loading="lazy"
/> />
) : ( ) : (
<Text fz="sm" c="dimmed"> <Text fz="sm" c="dimmed">

View File

@@ -126,10 +126,10 @@ function CreateSuratKeterangan() {
/> />
</Box> </Box>
{/* Gambar Utama */} {/* Gambar Konten Pelayanan */}
<Box> <Box>
<Text fw="bold" fz="sm" mb={6}> <Text fw="bold" fz="sm" mb={6}>
Gambar Utama Gambar Konten Pelayanan
</Text> </Text>
<Dropzone <Dropzone
onDrop={(files) => { onDrop={(files) => {
@@ -170,15 +170,16 @@ function CreateSuratKeterangan() {
alt="Preview Gambar Utama" alt="Preview Gambar Utama"
radius="md" radius="md"
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }} style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
loading="lazy"
/> />
</Box> </Box>
)} )}
</Box> </Box>
{/* Gambar Tambahan */} {/* Gambar Alur Pelayanan Surat */}
<Box> <Box>
<Text fw="bold" fz="sm" mb={6}> <Text fw="bold" fz="sm" mb={6}>
Gambar Tambahan (Opsional) Gambar Alur Pelayanan Surat
</Text> </Text>
<Dropzone <Dropzone
onDrop={(files) => { onDrop={(files) => {
@@ -219,6 +220,7 @@ function CreateSuratKeterangan() {
alt="Preview Gambar Tambahan" alt="Preview Gambar Tambahan"
radius="md" radius="md"
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }} style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
loading="lazy"
/> />
</Box> </Box>
) : ( ) : (

View File

@@ -20,7 +20,7 @@ import {
Title, Title,
Tooltip Tooltip
} from '@mantine/core'; } 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 { useRouter } from 'next/navigation';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
@@ -124,14 +124,16 @@ function ListSuratKeterangan({ search }: { search: string }) {
</TableTd> </TableTd>
<TableTd style={{ width: '15%' }}> <TableTd style={{ width: '15%' }}>
<Button <Button
size="xs"
radius="md"
variant="light" variant="light"
color="blue" color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() => onClick={() =>
router.push(`/admin/desa/layanan/pelayanan_surat_keterangan/${item.id}`) router.push(`/admin/desa/layanan/pelayanan_surat_keterangan/${item.id}`)
} }
> >
<IconDeviceImac size={20} /> Detail
<Text ml={5}>Detail</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>

View File

@@ -183,6 +183,7 @@ function EditPenghargaan() {
alt="Preview Gambar" alt="Preview Gambar"
radius="md" radius="md"
style={{ maxHeight: 220, objectFit: 'contain', border: `1px solid ${colors['blue-button']}` }} style={{ maxHeight: 220, objectFit: 'contain', border: `1px solid ${colors['blue-button']}` }}
loading="lazy"
/> />
</Box> </Box>
)} )}

View File

@@ -87,6 +87,7 @@ function DetailPenghargaan() {
h={200} h={200}
radius="md" radius="md"
fit="cover" fit="cover"
loading="lazy"
/> />
) : ( ) : (
<Text fz="sm" c="dimmed"> <Text fz="sm" c="dimmed">

View File

@@ -152,6 +152,7 @@ function CreatePenghargaan() {
alt="Preview Gambar" alt="Preview Gambar"
radius="md" radius="md"
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }} style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
loading="lazy"
/> />
</Box> </Box>
)} )}

View File

@@ -21,7 +21,7 @@ import {
Title, Title,
Tooltip Tooltip
} from '@mantine/core'; } 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 { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
@@ -113,14 +113,16 @@ function ListPenghargaan({ search }: { search: string }) {
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button <Button
variant="light" size="xs"
color="blue" radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() => onClick={() =>
router.push(`/admin/desa/penghargaan/${item.id}`) router.push(`/admin/desa/penghargaan/${item.id}`)
} }
> >
<IconDeviceImac size={20} /> Detail
<Text ml={5}>Detail</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>

View File

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

View File

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

View File

@@ -202,6 +202,7 @@ function EditPotensi() {
objectFit: "contain", objectFit: "contain",
border: `1px solid ${colors["blue-button"]}`, border: `1px solid ${colors["blue-button"]}`,
}} }}
loading="lazy"
/> />
</Box> </Box>
)} )}

View File

@@ -90,6 +90,7 @@ export default function DetailPotensi() {
h={200} h={200}
radius="md" radius="md"
fit="cover" fit="cover"
loading='lazy'
/> />
) : ( ) : (
<Text fz="sm" c="dimmed">Tidak ada gambar</Text> <Text fz="sm" c="dimmed">Tidak ada gambar</Text>

View File

@@ -165,6 +165,7 @@ function CreatePotensi() {
alt="Preview Gambar" alt="Preview Gambar"
radius="md" radius="md"
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }} style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
loading='lazy'
/> />
</Box> </Box>
)} )}
@@ -195,7 +196,7 @@ function CreatePotensi() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan Potensi Simpan
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -120,12 +120,14 @@ function ListPotensi({ search }: { search: string }) {
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button <Button
size="xs"
radius="md"
variant="light" variant="light"
color="blue" color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() => router.push(`/admin/desa/potensi/list-potensi/${item.id}`)} onClick={() => router.push(`/admin/desa/potensi/list-potensi/${item.id}`)}
> >
<IconDeviceImacCog size={20} /> Detail
<Text ml={5}>Detail</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>

View File

@@ -1,7 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core'; import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { IconUser, IconUsers, IconCalendar } from '@tabler/icons-react'; import { IconUser, IconUsers, IconCalendar } from '@tabler/icons-react';
@@ -55,56 +55,63 @@ function LayoutTabsDetail({ children }: { children: React.ReactNode }) {
<Stack gap="lg"> <Stack gap="lg">
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Profile Desa</Title> <Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Profile Desa</Title>
<Tabs <Tabs
color={colors['blue-button']} color={colors["blue-button"]}
variant='pills' variant="pills"
value={activeTab} value={activeTab}
onChange={handleTabChange} onChange={handleTabChange}
radius="lg" radius="lg"
keepMounted={false} keepMounted={false}
> >
<TabsList {/* ✅ Scroll horizontal wrapper */}
p="sm" <ScrollArea type="auto" offsetScrollbars>
style={{ <TabsList
background: "linear-gradient(135deg, #e7ebf7, #f9faff)", p="sm"
borderRadius: "1rem",
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",
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
{tabs.map((tab, i) => (
<TabsPanel
key={i}
value={tab.value}
style={{ style={{
padding: "1.5rem", background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
borderRadius: "1rem", borderRadius: "1rem",
boxShadow: "0 4px 16px rgba(0,0,0,0.05)", 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
}} }}
> >
{/* Konten dummy, bisa diganti sesuai routing */} {tabs.map((tab, i) => (
<>{children}</> <Tooltip key={i} label={tab.tooltip} position="bottom" withArrow transitionProps={{ transition: 'pop', duration: 200 }}>
</TabsPanel> <TabsTab
))} value={tab.value}
</Tabs> leftSection={tab.icon}
</Stack> style={{
); fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
</ScrollArea>
{tabs.map((tab, i) => (
<TabsPanel
key={i}
value={tab.value}
style={{
padding: "1.5rem",
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
borderRadius: "1rem",
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
}}
>
{/* Konten dummy, bisa diganti sesuai routing */}
<>{children}</>
</TabsPanel>
))}
</Tabs>
</Stack>
);
} }
export default LayoutTabsDetail; export default LayoutTabsDetail;

View File

@@ -241,6 +241,7 @@ function Page() {
height={180} height={180}
fit="cover" fit="cover"
radius="sm" radius="sm"
loading='lazy'
/> />
<TextInput <TextInput
label={`Label Gambar ${index + 1}`} label={`Label Gambar ${index + 1}`}

View File

@@ -55,7 +55,7 @@ function Page() {
<Paper bg={colors['white-1']} withBorder radius="md" shadow="xs" p="lg"> <Paper bg={colors['white-1']} withBorder radius="md" shadow="xs" p="lg">
<Stack gap={0}> <Stack gap={0}>
<Center> <Center>
<Image src="/darmasaba-icon.png" w={{ base: 150, md: 250 }} alt="Logo Desa" /> <Image loading='lazy' src="/darmasaba-icon.png" w={{ base: 150, md: 250 }} alt="Logo Desa" />
</Center> </Center>
<Paper <Paper
bg={colors['blue-button']} bg={colors['blue-button']}
@@ -102,7 +102,7 @@ function Page() {
<Paper bg={colors['white-1']} withBorder radius="md" shadow="xs" p="lg"> <Paper bg={colors['white-1']} withBorder radius="md" shadow="xs" p="lg">
<Stack gap={0}> <Stack gap={0}>
<Center> <Center>
<Image src="/darmasaba-icon.png" w={{ base: 150, md: 250 }} alt="Logo Desa" /> <Image loading='lazy' src="/darmasaba-icon.png" w={{ base: 150, md: 250 }} alt="Logo Desa" />
</Center> </Center>
<Paper <Paper
bg={colors['blue-button']} bg={colors['blue-button']}
@@ -152,7 +152,7 @@ function Page() {
<Paper bg={colors['white-1']} withBorder radius="md" shadow="xs" p="lg"> <Paper bg={colors['white-1']} withBorder radius="md" shadow="xs" p="lg">
<Stack gap={0}> <Stack gap={0}>
<Center> <Center>
<Image src="/darmasaba-icon.png" w={{ base: 150, md: 250 }} alt="Logo Desa" /> <Image loading='lazy' src="/darmasaba-icon.png" w={{ base: 150, md: 250 }} alt="Logo Desa" />
</Center> </Center>
<Paper <Paper
bg={colors['blue-button']} bg={colors['blue-button']}
@@ -199,7 +199,7 @@ function Page() {
<Paper bg={colors['white-1']} withBorder radius="md" shadow="xs" p="lg"> <Paper bg={colors['white-1']} withBorder radius="md" shadow="xs" p="lg">
<Stack gap={0}> <Stack gap={0}>
<Center> <Center>
<Image src="/pudak-icon.png" w={{ base: 150, md: 250 }} alt="Maskot Desa" /> <Image loading='lazy' src="/pudak-icon.png" w={{ base: 150, md: 250 }} alt="Maskot Desa" />
</Center> </Center>
<Paper <Paper
bg={colors['blue-button']} bg={colors['blue-button']}
@@ -228,6 +228,7 @@ function Page() {
fit="cover" fit="cover"
radius="md" radius="md"
style={{ border: '1px solid #ccc' }} style={{ border: '1px solid #ccc' }}
loading='lazy'
/> />
</Center> </Center>
<Text ta="center" mt="xs" fw="bold">{img.label}</Text> <Text ta="center" mt="xs" fw="bold">{img.label}</Text>

View File

@@ -161,6 +161,7 @@ function EditPerbekelDariMasaKeMasa() {
objectFit: 'contain', objectFit: 'contain',
border: `1px solid ${colors['blue-button']}`, border: `1px solid ${colors['blue-button']}`,
}} }}
loading='lazy'
/> />
</Box> </Box>
)} )}

View File

@@ -75,6 +75,7 @@ function DetailPerbekelDariMasa() {
h={150} h={150}
radius="md" radius="md"
fit="cover" fit="cover"
loading='lazy'
/> />
) : ( ) : (
<Text fz="sm" c="dimmed">Tidak ada gambar</Text> <Text fz="sm" c="dimmed">Tidak ada gambar</Text>

View File

@@ -131,6 +131,7 @@ function CreatePerbekelDariMasaKeMasa() {
alt="Preview Gambar" alt="Preview Gambar"
radius="md" radius="md"
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }} style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
loading='lazy'
/> />
</Box> </Box>
)} )}

View File

@@ -2,7 +2,7 @@
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core'; import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
@@ -82,12 +82,14 @@ function ListPerbekelDariMasaKeMasa({ search }: { search: string }) {
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button <Button
size="xs"
radius="md"
variant="light" variant="light"
color="blue" color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() => router.push(`/admin/desa/profile/profile-perbekel-dari-masa-ke-masa/${item.id}`)} onClick={() => router.push(`/admin/desa/profile/profile-perbekel-dari-masa-ke-masa/${item.id}`)}
> >
<IconDeviceImac size={20} /> Detail
<Text ml={5}>Detail</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>

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