Compare commits
23 Commits
nico/26-au
...
nico/19-se
| Author | SHA1 | Date | |
|---|---|---|---|
| 0fc47c28ff | |||
| 8e25c91e85 | |||
| 068d8b1077 | |||
| 9f72e94557 | |||
| 79ad39fc55 | |||
| 39e1e7b575 | |||
| 4ceea5203f | |||
| a5d841bb6b | |||
| 6a7bd386ae | |||
| a9d98895bb | |||
| 75475dc62e | |||
| b39800a475 | |||
| 797713ef49 | |||
| 8817b937b1 | |||
| 2adf60f9eb | |||
| fa9601e126 | |||
| 7ae83788b4 | |||
| 22ec8d942d | |||
| 9f9a0fb451 | |||
| b6d6583e77 | |||
| a8fd715822 | |||
| f9530c32eb | |||
| f15ef5a275 |
2
.gitignore
vendored
@@ -48,3 +48,5 @@ next-env.d.ts
|
|||||||
|
|
||||||
.env.*
|
.env.*
|
||||||
|
|
||||||
|
*.tar.gz
|
||||||
|
|
||||||
|
|||||||
15
package.json
@@ -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"
|
||||||
@@ -57,6 +55,8 @@
|
|||||||
"form-data": "^4.0.2",
|
"form-data": "^4.0.2",
|
||||||
"framer-motion": "^12.23.5",
|
"framer-motion": "^12.23.5",
|
||||||
"get-port": "^7.1.0",
|
"get-port": "^7.1.0",
|
||||||
|
"iron-session": "^8.0.4",
|
||||||
|
"jose": "^6.1.0",
|
||||||
"jotai": "^2.12.3",
|
"jotai": "^2.12.3",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"motion": "^12.4.1",
|
"motion": "^12.4.1",
|
||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
"next": "15.1.6",
|
"next": "^15.5.2",
|
||||||
"next-view-transitions": "^0.3.4",
|
"next-view-transitions": "^0.3.4",
|
||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
"p-limit": "^6.2.0",
|
"p-limit": "^6.2.0",
|
||||||
@@ -73,15 +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": {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
137
prisma/data/file-storage.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"id": "cmdpm429r0000vnndkcwslt0h",
|
|
||||||
"name": "warga"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
29
prisma/data/user/roles.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "1",
|
||||||
|
"name": "ADMIN DESA",
|
||||||
|
"description": "Administrator Desa",
|
||||||
|
"permissions": ["manage_users", "manage_content", "view_reports"],
|
||||||
|
"isActive": true,
|
||||||
|
"createdAt": "2025-09-01T00:00:00.000Z",
|
||||||
|
"updatedAt": "2025-09-01T00:00:00.000Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2",
|
||||||
|
"name": "ADMIN KESEHATAN",
|
||||||
|
"description": "Administrator Bidang Kesehatan",
|
||||||
|
"permissions": ["manage_health_data", "view_reports"],
|
||||||
|
"isActive": true,
|
||||||
|
"createdAt": "2025-09-01T00:00:00.000Z",
|
||||||
|
"updatedAt": "2025-09-01T00:00:00.000Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3",
|
||||||
|
"name": "ADMIN SEKOLAH",
|
||||||
|
"description": "Administrator Sekolah",
|
||||||
|
"permissions": ["manage_school_data", "view_reports"],
|
||||||
|
"isActive": true,
|
||||||
|
"createdAt": "2025-09-01T00:00:00.000Z",
|
||||||
|
"updatedAt": "2025-09-01T00:00:00.000Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
32
prisma/data/user/users.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "1",
|
||||||
|
"nama": "Admin Desa",
|
||||||
|
"nomor": "089647037426",
|
||||||
|
"roleId": "1",
|
||||||
|
"isActive": true,
|
||||||
|
"lastLogin": "2025-08-31T10:00:00.000Z",
|
||||||
|
"createdAt": "2025-09-01T00:00:00.000Z",
|
||||||
|
"updatedAt": "2025-09-01T00:00:00.000Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2",
|
||||||
|
"nama": "Admin Kesehatan",
|
||||||
|
"nomor": "082339004198",
|
||||||
|
"roleId": "2",
|
||||||
|
"isActive": true,
|
||||||
|
"lastLogin": null,
|
||||||
|
"createdAt": "2025-09-01T00:00:00.000Z",
|
||||||
|
"updatedAt": "2025-09-01T00:00:00.000Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3",
|
||||||
|
"nama": "Admin Sekolah",
|
||||||
|
"nomor": "085237157222",
|
||||||
|
"roleId": "3",
|
||||||
|
"isActive": true,
|
||||||
|
"lastLogin": null,
|
||||||
|
"createdAt": "2025-09-01T00:00:00.000Z",
|
||||||
|
"updatedAt": "2025-09-01T00:00:00.000Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -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,11 +198,11 @@ 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
|
||||||
image FileStorage? @relation(fields: [imageId], references: [id])
|
image FileStorage? @relation(fields: [imageId], references: [id])
|
||||||
imageId String?
|
imageId String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
@@ -293,9 +293,9 @@ model PosisiOrganisasiPPID {
|
|||||||
pegawai PegawaiPPID[]
|
pegawai PegawaiPPID[]
|
||||||
strukturOrganisasi StrukturPPID[] // Relasi balik
|
strukturOrganisasi StrukturPPID[] // Relasi balik
|
||||||
parentId String?
|
parentId String?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
parent PosisiOrganisasiPPID? @relation("Parent", fields: [parentId], references: [id])
|
parent PosisiOrganisasiPPID? @relation("Parent", fields: [parentId], references: [id])
|
||||||
children PosisiOrganisasiPPID[] @relation("Parent")
|
children PosisiOrganisasiPPID[] @relation("Parent")
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
@@ -1639,28 +1626,29 @@ model ProgramKreatif {
|
|||||||
|
|
||||||
// ========================================= KOLABORASI INOVASI ========================================= //
|
// ========================================= KOLABORASI INOVASI ========================================= //
|
||||||
model KolaborasiInovasi {
|
model KolaborasiInovasi {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String
|
name String
|
||||||
tahun Int
|
tahun Int
|
||||||
slug String @db.Text //deskripsi singkat
|
slug String @db.Text //deskripsi singkat
|
||||||
deskripsi String @db.Text //deskripsi panjang
|
deskripsi String @db.Text //deskripsi panjang
|
||||||
kolaborator String
|
kolaborator String
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime @default(now())
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
model MitraKolaborasi {
|
model MitraKolaborasi {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String
|
name String
|
||||||
image FileStorage? @relation(fields: [imageId], references: [id])
|
image FileStorage? @relation(fields: [imageId], references: [id])
|
||||||
imageId String?
|
imageId String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime @default(now())
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================= INFO TEKHNOLOGI TEPAT GUNA ========================================= //
|
// ========================================= INFO TEKHNOLOGI TEPAT GUNA ========================================= //
|
||||||
model InfoTekno {
|
model InfoTekno {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
@@ -2102,26 +2090,66 @@ model KategoriBuku {
|
|||||||
DataPerpustakaan DataPerpustakaan[]
|
DataPerpustakaan DataPerpustakaan[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========================================= USER ========================================= //
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
nama String
|
username String
|
||||||
email String @unique
|
nomor String @unique
|
||||||
password String
|
role Role @relation(fields: [roleId], references: [id])
|
||||||
role Role @relation(fields: [roleId], references: [id])
|
roleId String @default("1")
|
||||||
roleId String
|
instansi String?
|
||||||
isActive Boolean @default(true)
|
UserSession UserSession? // Nama instansi (Puskesmas, Sekolah, dll)
|
||||||
createdAt DateTime @default(now())
|
isActive Boolean @default(true)
|
||||||
updatedAt DateTime @updatedAt
|
lastLogin DateTime?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
deletedAt DateTime?
|
||||||
}
|
}
|
||||||
|
|
||||||
model Role {
|
model Role {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
name String @unique // ADMIN_DESA, ADMIN_KESEHATAN, ADMIN_SEKOLAH
|
||||||
|
description String?
|
||||||
|
permissions Json // Menyimpan permission dalam format JSON
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
deletedAt DateTime?
|
||||||
|
users User[]
|
||||||
|
|
||||||
|
@@map("roles")
|
||||||
|
}
|
||||||
|
|
||||||
|
model KodeOtp {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String
|
isActive Boolean @default(true)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
nomor String
|
||||||
isActive Boolean @default(true)
|
otp Int
|
||||||
User User[]
|
}
|
||||||
|
|
||||||
|
// Tabel untuk menyimpan permission
|
||||||
|
model Permission {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
name String @unique
|
||||||
|
description String?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@map("permissions")
|
||||||
|
}
|
||||||
|
|
||||||
|
model UserSession {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
token String
|
||||||
|
expires DateTime?
|
||||||
|
active Boolean @default(true)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @default(now()) @updatedAt
|
||||||
|
User User @relation(fields: [userId], references: [id])
|
||||||
|
userId String @unique
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================= DATA PENDIDIKAN ========================================= //
|
// ========================================= DATA PENDIDIKAN ========================================= //
|
||||||
|
|||||||
171
prisma/seed.ts
@@ -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";
|
||||||
@@ -52,8 +53,92 @@ import tempatKegiatan from "./data/pendidikan/pendidikan-non-formal/tempat-kegia
|
|||||||
import tujuanProgram2 from "./data/pendidikan/pendidikan-non-formal/tujuan-program2.json";
|
import tujuanProgram2 from "./data/pendidikan/pendidikan-non-formal/tujuan-program2.json";
|
||||||
import programUnggulan from "./data/pendidikan/program-pendidikan-anak/program-unggulan.json";
|
import programUnggulan from "./data/pendidikan/program-pendidikan-anak/program-unggulan.json";
|
||||||
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 users from "./data/user/users.json";
|
||||||
|
import fileStorage from "./data/file-storage.json";
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
|
// =========== USER & ROLE ===========
|
||||||
|
// In your seed.ts
|
||||||
|
// =========== ROLES ===========
|
||||||
|
console.log("🔄 Seeding roles...");
|
||||||
|
for (const r of roles) {
|
||||||
|
await prisma.role.upsert({
|
||||||
|
where: { id: r.id },
|
||||||
|
update: {
|
||||||
|
name: r.name,
|
||||||
|
description: r.description,
|
||||||
|
permissions: r.permissions,
|
||||||
|
isActive: r.isActive,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: r.id,
|
||||||
|
name: r.name,
|
||||||
|
description: r.description,
|
||||||
|
permissions: r.permissions,
|
||||||
|
isActive: r.isActive,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Roles seeded");
|
||||||
|
|
||||||
|
// =========== USERS ===========
|
||||||
|
console.log("🔄 Seeding users...");
|
||||||
|
for (const u of users) {
|
||||||
|
// First verify the role exists
|
||||||
|
const roleExists = await prisma.role.findUnique({
|
||||||
|
where: { id: u.roleId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!roleExists) {
|
||||||
|
console.error(`❌ Role with id ${u.roleId} not found for user ${u.nama}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.user.upsert({
|
||||||
|
where: { id: u.id },
|
||||||
|
update: {
|
||||||
|
username: u.nama,
|
||||||
|
nomor: u.nomor,
|
||||||
|
roleId: u.roleId,
|
||||||
|
isActive: u.isActive,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: u.id,
|
||||||
|
username: u.nama,
|
||||||
|
nomor: u.nomor,
|
||||||
|
roleId: u.roleId,
|
||||||
|
isActive: u.isActive,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Users seeded");
|
||||||
|
|
||||||
|
// =========== 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 ===========
|
||||||
@@ -63,11 +148,13 @@ import tujuanProgram from "./data/pendidikan/program-pendidikan-anak/tujuan-prog
|
|||||||
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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -77,18 +164,35 @@ import tujuanProgram from "./data/pendidikan/program-pendidikan-anak/tujuan-prog
|
|||||||
|
|
||||||
// =========== 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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -101,11 +205,13 @@ import tujuanProgram from "./data/pendidikan/program-pendidikan-anak/tujuan-prog
|
|||||||
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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -251,11 +357,8 @@ import tujuanProgram from "./data/pendidikan/program-pendidikan-anak/tujuan-prog
|
|||||||
|
|
||||||
// =========== 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,
|
||||||
@@ -273,8 +376,7 @@ import tujuanProgram from "./data/pendidikan/program-pendidikan-anak/tujuan-prog
|
|||||||
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,
|
||||||
@@ -501,29 +603,29 @@ import tujuanProgram from "./data/pendidikan/program-pendidikan-anak/tujuan-prog
|
|||||||
}
|
}
|
||||||
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({
|
||||||
@@ -792,12 +894,9 @@ import tujuanProgram from "./data/pendidikan/program-pendidikan-anak/tujuan-prog
|
|||||||
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,
|
||||||
@@ -807,7 +906,7 @@ import tujuanProgram from "./data/pendidikan/program-pendidikan-anak/tujuan-prog
|
|||||||
},
|
},
|
||||||
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,
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 275 KiB |
|
Before Width: | Height: | Size: 275 KiB |
|
Before Width: | Height: | Size: 275 KiB |
|
Before Width: | Height: | Size: 275 KiB |
|
Before Width: | Height: | Size: 275 KiB |
|
Before Width: | Height: | Size: 275 KiB |
BIN
public/chatbot-removebg-preview.png
Normal file
|
After Width: | Height: | Size: 303 KiB |
|
Before Width: | Height: | Size: 275 KiB After Width: | Height: | Size: 266 KiB |
|
Before Width: | Height: | Size: 275 KiB |
|
Before Width: | Height: | Size: 275 KiB |
|
Before Width: | Height: | Size: 275 KiB |
|
Before Width: | Height: | Size: 275 KiB |
|
Before Width: | Height: | Size: 275 KiB |
|
Before Width: | Height: | Size: 275 KiB |
|
Before Width: | Height: | Size: 275 KiB |
|
Before Width: | Height: | Size: 275 KiB |
|
Before Width: | Height: | Size: 275 KiB |
|
Before Width: | Height: | Size: 275 KiB |
|
Before Width: | Height: | Size: 275 KiB |
|
Before Width: | Height: | Size: 275 KiB |
|
Before Width: | Height: | Size: 275 KiB |
|
Before Width: | Height: | Size: 275 KiB |
|
Before Width: | Height: | Size: 275 KiB |
|
Before Width: | Height: | Size: 275 KiB |
|
Before Width: | Height: | Size: 275 KiB |
|
Before Width: | Height: | Size: 275 KiB |
|
Before Width: | Height: | Size: 275 KiB |
|
Before Width: | Height: | Size: 275 KiB |
|
Before Width: | Height: | Size: 275 KiB |
|
Before Width: | Height: | Size: 275 KiB |
|
Before Width: | Height: | Size: 275 KiB |
@@ -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}
|
||||||
|
|||||||
100
src/app/admin/(dashboard)/_com/iconMap.tsx
Normal 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} />
|
||||||
|
}
|
||||||
@@ -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 },
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -74,18 +74,18 @@ const berita = proxy({
|
|||||||
totalPages: 1,
|
totalPages: 1,
|
||||||
loading: false,
|
loading: false,
|
||||||
search: "",
|
search: "",
|
||||||
load: async (page = 1, limit = 10, search = "", kategori = "") => {
|
load: async (page = 1, limit = 10, search = "", kategori = "") => {
|
||||||
berita.findMany.loading = true; // ✅ Akses langsung via nama path
|
berita.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||||
berita.findMany.page = page;
|
berita.findMany.page = page;
|
||||||
berita.findMany.search = search;
|
berita.findMany.search = search;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const query: any = { page, limit };
|
const query: any = { page, limit };
|
||||||
if (search) query.search = search;
|
if (search) query.search = search;
|
||||||
if (kategori) query.kategori = kategori;
|
if (kategori) query.kategori = kategori;
|
||||||
|
|
||||||
const res = await ApiFetch.api.desa.berita["find-many"].get({ query });
|
const res = await ApiFetch.api.desa.berita["find-many"].get({ query });
|
||||||
|
|
||||||
if (res.status === 200 && res.data?.success) {
|
if (res.status === 200 && res.data?.success) {
|
||||||
berita.findMany.data = res.data.data ?? [];
|
berita.findMany.data = res.data.data ?? [];
|
||||||
berita.findMany.totalPages = res.data.totalPages ?? 1;
|
berita.findMany.totalPages = res.data.totalPages ?? 1;
|
||||||
@@ -368,11 +368,37 @@ const kategoriBerita = proxy({
|
|||||||
isActive: true;
|
isActive: true;
|
||||||
};
|
};
|
||||||
}>[],
|
}>[],
|
||||||
|
page: 1,
|
||||||
|
totalPages: 1,
|
||||||
loading: false,
|
loading: false,
|
||||||
async load() {
|
search: "",
|
||||||
const res = await ApiFetch.api.desa.kategoriberita["findMany"].get();
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
if (res.status === 200) {
|
kategoriBerita.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||||
kategoriBerita.findMany.data = res.data?.data ?? [];
|
kategoriBerita.findMany.page = page;
|
||||||
|
kategoriBerita.findMany.search = search;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.desa.kategoriberita[
|
||||||
|
"findMany"
|
||||||
|
].get({ query });
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
kategoriBerita.findMany.data = res.data.data ?? [];
|
||||||
|
kategoriBerita.findMany.totalPages =
|
||||||
|
res.data.totalPages ?? 1;
|
||||||
|
} else {
|
||||||
|
kategoriBerita.findMany.data = [];
|
||||||
|
kategoriBerita.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Gagal fetch kategori berita paginated:", err);
|
||||||
|
kategoriBerita.findMany.data = [];
|
||||||
|
kategoriBerita.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
kategoriBerita.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ const templateTelunjukSaktiDesaForm = z.object({
|
|||||||
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
|
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const templatePelayananPerizinanBerusaha = z.object({
|
const templatePelayananPerizinanBerusaha = z.object({
|
||||||
name: z.string().min(3, "Nama minimal 3 karakter"),
|
name: z.string().min(3, "Nama minimal 3 karakter"),
|
||||||
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
|
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
|
||||||
@@ -72,7 +71,6 @@ const pelayananPendudukNonPermanenForm = {
|
|||||||
deskripsi: "",
|
deskripsi: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const suratKeterangan = proxy({
|
const suratKeterangan = proxy({
|
||||||
create: {
|
create: {
|
||||||
form: { ...suratKeteranganForm },
|
form: { ...suratKeteranganForm },
|
||||||
@@ -113,16 +111,21 @@ const suratKeterangan = proxy({
|
|||||||
totalPages: 1,
|
totalPages: 1,
|
||||||
total: 0,
|
total: 0,
|
||||||
loading: false,
|
loading: false,
|
||||||
load: async (page = 1, limit = 10) => { // Change to arrow function
|
search: "",
|
||||||
suratKeterangan.findMany.loading = true; // Use the full path to access the property
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
|
// Change to arrow function
|
||||||
|
suratKeterangan.findMany.loading = true; // Use the full path to access the property
|
||||||
suratKeterangan.findMany.page = page;
|
suratKeterangan.findMany.page = page;
|
||||||
|
suratKeterangan.findMany.search = search;
|
||||||
try {
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
const res = await ApiFetch.api.desa.layanan.pelayanansuratketerangan[
|
const res = await ApiFetch.api.desa.layanan.pelayanansuratketerangan[
|
||||||
"find-many"
|
"find-many"
|
||||||
].get({
|
].get({
|
||||||
query: { page, limit },
|
query,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status === 200 && res.data?.success) {
|
if (res.status === 200 && res.data?.success) {
|
||||||
suratKeterangan.findMany.data = res.data.data || [];
|
suratKeterangan.findMany.data = res.data.data || [];
|
||||||
suratKeterangan.findMany.total = res.data.total || 0;
|
suratKeterangan.findMany.total = res.data.total || 0;
|
||||||
@@ -341,28 +344,34 @@ const pelayananTelunjukSaktiDesa = proxy({
|
|||||||
totalPages: 1,
|
totalPages: 1,
|
||||||
total: 0,
|
total: 0,
|
||||||
loading: false,
|
loading: false,
|
||||||
load: async (page = 1, limit = 10) => { // Change to arrow function
|
search: "",
|
||||||
pelayananTelunjukSaktiDesa.findMany.loading = true; // Use the full path to access the property
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
|
// Change to arrow function
|
||||||
|
pelayananTelunjukSaktiDesa.findMany.loading = true; // Use the full path to access the property
|
||||||
pelayananTelunjukSaktiDesa.findMany.page = page;
|
pelayananTelunjukSaktiDesa.findMany.page = page;
|
||||||
|
pelayananTelunjukSaktiDesa.findMany.search = search;
|
||||||
try {
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
const res = await ApiFetch.api.desa.layanan.pelayanantelunjuksaktidesa[
|
const res = await ApiFetch.api.desa.layanan.pelayanantelunjuksaktidesa[
|
||||||
"find-many"
|
"find-many"
|
||||||
].get({
|
].get({
|
||||||
query: { page, limit },
|
query,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status === 200 && res.data?.success) {
|
if (res.status === 200 && res.data?.success) {
|
||||||
pelayananTelunjukSaktiDesa.findMany.data = res.data.data || [];
|
pelayananTelunjukSaktiDesa.findMany.data = res.data.data || [];
|
||||||
pelayananTelunjukSaktiDesa.findMany.total = res.data.total || 0;
|
pelayananTelunjukSaktiDesa.findMany.total = res.data.total || 0;
|
||||||
pelayananTelunjukSaktiDesa.findMany.totalPages = res.data.totalPages || 1;
|
pelayananTelunjukSaktiDesa.findMany.totalPages =
|
||||||
|
res.data.totalPages || 1;
|
||||||
} else {
|
} else {
|
||||||
console.error("Failed to load telunjuk sakti desa:", res.data?.message);
|
console.error("Failed to load surat keterangan:", res.data?.message);
|
||||||
pelayananTelunjukSaktiDesa.findMany.data = [];
|
pelayananTelunjukSaktiDesa.findMany.data = [];
|
||||||
pelayananTelunjukSaktiDesa.findMany.total = 0;
|
suratKeterangan.findMany.total = 0;
|
||||||
pelayananTelunjukSaktiDesa.findMany.totalPages = 1;
|
suratKeterangan.findMany.totalPages = 1;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading telunjuk sakti desa:", error);
|
console.error("Error loading surat keterangan:", error);
|
||||||
pelayananTelunjukSaktiDesa.findMany.data = [];
|
pelayananTelunjukSaktiDesa.findMany.data = [];
|
||||||
pelayananTelunjukSaktiDesa.findMany.total = 0;
|
pelayananTelunjukSaktiDesa.findMany.total = 0;
|
||||||
pelayananTelunjukSaktiDesa.findMany.totalPages = 1;
|
pelayananTelunjukSaktiDesa.findMany.totalPages = 1;
|
||||||
@@ -410,7 +419,9 @@ const pelayananTelunjukSaktiDesa = proxy({
|
|||||||
);
|
);
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
toast.success(result.message || "Telunjuk Sakti Desa berhasil dihapus");
|
toast.success(
|
||||||
|
result.message || "Telunjuk Sakti Desa berhasil dihapus"
|
||||||
|
);
|
||||||
await pelayananTelunjukSaktiDesa.findMany.load(); // refresh list
|
await pelayananTelunjukSaktiDesa.findMany.load(); // refresh list
|
||||||
} else {
|
} else {
|
||||||
toast.error(result.message || "Gagal menghapus telunjuk sakti desa");
|
toast.error(result.message || "Gagal menghapus telunjuk sakti desa");
|
||||||
@@ -501,7 +512,9 @@ const pelayananTelunjukSaktiDesa = proxy({
|
|||||||
}
|
}
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
toast.success(result.message || "Telunjuk Sakti Desa berhasil diupdate");
|
toast.success(
|
||||||
|
result.message || "Telunjuk Sakti Desa berhasil diupdate"
|
||||||
|
);
|
||||||
await pelayananTelunjukSaktiDesa.findMany.load(); // refresh list
|
await pelayananTelunjukSaktiDesa.findMany.load(); // refresh list
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
@@ -522,7 +535,7 @@ const pelayananTelunjukSaktiDesa = proxy({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
const pelayananPerizinanBerusaha = proxy({
|
const pelayananPerizinanBerusaha = proxy({
|
||||||
findById: {
|
findById: {
|
||||||
@@ -596,9 +609,7 @@ const pelayananPerizinanBerusaha = proxy({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching pelayanan perizinan berusaha:", error);
|
console.error("Error fetching pelayanan perizinan berusaha:", error);
|
||||||
toast.error(
|
toast.error(
|
||||||
error instanceof Error
|
error instanceof Error ? error.message : "Gagal memuat data"
|
||||||
? error.message
|
|
||||||
: "Gagal memuat data"
|
|
||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -713,9 +724,7 @@ const pelayananPendudukNonPermanen = proxy({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching pelayanan penduduk non permanen:", error);
|
console.error("Error fetching pelayanan penduduk non permanen:", error);
|
||||||
toast.error(
|
toast.error(
|
||||||
error instanceof Error
|
error instanceof Error ? error.message : "Gagal memuat data"
|
||||||
? error.message
|
|
||||||
: "Gagal memuat data"
|
|
||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,16 +56,21 @@ const penghargaanState = proxy({
|
|||||||
totalPages: 1,
|
totalPages: 1,
|
||||||
total: 0,
|
total: 0,
|
||||||
loading: false,
|
loading: false,
|
||||||
load: async (page = 1, limit = 10) => { // Change to arrow function
|
search: "",
|
||||||
penghargaanState.findMany.loading = true; // Use the full path to access the property
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
|
// Change to arrow function
|
||||||
|
penghargaanState.findMany.loading = true; // Use the full path to access the property
|
||||||
penghargaanState.findMany.page = page;
|
penghargaanState.findMany.page = page;
|
||||||
|
penghargaanState.findMany.search = search;
|
||||||
try {
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
const res = await ApiFetch.api.desa.penghargaan[
|
const res = await ApiFetch.api.desa.penghargaan[
|
||||||
"find-many"
|
"find-many"
|
||||||
].get({
|
].get({
|
||||||
query: { page, limit },
|
query,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status === 200 && res.data?.success) {
|
if (res.status === 200 && res.data?.success) {
|
||||||
penghargaanState.findMany.data = res.data.data || [];
|
penghargaanState.findMany.data = res.data.data || [];
|
||||||
penghargaanState.findMany.total = res.data.total || 0;
|
penghargaanState.findMany.total = res.data.total || 0;
|
||||||
|
|||||||
@@ -55,11 +55,39 @@ const category = proxy({
|
|||||||
pengumumans: number;
|
pengumumans: number;
|
||||||
};
|
};
|
||||||
})[],
|
})[],
|
||||||
|
page: 1,
|
||||||
|
totalPages: 1,
|
||||||
|
total: 0,
|
||||||
loading: false,
|
loading: false,
|
||||||
async load() {
|
search: "",
|
||||||
const res = await ApiFetch.api.desa.kategoripengumuman["findMany"].get();
|
load: async (page = 1, limit = 10, search = "") => { // Change to arrow function
|
||||||
if (res.status === 200) {
|
category.findMany.loading = true; // Use the full path to access the property
|
||||||
category.findMany.data = res.data?.data ?? [];
|
category.findMany.page = page;
|
||||||
|
category.findMany.search = search;
|
||||||
|
try {
|
||||||
|
const res = await ApiFetch.api.desa.kategoripengumuman[
|
||||||
|
"findMany"
|
||||||
|
].get({
|
||||||
|
query: { page, limit },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
category.findMany.data = res.data.data || [];
|
||||||
|
category.findMany.total = res.data.total || 0;
|
||||||
|
category.findMany.totalPages = res.data.totalPages || 1;
|
||||||
|
} else {
|
||||||
|
console.error("Failed to load potensi desa:", res.data?.message);
|
||||||
|
category.findMany.data = [];
|
||||||
|
category.findMany.total = 0;
|
||||||
|
category.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading potensi desa:", error);
|
||||||
|
category.findMany.data = [];
|
||||||
|
category.findMany.total = 0;
|
||||||
|
category.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
category.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -56,9 +56,11 @@ const potensiDesa = proxy({
|
|||||||
totalPages: 1,
|
totalPages: 1,
|
||||||
total: 0,
|
total: 0,
|
||||||
loading: false,
|
loading: false,
|
||||||
load: async (page = 1, limit = 10) => { // Change to arrow function
|
search: "",
|
||||||
|
load: async (page = 1, limit = 10, search = "") => { // Change to arrow function
|
||||||
potensiDesa.findMany.loading = true; // Use the full path to access the property
|
potensiDesa.findMany.loading = true; // Use the full path to access the property
|
||||||
potensiDesa.findMany.page = page;
|
potensiDesa.findMany.page = page;
|
||||||
|
potensiDesa.findMany.search = search;
|
||||||
try {
|
try {
|
||||||
const res = await ApiFetch.api.desa.potensi[
|
const res = await ApiFetch.api.desa.potensi[
|
||||||
"find-many"
|
"find-many"
|
||||||
@@ -298,11 +300,34 @@ const kategoriPotensi = proxy({
|
|||||||
isActive: true;
|
isActive: true;
|
||||||
};
|
};
|
||||||
}>[],
|
}>[],
|
||||||
|
page: 1,
|
||||||
|
totalPages: 1,
|
||||||
loading: false,
|
loading: false,
|
||||||
async load() {
|
search: "",
|
||||||
const res = await ApiFetch.api.desa.kategoripotensi["findMany"].get();
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
if (res.status === 200) {
|
kategoriPotensi.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||||
kategoriPotensi.findMany.data = res.data?.data ?? [];
|
kategoriPotensi.findMany.page = page;
|
||||||
|
kategoriPotensi.findMany.search = search;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.desa.kategoripotensi["findMany"].get({ query });
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
kategoriPotensi.findMany.data = res.data.data ?? [];
|
||||||
|
kategoriPotensi.findMany.totalPages = res.data.totalPages ?? 1;
|
||||||
|
} else {
|
||||||
|
kategoriPotensi.findMany.data = [];
|
||||||
|
kategoriPotensi.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Gagal fetch kategori potensi paginated:", err);
|
||||||
|
kategoriPotensi.findMany.data = [];
|
||||||
|
kategoriPotensi.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
kategoriPotensi.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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<{
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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<{
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 };
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -67,10 +68,46 @@ const kegiatanDesa = proxy({
|
|||||||
};
|
};
|
||||||
}>
|
}>
|
||||||
> | null,
|
> | null,
|
||||||
async load() {
|
page: 1,
|
||||||
const res = await ApiFetch.api.lingkungan.kegiatandesa["find-many"].get();
|
totalPages: 1,
|
||||||
if (res.status === 200) {
|
total: 0,
|
||||||
kegiatanDesa.findMany.data = res.data?.data ?? [];
|
loading: false,
|
||||||
|
search: "",
|
||||||
|
load: async (page = 1, limit = 10, search = "", kategori = "") => {
|
||||||
|
// Change to arrow function
|
||||||
|
kegiatanDesa.findMany.loading = true; // Use the full path to access the property
|
||||||
|
kegiatanDesa.findMany.page = page;
|
||||||
|
kegiatanDesa.findMany.search = search;
|
||||||
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
if (kategori) query.kategori = kategori;
|
||||||
|
const res = await ApiFetch.api.lingkungan.kegiatandesa[
|
||||||
|
"find-many"
|
||||||
|
].get({
|
||||||
|
query,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
kegiatanDesa.findMany.data = res.data.data || [];
|
||||||
|
kegiatanDesa.findMany.total = res.data.total || 0;
|
||||||
|
kegiatanDesa.findMany.totalPages = res.data.totalPages || 1;
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
"Failed to load kegiatan desa:",
|
||||||
|
res.data?.message
|
||||||
|
);
|
||||||
|
kegiatanDesa.findMany.data = [];
|
||||||
|
kegiatanDesa.findMany.total = 0;
|
||||||
|
kegiatanDesa.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading kegiatan desa:", error);
|
||||||
|
kegiatanDesa.findMany.data = [];
|
||||||
|
kegiatanDesa.findMany.total = 0;
|
||||||
|
kegiatanDesa.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
kegiatanDesa.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -244,6 +281,35 @@ const kegiatanDesa = proxy({
|
|||||||
kegiatanDesa.edit.form = { ...defaultKegiatanDesaForm };
|
kegiatanDesa.edit.form = { ...defaultKegiatanDesaForm };
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
findFirst: {
|
||||||
|
data: null as Prisma.KegiatanDesaGetPayload<{
|
||||||
|
include: {
|
||||||
|
image: true;
|
||||||
|
kategoriKegiatan: true;
|
||||||
|
};
|
||||||
|
}> | null,
|
||||||
|
loading: false,
|
||||||
|
// findFirst.load()
|
||||||
|
async load(kategori?: string) {
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const res = await ApiFetch.api.lingkungan.kegiatandesa["find-first"].get({
|
||||||
|
query: kategori ? { kategori } : {},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
this.data = res.data.data || null;
|
||||||
|
} else {
|
||||||
|
this.data = null;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Gagal fetch kegiatan desa terbaru:", err);
|
||||||
|
this.data = null;
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// ========================================= KATEGORI kegiatan ========================================= //
|
// ========================================= KATEGORI kegiatan ========================================= //
|
||||||
@@ -269,9 +335,7 @@ const kategoriKegiatan = proxy({
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
kategoriKegiatan.create.loading = true;
|
kategoriKegiatan.create.loading = true;
|
||||||
const res = await ApiFetch.api.lingkungan.kategorikegiatan[
|
const res = await ApiFetch.api.lingkungan.kategorikegiatan["create"].post(kategoriKegiatan.create.form);
|
||||||
"create"
|
|
||||||
].post(kategoriKegiatan.create.form);
|
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
kategoriKegiatan.findMany.load();
|
kategoriKegiatan.findMany.load();
|
||||||
return toast.success("Data berhasil ditambahkan");
|
return toast.success("Data berhasil ditambahkan");
|
||||||
@@ -305,9 +369,7 @@ const kategoriKegiatan = proxy({
|
|||||||
}> | null,
|
}> | null,
|
||||||
async load(id: string) {
|
async load(id: string) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(
|
const res = await fetch(`/api/lingkungan/kategorikegiatan/${id}`);
|
||||||
`/api/lingkungan/kategorikegiatan/${id}`
|
|
||||||
);
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
kategoriKegiatan.findUnique.data = data.data ?? null;
|
kategoriKegiatan.findUnique.data = data.data ?? null;
|
||||||
@@ -367,15 +429,12 @@ const kategoriKegiatan = proxy({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(`/api/lingkungan/kategorikegiatan/${id}`, {
|
||||||
`/api/lingkungan/kategorikegiatan/${id}`,
|
method: "GET",
|
||||||
{
|
headers: {
|
||||||
method: "GET",
|
"Content-Type": "application/json",
|
||||||
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}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ const keteranganSampah = proxy({
|
|||||||
try {
|
try {
|
||||||
keteranganSampah.create.loading = true;
|
keteranganSampah.create.loading = true;
|
||||||
const res =
|
const res =
|
||||||
await ApiFetch.api.lingkungan.pengelolaansampah.keteranganbankterdekat[
|
await ApiFetch.api.lingkungan.keteranganbankterdekat[
|
||||||
"create"
|
"create"
|
||||||
].post(keteranganSampah.create.form);
|
].post(keteranganSampah.create.form);
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
@@ -291,14 +291,47 @@ const keteranganSampah = proxy({
|
|||||||
omit: { isActive: true };
|
omit: { isActive: true };
|
||||||
}>[]
|
}>[]
|
||||||
| null,
|
| null,
|
||||||
async load() {
|
page: 1,
|
||||||
const res = await ApiFetch.api.lingkungan.pengelolaansampah.keteranganbankterdekat[
|
totalPages: 1,
|
||||||
"find-many"
|
total: 0,
|
||||||
].get();
|
loading: false,
|
||||||
if (res.status === 200) {
|
search: "",
|
||||||
keteranganSampah.findMany.data = res.data?.data ?? [];
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
}
|
// Change to arrow function
|
||||||
},
|
keteranganSampah.findMany.loading = true; // Use the full path to access the property
|
||||||
|
keteranganSampah.findMany.page = page;
|
||||||
|
keteranganSampah.findMany.search = search;
|
||||||
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
const res = await ApiFetch.api.lingkungan.keteranganbankterdekat[
|
||||||
|
"find-many"
|
||||||
|
].get({
|
||||||
|
query,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
keteranganSampah.findMany.data = res.data.data || [];
|
||||||
|
keteranganSampah.findMany.total = res.data.total || 0;
|
||||||
|
keteranganSampah.findMany.totalPages = res.data.totalPages || 1;
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
"Failed to load keterangan bank sampah terdekat:",
|
||||||
|
res.data?.message
|
||||||
|
);
|
||||||
|
keteranganSampah.findMany.data = [];
|
||||||
|
keteranganSampah.findMany.total = 0;
|
||||||
|
keteranganSampah.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading keterangan bank sampah terdekat:", error);
|
||||||
|
keteranganSampah.findMany.data = [];
|
||||||
|
keteranganSampah.findMany.total = 0;
|
||||||
|
keteranganSampah.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
keteranganSampah.findMany.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
findUnique: {
|
findUnique: {
|
||||||
data: null as Prisma.KeteranganBankSampahTerdekatGetPayload<{
|
data: null as Prisma.KeteranganBankSampahTerdekatGetPayload<{
|
||||||
@@ -306,7 +339,7 @@ const keteranganSampah = proxy({
|
|||||||
}> | null,
|
}> | null,
|
||||||
async load(id: string) {
|
async load(id: string) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/lingkungan/pengelolaansampah/keteranganbankterdekat/${id}`);
|
const res = await fetch(`/api/lingkungan/keteranganbankterdekat/${id}`);
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
keteranganSampah.findUnique.data = data.data ?? null;
|
keteranganSampah.findUnique.data = data.data ?? null;
|
||||||
@@ -328,7 +361,7 @@ const keteranganSampah = proxy({
|
|||||||
try {
|
try {
|
||||||
keteranganSampah.delete.loading = true;
|
keteranganSampah.delete.loading = true;
|
||||||
|
|
||||||
const response = await fetch(`/api/lingkungan/pengelolaansampah/keteranganbankterdekat/del/${id}`, {
|
const response = await fetch(`/api/lingkungan/keteranganbankterdekat/del/${id}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -363,7 +396,7 @@ const keteranganSampah = proxy({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/lingkungan/pengelolaansampah/keteranganbankterdekat/${id}`, {
|
const response = await fetch(`/api/lingkungan/keteranganbankterdekat/${id}`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -408,7 +441,7 @@ const keteranganSampah = proxy({
|
|||||||
try {
|
try {
|
||||||
keteranganSampah.edit.loading = true;
|
keteranganSampah.edit.loading = true;
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`/api/lingkungan/pengelolaansampah/keteranganbankterdekat/${this.id}`,
|
`/api/lingkungan/keteranganbankterdekat/${this.id}`,
|
||||||
{
|
{
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
/* 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";
|
||||||
import { proxy } from "valtio";
|
import { proxy } from "valtio";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
// ========================================= BEASISWA PENDAFTAR ========================================= //
|
||||||
|
|
||||||
const templateBeasiswaPendaftar = z.object({
|
const templateBeasiswaPendaftar = z.object({
|
||||||
namaLengkap: z.string().min(1, "Nama harus diisi"),
|
namaLengkap: z.string().min(1, "Nama harus diisi"),
|
||||||
nik: z.string().min(1, "NIK harus diisi"),
|
nik: z.string().min(1, "NIK harus diisi"),
|
||||||
@@ -76,13 +79,34 @@ const beasiswaPendaftar = proxy({
|
|||||||
isActive: true;
|
isActive: true;
|
||||||
};
|
};
|
||||||
}>[],
|
}>[],
|
||||||
|
page: 1,
|
||||||
|
totalPages: 1,
|
||||||
loading: false,
|
loading: false,
|
||||||
async load() {
|
search: "",
|
||||||
const res = await ApiFetch.api.pendidikan.beasiswa.beasiswapendaftar[
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
"findMany"
|
beasiswaPendaftar.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||||
].get();
|
beasiswaPendaftar.findMany.page = page;
|
||||||
if (res.status === 200) {
|
beasiswaPendaftar.findMany.search = search;
|
||||||
beasiswaPendaftar.findMany.data = res.data?.data ?? [];
|
|
||||||
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.pendidikan.beasiswa.beasiswapendaftar["findMany"].get({ query });
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
beasiswaPendaftar.findMany.data = res.data.data ?? [];
|
||||||
|
beasiswaPendaftar.findMany.totalPages = res.data.totalPages ?? 1;
|
||||||
|
} else {
|
||||||
|
beasiswaPendaftar.findMany.data = [];
|
||||||
|
beasiswaPendaftar.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Gagal fetch beasiswa pendaftar paginated:", err);
|
||||||
|
beasiswaPendaftar.findMany.data = [];
|
||||||
|
beasiswaPendaftar.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
beasiswaPendaftar.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -275,8 +299,260 @@ const beasiswaPendaftar = proxy({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ========================================= KEUNGGULAN PROGRAM ========================================= //
|
||||||
|
const templateKeunggulanProgram = z.object({
|
||||||
|
judul: z.string().min(1, "Judul harus diisi"),
|
||||||
|
deskripsi: z.string().min(1, "Deskripsi harus diisi"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultKeunggulanProgram = {
|
||||||
|
judul: "",
|
||||||
|
deskripsi: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const keunggulanProgram = proxy({
|
||||||
|
create: {
|
||||||
|
form: { ...defaultKeunggulanProgram },
|
||||||
|
loading: false,
|
||||||
|
async create() {
|
||||||
|
const cek = templateKeunggulanProgram.safeParse(
|
||||||
|
keunggulanProgram.create.form
|
||||||
|
);
|
||||||
|
if (!cek.success) {
|
||||||
|
const err = `[${cek.error.issues
|
||||||
|
.map((v) => `${v.path.join(".")}`)
|
||||||
|
.join("\n")}] required`;
|
||||||
|
return toast.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
keunggulanProgram.create.loading = true;
|
||||||
|
const res = await ApiFetch.api.pendidikan.beasiswa.keunggulanprogram[
|
||||||
|
"create"
|
||||||
|
].post(keunggulanProgram.create.form);
|
||||||
|
if (res.status === 200) {
|
||||||
|
keunggulanProgram.findMany.load();
|
||||||
|
return toast.success("Data Berhasil Dibuat, Silahkan Menunggu Konfirmasi dari Admin di WhatsApp");
|
||||||
|
}
|
||||||
|
console.log(res);
|
||||||
|
return toast.error("failed create");
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
return toast.error("failed create");
|
||||||
|
} finally {
|
||||||
|
keunggulanProgram.create.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
findMany: {
|
||||||
|
data: [] as Prisma.KeunggulanProgramGetPayload<{
|
||||||
|
omit: {
|
||||||
|
isActive: true;
|
||||||
|
};
|
||||||
|
}>[],
|
||||||
|
page: 1,
|
||||||
|
totalPages: 1,
|
||||||
|
loading: false,
|
||||||
|
search: "",
|
||||||
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
|
keunggulanProgram.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||||
|
keunggulanProgram.findMany.page = page;
|
||||||
|
keunggulanProgram.findMany.search = search;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.pendidikan.beasiswa.keunggulanprogram["findMany"].get({ query });
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
keunggulanProgram.findMany.data = res.data.data ?? [];
|
||||||
|
keunggulanProgram.findMany.totalPages = res.data.totalPages ?? 1;
|
||||||
|
} else {
|
||||||
|
keunggulanProgram.findMany.data = [];
|
||||||
|
keunggulanProgram.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Gagal fetch keunggulan program paginated:", err);
|
||||||
|
keunggulanProgram.findMany.data = [];
|
||||||
|
keunggulanProgram.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
keunggulanProgram.findMany.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
findUnique: {
|
||||||
|
data: null as Prisma.KeunggulanProgramGetPayload<{
|
||||||
|
omit: {
|
||||||
|
isActive: true;
|
||||||
|
};
|
||||||
|
}> | null,
|
||||||
|
loading: false,
|
||||||
|
async load(id: string) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`/api/pendidikan/beasiswa/keunggulanprogram/${id}`
|
||||||
|
);
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
keunggulanProgram.findUnique.data = data.data ?? null;
|
||||||
|
} else {
|
||||||
|
console.error("Failed to fetch data", res.status, res.statusText);
|
||||||
|
keunggulanProgram.findUnique.data = null;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching data:", error);
|
||||||
|
keunggulanProgram.findUnique.data = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
delete: {
|
||||||
|
loading: false,
|
||||||
|
async delete(id: string) {
|
||||||
|
if (!id) return toast.warn("ID tidak valid");
|
||||||
|
|
||||||
|
try {
|
||||||
|
keunggulanProgram.delete.loading = true;
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/pendidikan/beasiswa/keunggulanprogram/del/${id}`,
|
||||||
|
{
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (response.ok && result?.success) {
|
||||||
|
toast.success(result.message || "Keunggulan Program berhasil dihapus");
|
||||||
|
await keunggulanProgram.findMany.load(); // refresh list
|
||||||
|
} else {
|
||||||
|
toast.error(result?.message || "Gagal menghapus keunggulan program");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Gagal delete:", error);
|
||||||
|
toast.error("Terjadi kesalahan saat menghapus keunggulan program");
|
||||||
|
} finally {
|
||||||
|
keunggulanProgram.delete.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
id: "",
|
||||||
|
form: { ...defaultKeunggulanProgram },
|
||||||
|
loading: false,
|
||||||
|
async load(id: string) {
|
||||||
|
if (!id) {
|
||||||
|
toast.warn("ID tidak valid");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/pendidikan/beasiswa/keunggulanprogram/${id}`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result?.success) {
|
||||||
|
const data = result.data;
|
||||||
|
this.id = data.id;
|
||||||
|
this.form = {
|
||||||
|
judul: data.judul,
|
||||||
|
deskripsi: data.deskripsi,
|
||||||
|
};
|
||||||
|
return data; // Return the loaded data
|
||||||
|
} else {
|
||||||
|
throw new Error(result?.message || "Gagal memuat data");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading keunggulan program:", error);
|
||||||
|
toast.error(
|
||||||
|
error instanceof Error ? error.message : "Gagal memuat data"
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async update() {
|
||||||
|
const cek = templateKeunggulanProgram.safeParse(
|
||||||
|
keunggulanProgram.update.form
|
||||||
|
);
|
||||||
|
if (!cek.success) {
|
||||||
|
const err = `[${cek.error.issues
|
||||||
|
.map((v) => `${v.path.join(".")}`)
|
||||||
|
.join("\n")}] required`;
|
||||||
|
toast.error(err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
keunggulanProgram.update.loading = true;
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/pendidikan/beasiswa/keunggulanprogram/${this.id}`,
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
judul: this.form.judul,
|
||||||
|
deskripsi: this.form.deskripsi,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
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 keunggulan program");
|
||||||
|
await keunggulanProgram.findMany.load(); // refresh list
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(result.message || "Gagal update keunggulan program");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating keunggulan program:", error);
|
||||||
|
toast.error(
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Terjadi kesalahan saat update keunggulan program"
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
keunggulanProgram.update.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reset() {
|
||||||
|
keunggulanProgram.update.id = "";
|
||||||
|
keunggulanProgram.update.form = { ...defaultKeunggulanProgram };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
const beasiswaDesaState = proxy({
|
const beasiswaDesaState = proxy({
|
||||||
beasiswaPendaftar,
|
beasiswaPendaftar,
|
||||||
|
keunggulanProgram
|
||||||
});
|
});
|
||||||
|
|
||||||
export default beasiswaDesaState;
|
export default beasiswaDesaState;
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -51,13 +52,46 @@ const jenjangPendidikan = proxy({
|
|||||||
id: string;
|
id: string;
|
||||||
nama: string;
|
nama: string;
|
||||||
}> | null,
|
}> | null,
|
||||||
async load() {
|
page: 1,
|
||||||
const res =
|
totalPages: 1,
|
||||||
await ApiFetch.api.pendidikan.infosekolahpaud.jenjangpendidikan[
|
total: 0,
|
||||||
"find-many"
|
loading: false,
|
||||||
].get();
|
search: "",
|
||||||
if (res.status === 200) {
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
jenjangPendidikan.findMany.data = res.data?.data ?? [];
|
// Change to arrow function
|
||||||
|
jenjangPendidikan.findMany.loading = true; // Use the full path to access the property
|
||||||
|
jenjangPendidikan.findMany.page = page;
|
||||||
|
jenjangPendidikan.findMany.search = search;
|
||||||
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
const res =
|
||||||
|
await ApiFetch.api.pendidikan.infosekolahpaud.jenjangpendidikan[
|
||||||
|
"find-many"
|
||||||
|
].get({
|
||||||
|
query,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
jenjangPendidikan.findMany.data = res.data.data || [];
|
||||||
|
jenjangPendidikan.findMany.total = res.data.total || 0;
|
||||||
|
jenjangPendidikan.findMany.totalPages = res.data.totalPages || 1;
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
"Failed to load jenjang pendidikan:",
|
||||||
|
res.data?.message
|
||||||
|
);
|
||||||
|
jenjangPendidikan.findMany.data = [];
|
||||||
|
jenjangPendidikan.findMany.total = 0;
|
||||||
|
jenjangPendidikan.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading jenjang pendidikan:", error);
|
||||||
|
jenjangPendidikan.findMany.data = [];
|
||||||
|
jenjangPendidikan.findMany.total = 0;
|
||||||
|
jenjangPendidikan.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
jenjangPendidikan.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -299,18 +333,64 @@ const lembagaPendidikan = proxy({
|
|||||||
Prisma.LembagaGetPayload<{
|
Prisma.LembagaGetPayload<{
|
||||||
include: {
|
include: {
|
||||||
jenjangPendidikan: true;
|
jenjangPendidikan: true;
|
||||||
siswa: true;
|
|
||||||
pengajar: true;
|
|
||||||
};
|
};
|
||||||
}>
|
}> & {
|
||||||
|
siswa?: [];
|
||||||
|
pengajar?: [];
|
||||||
|
}
|
||||||
> | null,
|
> | null,
|
||||||
async load() {
|
page: 1,
|
||||||
const res =
|
totalPages: 1,
|
||||||
await ApiFetch.api.pendidikan.infosekolahpaud.lembagapendidikan[
|
total: 0,
|
||||||
"find-many"
|
loading: false,
|
||||||
].get();
|
search: "",
|
||||||
if (res.status === 200) {
|
load: async (page = 1, limit = 10, search = "", jenjangPendidikan = "") => {
|
||||||
lembagaPendidikan.findMany.data = res.data?.data ?? [];
|
lembagaPendidikan.findMany.loading = true;
|
||||||
|
lembagaPendidikan.findMany.page = page;
|
||||||
|
lembagaPendidikan.findMany.search = search;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const query: any = {
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
...(search && { search }),
|
||||||
|
...(jenjangPendidikan && { jenjangPendidikanId: jenjangPendidikan })
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('Fetching lembaga with query:', query);
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.pendidikan.infosekolahpaud.lembagapendidikan["find-many"].get({ query });
|
||||||
|
|
||||||
|
console.log('API Response:', res);
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
const data = Array.isArray(res.data.data) ? res.data.data : [];
|
||||||
|
const total = typeof res.data.total === 'number' ? res.data.total : 0;
|
||||||
|
const totalPages = typeof res.data.totalPages === 'number' ? res.data.totalPages : 1;
|
||||||
|
|
||||||
|
lembagaPendidikan.findMany.data = data;
|
||||||
|
lembagaPendidikan.findMany.total = total;
|
||||||
|
lembagaPendidikan.findMany.totalPages = totalPages;
|
||||||
|
|
||||||
|
console.log('Successfully loaded lembaga data:', {
|
||||||
|
count: data.length,
|
||||||
|
total,
|
||||||
|
totalPages
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
"Failed to load lembaga pendidikan:",
|
||||||
|
res.data?.message || 'No error message provided'
|
||||||
|
);
|
||||||
|
throw new Error(res.data?.message || 'Failed to load lembaga pendidikan');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading lembaga pendidikan:", error);
|
||||||
|
lembagaPendidikan.findMany.data = [];
|
||||||
|
lembagaPendidikan.findMany.total = 0;
|
||||||
|
lembagaPendidikan.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
lembagaPendidikan.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -554,16 +634,55 @@ const siswa = proxy({
|
|||||||
data: null as Array<
|
data: null as Array<
|
||||||
Prisma.SiswaGetPayload<{
|
Prisma.SiswaGetPayload<{
|
||||||
include: {
|
include: {
|
||||||
lembaga: true;
|
lembaga: {
|
||||||
|
include: {
|
||||||
|
jenjangPendidikan: true;
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}>
|
}>
|
||||||
> | null,
|
> | null,
|
||||||
async load() {
|
page: 1,
|
||||||
const res = await ApiFetch.api.pendidikan.infosekolahpaud.siswa[
|
totalPages: 1,
|
||||||
"find-many"
|
total: 0,
|
||||||
].get();
|
loading: false,
|
||||||
if (res.status === 200) {
|
search: "",
|
||||||
siswa.findMany.data = res.data?.data ?? [];
|
jenjangPendidikan: "",
|
||||||
|
load: async (page = 1, limit = 10, search = "", jenjangPendidikan = "") => {
|
||||||
|
siswa.findMany.loading = true;
|
||||||
|
siswa.findMany.page = page;
|
||||||
|
siswa.findMany.search = search;
|
||||||
|
siswa.findMany.jenjangPendidikan = jenjangPendidikan;
|
||||||
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
if (jenjangPendidikan) query.jenjangPendidikanName = jenjangPendidikan;
|
||||||
|
const res = await ApiFetch.api.pendidikan.infosekolahpaud.siswa[
|
||||||
|
"find-many"
|
||||||
|
].get({
|
||||||
|
query,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
siswa.findMany.data = res.data.data || [];
|
||||||
|
siswa.findMany.total = res.data.total || 0;
|
||||||
|
siswa.findMany.totalPages = res.data.totalPages || 1;
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
"Failed to load siswa:",
|
||||||
|
res.data?.message
|
||||||
|
);
|
||||||
|
siswa.findMany.data = [];
|
||||||
|
siswa.findMany.total = 0;
|
||||||
|
siswa.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading siswa:", error);
|
||||||
|
siswa.findMany.data = [];
|
||||||
|
siswa.findMany.total = 0;
|
||||||
|
siswa.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
siswa.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -794,16 +913,56 @@ const pengajar = proxy({
|
|||||||
data: null as Array<
|
data: null as Array<
|
||||||
Prisma.PengajarGetPayload<{
|
Prisma.PengajarGetPayload<{
|
||||||
include: {
|
include: {
|
||||||
lembaga: true;
|
lembaga: {
|
||||||
|
include: {
|
||||||
|
jenjangPendidikan: true
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}>
|
}>
|
||||||
> | null,
|
> | null,
|
||||||
async load() {
|
page: 1,
|
||||||
const res = await ApiFetch.api.pendidikan.infosekolahpaud.pengajar[
|
totalPages: 1,
|
||||||
"find-many"
|
total: 0,
|
||||||
].get();
|
loading: false,
|
||||||
if (res.status === 200) {
|
search: "",
|
||||||
pengajar.findMany.data = res.data?.data ?? [];
|
jenjangPendidikan: "",
|
||||||
|
load: async (page = 1, limit = 10, search = "", jenjangPendidikan = "") => {
|
||||||
|
// Change to arrow function
|
||||||
|
pengajar.findMany.loading = true; // Use the full path to access the property
|
||||||
|
pengajar.findMany.page = page;
|
||||||
|
pengajar.findMany.search = search;
|
||||||
|
pengajar.findMany.jenjangPendidikan = jenjangPendidikan;
|
||||||
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
if (jenjangPendidikan) query.jenjangPendidikanId = jenjangPendidikan;
|
||||||
|
const res = await ApiFetch.api.pendidikan.infosekolahpaud.pengajar[
|
||||||
|
"find-many"
|
||||||
|
].get({
|
||||||
|
query,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
pengajar.findMany.data = res.data.data || [];
|
||||||
|
pengajar.findMany.total = res.data.total || 0;
|
||||||
|
pengajar.findMany.totalPages = res.data.totalPages || 1;
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
"Failed to load pengajar:",
|
||||||
|
res.data?.message
|
||||||
|
);
|
||||||
|
pengajar.findMany.data = [];
|
||||||
|
pengajar.findMany.total = 0;
|
||||||
|
pengajar.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading pengajar:", error);
|
||||||
|
pengajar.findMany.data = [];
|
||||||
|
pengajar.findMany.total = 0;
|
||||||
|
pengajar.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
pengajar.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -815,7 +974,9 @@ const pengajar = proxy({
|
|||||||
}> | null,
|
}> | null,
|
||||||
async load(id: string) {
|
async load(id: string) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/pendidikan/infosekolahpaud/pengajar/${id}`);
|
const res = await fetch(
|
||||||
|
`/api/pendidikan/infosekolahpaud/pengajar/${id}`
|
||||||
|
);
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
pengajar.findUnique.data = data.data ?? null;
|
pengajar.findUnique.data = data.data ?? null;
|
||||||
@@ -948,7 +1109,8 @@ const pengajar = proxy({
|
|||||||
result
|
result
|
||||||
);
|
);
|
||||||
throw new Error(
|
throw new Error(
|
||||||
result?.message || `Gagal mengupdate pengajar (${response.status})`
|
result?.message ||
|
||||||
|
`Gagal mengupdate pengajar (${response.status})`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,23 +55,46 @@ const dataPerpustakaan = proxy({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
findMany: {
|
findMany: {
|
||||||
data: [] as Prisma.DataPerpustakaanGetPayload<{
|
data: null as
|
||||||
include: {
|
| Prisma.DataPerpustakaanGetPayload<{
|
||||||
kategori: true;
|
include: {
|
||||||
image: true;
|
image: true;
|
||||||
};
|
kategori: true;
|
||||||
}>[],
|
};
|
||||||
loading: false,
|
}>[]
|
||||||
async load() {
|
| null,
|
||||||
const res =
|
page: 1,
|
||||||
await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan[
|
totalPages: 1,
|
||||||
"findMany"
|
loading: false,
|
||||||
].get();
|
search: "",
|
||||||
if (res.status === 200) {
|
load: async (page = 1, limit = 10, search = "", kategori = "") => {
|
||||||
dataPerpustakaan.findMany.data = res.data?.data ?? [];
|
dataPerpustakaan.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||||
}
|
dataPerpustakaan.findMany.page = page;
|
||||||
|
dataPerpustakaan.findMany.search = search;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
if (kategori) query.kategori = kategori;
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan["findMany"].get({ query });
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
dataPerpustakaan.findMany.data = res.data.data ?? [];
|
||||||
|
dataPerpustakaan.findMany.totalPages = res.data.totalPages ?? 1;
|
||||||
|
} else {
|
||||||
|
dataPerpustakaan.findMany.data = [];
|
||||||
|
dataPerpustakaan.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Gagal fetch data perpustakaan paginated:", err);
|
||||||
|
dataPerpustakaan.findMany.data = [];
|
||||||
|
dataPerpustakaan.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
dataPerpustakaan.findMany.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
|
||||||
findUnique: {
|
findUnique: {
|
||||||
data: null as Prisma.DataPerpustakaanGetPayload<{
|
data: null as Prisma.DataPerpustakaanGetPayload<{
|
||||||
include: {
|
include: {
|
||||||
@@ -293,14 +317,34 @@ const kategoriBuku = proxy({
|
|||||||
isActive: true;
|
isActive: true;
|
||||||
};
|
};
|
||||||
}>[],
|
}>[],
|
||||||
|
page: 1,
|
||||||
|
totalPages: 1,
|
||||||
loading: false,
|
loading: false,
|
||||||
async load() {
|
search: "",
|
||||||
const res =
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
await ApiFetch.api.pendidikan.perpustakaandigital.kategoribuku[
|
kategoriBuku.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||||
"findMany"
|
kategoriBuku.findMany.page = page;
|
||||||
].get();
|
kategoriBuku.findMany.search = search;
|
||||||
if (res.status === 200) {
|
|
||||||
kategoriBuku.findMany.data = res.data?.data ?? [];
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.pendidikan.perpustakaandigital.kategoribuku["findMany"].get({ query });
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
kategoriBuku.findMany.data = res.data.data ?? [];
|
||||||
|
kategoriBuku.findMany.totalPages = res.data.totalPages ?? 1;
|
||||||
|
} else {
|
||||||
|
kategoriBuku.findMany.data = [];
|
||||||
|
kategoriBuku.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Gagal fetch data kategori buku paginated:", err);
|
||||||
|
kategoriBuku.findMany.data = [];
|
||||||
|
kategoriBuku.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
kategoriBuku.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -1,124 +1,43 @@
|
|||||||
import { proxy } from 'valtio'
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { toast } from 'react-toastify'
|
import { proxy } from "valtio";
|
||||||
import ApiFetch from '@/lib/api-fetch'
|
import { toast } from "react-toastify";
|
||||||
import { Prisma } from '@prisma/client'
|
import ApiFetch from "@/lib/api-fetch";
|
||||||
import { z } from 'zod'
|
import { Prisma } from "@prisma/client";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
// 1. Validasi Zod
|
// State Valtio
|
||||||
const userSchema = z.object({
|
|
||||||
nama: z.string().min(1, 'Nama harus diisi'),
|
|
||||||
email: z.string().email('Email tidak valid'),
|
|
||||||
password: z.string().min(6, 'Password minimal 6 karakter'),
|
|
||||||
roleId: z.string().optional(),
|
|
||||||
})
|
|
||||||
|
|
||||||
const defaultForm = { nama: '', email: '', password: '', roleId: '' }
|
|
||||||
|
|
||||||
// 2. State Valtio
|
|
||||||
const userState = proxy({
|
const userState = proxy({
|
||||||
// Register
|
|
||||||
register: {
|
|
||||||
form: { ...defaultForm },
|
|
||||||
loading: false,
|
|
||||||
async submit() {
|
|
||||||
const valid = userSchema.omit({ roleId: true }).safeParse(userState.register.form)
|
|
||||||
if (!valid.success) {
|
|
||||||
const err = valid.error.issues.map(i => i.message).join(', ')
|
|
||||||
return toast.error(err)
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
userState.register.loading = true
|
|
||||||
const res = await ApiFetch.api.user.register.post(userState.register.form)
|
|
||||||
if (res.status === 200) {
|
|
||||||
toast.success('Registrasi berhasil, silakan login')
|
|
||||||
userState.register.form = { ...defaultForm } // reset
|
|
||||||
} else {
|
|
||||||
toast.error(res.data?.message || 'Gagal registrasi')
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
toast.error('Terjadi kesalahan saat registrasi')
|
|
||||||
} finally {
|
|
||||||
userState.register.loading = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// Login
|
|
||||||
login: {
|
|
||||||
form: { email: '', password: '' },
|
|
||||||
loading: false,
|
|
||||||
async submit() {
|
|
||||||
try {
|
|
||||||
userState.login.loading = true
|
|
||||||
const res = await ApiFetch.api.user.login.post(userState.login.form)
|
|
||||||
if (res.status === 200) {
|
|
||||||
toast.success('Login berhasil')
|
|
||||||
const token = res.data?.data?.token
|
|
||||||
if (typeof token === 'string') {
|
|
||||||
localStorage.setItem('token', token)
|
|
||||||
// Optional: simpan user role untuk otorisasi
|
|
||||||
const user = res.data?.data?.user
|
|
||||||
localStorage.setItem('user', JSON.stringify(user))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
toast.error(res.data?.message || 'Login gagal')
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
toast.error('Terjadi kesalahan saat login')
|
|
||||||
} finally {
|
|
||||||
userState.login.loading = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// CRUD User (untuk admin)
|
|
||||||
create: {
|
|
||||||
form: { ...defaultForm },
|
|
||||||
loading: false,
|
|
||||||
async create(isAdmin = false) {
|
|
||||||
const valid = userSchema.safeParse(userState.create.form)
|
|
||||||
if (!valid.success) {
|
|
||||||
const err = valid.error.issues.map(i => i.message).join(', ')
|
|
||||||
return toast.error(err)
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
userState.create.loading = true
|
|
||||||
const endpoint = isAdmin ? 'create' : 'register'
|
|
||||||
const res = await ApiFetch.api.user[endpoint].post(userState.create.form)
|
|
||||||
if (res.status === 200) {
|
|
||||||
toast.success('User berhasil dibuat')
|
|
||||||
userState.findMany.load() // refresh list
|
|
||||||
userState.create.form = { ...defaultForm } // reset form
|
|
||||||
} else {
|
|
||||||
toast.error(res.data?.message || 'Gagal membuat user')
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
toast.error('Gagal membuat user')
|
|
||||||
} finally {
|
|
||||||
userState.create.loading = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// Find Many
|
// Find Many
|
||||||
findMany: {
|
findMany: {
|
||||||
data: [] as Prisma.UserGetPayload<{ include: { role: true } }>[],
|
data: [] as Prisma.UserGetPayload<{ include: { role: true } }>[],
|
||||||
|
page: 1,
|
||||||
|
totalPages: 1,
|
||||||
loading: false,
|
loading: false,
|
||||||
async load() {
|
search: "",
|
||||||
this.loading = true
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
|
userState.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||||
|
userState.findMany.page = page;
|
||||||
|
userState.findMany.search = search;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await ApiFetch.api.user.findMany.get()
|
const query: any = { page, limit };
|
||||||
if (res.status === 200) {
|
if (search) query.search = search;
|
||||||
this.data = res.data?.data || []
|
|
||||||
|
const res = await ApiFetch.api.user["findMany"].get({ query });
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
userState.findMany.data = res.data.data ?? [];
|
||||||
|
userState.findMany.totalPages = res.data.totalPages ?? 1;
|
||||||
|
} else {
|
||||||
|
userState.findMany.data = [];
|
||||||
|
userState.findMany.totalPages = 1;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (err) {
|
||||||
console.error(e)
|
console.error("Gagal fetch user paginated:", err);
|
||||||
toast.error('Gagal muat data user')
|
userState.findMany.data = [];
|
||||||
|
userState.findMany.totalPages = 1;
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false
|
userState.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -128,71 +47,20 @@ const userState = proxy({
|
|||||||
data: null as Prisma.UserGetPayload<{ include: { role: true } }> | null,
|
data: null as Prisma.UserGetPayload<{ include: { role: true } }> | null,
|
||||||
loading: false,
|
loading: false,
|
||||||
async load(id: string) {
|
async load(id: string) {
|
||||||
this.loading = true
|
this.loading = true;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/user/findUnique/${id}`)
|
const res = await fetch(`/api/user/findUnique/${id}`);
|
||||||
const data = await res.json()
|
const data = await res.json();
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
this.data = data.data
|
this.data = data.data;
|
||||||
} else {
|
} else {
|
||||||
toast.error(data.message)
|
toast.error(data.message);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e);
|
||||||
toast.error('Gagal ambil data user')
|
toast.error("Gagal ambil data user");
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false
|
this.loading = false;
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// Update
|
|
||||||
update: {
|
|
||||||
id: '',
|
|
||||||
form: { ...defaultForm },
|
|
||||||
loading: false,
|
|
||||||
async load(id: string) {
|
|
||||||
this.loading = true
|
|
||||||
try {
|
|
||||||
const res = await fetch(`/api/user/findUnique/${id}`)
|
|
||||||
const data = await res.json()
|
|
||||||
if (res.status === 200) {
|
|
||||||
const user = data.data
|
|
||||||
this.id = user.id
|
|
||||||
this.form = {
|
|
||||||
nama: user.nama,
|
|
||||||
email: user.email,
|
|
||||||
password: '', // jangan kirim password lama
|
|
||||||
roleId: user.roleId,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
toast.error('Gagal muat data')
|
|
||||||
} finally {
|
|
||||||
this.loading = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async submit() {
|
|
||||||
this.loading = true
|
|
||||||
try {
|
|
||||||
const res = await fetch(`/api/user/update/${this.id}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(this.form),
|
|
||||||
})
|
|
||||||
const data = await res.json()
|
|
||||||
if (res.status === 200) {
|
|
||||||
toast.success('Update berhasil')
|
|
||||||
userState.findMany.load()
|
|
||||||
} else {
|
|
||||||
toast.error(data.message || 'Gagal update')
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
toast.error('Gagal update user')
|
|
||||||
} finally {
|
|
||||||
this.loading = false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -201,35 +69,63 @@ const userState = proxy({
|
|||||||
delete: {
|
delete: {
|
||||||
loading: false,
|
loading: false,
|
||||||
async submit(id: string) {
|
async submit(id: string) {
|
||||||
this.loading = true
|
this.loading = true;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/user/del/${id}`, {
|
const res = await fetch(`/api/user/del/${id}`, {
|
||||||
method: 'PUT',
|
method: "PUT",
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { "Content-Type": "application/json" },
|
||||||
})
|
});
|
||||||
const data = await res.json()
|
const data = await res.json();
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
toast.success('User dinonaktifkan')
|
toast.success("User dinonaktifkan");
|
||||||
userState.findMany.load()
|
userState.findMany.load();
|
||||||
} else {
|
} else {
|
||||||
toast.error(data.message || 'Gagal hapus')
|
toast.error(data.message || "Gagal hapus");
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e);
|
||||||
toast.error('Gagal hapus user')
|
toast.error("Gagal hapus user");
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false
|
this.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
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({
|
||||||
name: z.string().min(1, "Nama harus diisi"),
|
name: z.string().min(1, "Nama harus diisi"),
|
||||||
|
permissions: z.array(z.string()).min(1, "Permission harus diisi"),
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultRole = {
|
const defaultRole = {
|
||||||
name: "",
|
name: "",
|
||||||
|
permissions: [] as string[],
|
||||||
};
|
};
|
||||||
|
|
||||||
const roleState = proxy({
|
const roleState = proxy({
|
||||||
@@ -247,10 +143,9 @@ const roleState = proxy({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
roleState.create.loading = true;
|
roleState.create.loading = true;
|
||||||
const res =
|
const res = await ApiFetch.api.role["create"].post(
|
||||||
await ApiFetch.api.role[
|
roleState.create.form
|
||||||
"create"
|
);
|
||||||
].post(roleState.create.form);
|
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
roleState.findMany.load();
|
roleState.findMany.load();
|
||||||
return toast.success("Data role Berhasil Dibuat");
|
return toast.success("Data role Berhasil Dibuat");
|
||||||
@@ -273,10 +168,7 @@ const roleState = proxy({
|
|||||||
}>[],
|
}>[],
|
||||||
loading: false,
|
loading: false,
|
||||||
async load() {
|
async load() {
|
||||||
const res =
|
const res = await ApiFetch.api.role["findMany"].get();
|
||||||
await ApiFetch.api.role[
|
|
||||||
"findMany"
|
|
||||||
].get();
|
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
roleState.findMany.data = res.data?.data ?? [];
|
roleState.findMany.data = res.data?.data ?? [];
|
||||||
}
|
}
|
||||||
@@ -291,9 +183,7 @@ const roleState = proxy({
|
|||||||
loading: false,
|
loading: false,
|
||||||
async load(id: string) {
|
async load(id: string) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(
|
const res = await fetch(`/api/role/${id}`);
|
||||||
`/api/role/${id}`
|
|
||||||
);
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
roleState.findUnique.data = data.data ?? null;
|
roleState.findUnique.data = data.data ?? null;
|
||||||
@@ -315,22 +205,17 @@ const roleState = proxy({
|
|||||||
try {
|
try {
|
||||||
roleState.delete.loading = true;
|
roleState.delete.loading = true;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(`/api/role/del/${id}`, {
|
||||||
`/api/role/del/${id}`,
|
method: "DELETE",
|
||||||
{
|
headers: {
|
||||||
method: "DELETE",
|
"Content-Type": "application/json",
|
||||||
headers: {
|
},
|
||||||
"Content-Type": "application/json",
|
});
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (response.ok && result?.success) {
|
if (response.ok && result?.success) {
|
||||||
toast.success(
|
toast.success(result.message || "Data role berhasil dihapus");
|
||||||
result.message || "Data role berhasil dihapus"
|
|
||||||
);
|
|
||||||
await roleState.findMany.load(); // refresh list
|
await roleState.findMany.load(); // refresh list
|
||||||
} else {
|
} else {
|
||||||
toast.error(result?.message || "Gagal menghapus Data role");
|
toast.error(result?.message || "Gagal menghapus Data role");
|
||||||
@@ -354,15 +239,12 @@ const roleState = proxy({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(`/api/role/${id}`, {
|
||||||
`/api/role/${id}`,
|
method: "GET",
|
||||||
{
|
headers: {
|
||||||
method: "GET",
|
"Content-Type": "application/json",
|
||||||
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}`);
|
||||||
}
|
}
|
||||||
@@ -374,6 +256,7 @@ const roleState = proxy({
|
|||||||
this.id = data.id;
|
this.id = data.id;
|
||||||
this.form = {
|
this.form = {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
|
permissions: data.permissions,
|
||||||
};
|
};
|
||||||
return data; // Return the loaded data
|
return data; // Return the loaded data
|
||||||
} else {
|
} else {
|
||||||
@@ -400,18 +283,16 @@ const roleState = proxy({
|
|||||||
try {
|
try {
|
||||||
roleState.update.loading = true;
|
roleState.update.loading = true;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(`/api/role/${this.id}`, {
|
||||||
`/api/role/${this.id}`,
|
method: "PUT",
|
||||||
{
|
headers: {
|
||||||
method: "PUT",
|
"Content-Type": "application/json",
|
||||||
headers: {
|
},
|
||||||
"Content-Type": "application/json",
|
body: JSON.stringify({
|
||||||
},
|
name: this.form.name,
|
||||||
body: JSON.stringify({
|
permissions: this.form.permissions,
|
||||||
name: this.form.name,
|
}),
|
||||||
}),
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorData = await response.json().catch(() => ({}));
|
const errorData = await response.json().catch(() => ({}));
|
||||||
@@ -451,6 +332,6 @@ const roleState = proxy({
|
|||||||
const user = proxy({
|
const user = proxy({
|
||||||
userState,
|
userState,
|
||||||
roleState,
|
roleState,
|
||||||
})
|
});
|
||||||
|
|
||||||
export default user
|
export default user;
|
||||||
|
|||||||
111
src/app/admin/(dashboard)/auth/login-admin/page.tsx
Normal 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;
|
||||||
121
src/app/admin/(dashboard)/auth/registrasi-admin/page.tsx
Normal 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;
|
||||||
38
src/app/admin/(dashboard)/auth/validasi-admin/page.tsx
Normal 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;
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } 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';
|
||||||
|
|
||||||
function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
|
function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -12,26 +13,35 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
|
|||||||
{
|
{
|
||||||
label: "Pelayanan Surat Keterangan",
|
label: "Pelayanan Surat Keterangan",
|
||||||
value: "pelayanansuratketerangan",
|
value: "pelayanansuratketerangan",
|
||||||
href: "/admin/desa/layanan/pelayanan_surat_keterangan"
|
href: "/admin/desa/layanan/pelayanan_surat_keterangan",
|
||||||
|
icon: <IconFileText size={18} stroke={1.8} />,
|
||||||
|
tooltip: "Layanan terkait surat keterangan resmi desa"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Pelayanan Perizinan Berusaha",
|
label: "Pelayanan Perizinan Berusaha",
|
||||||
value: "pelayananperizinanusaha",
|
value: "pelayananperizinanusaha",
|
||||||
href: "/admin/desa/layanan/pelayanan_perizinan_berusaha"
|
href: "/admin/desa/layanan/pelayanan_perizinan_berusaha",
|
||||||
|
icon: <IconBuildingStore size={18} stroke={1.8} />,
|
||||||
|
tooltip: "Layanan untuk izin usaha masyarakat"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Pelayanan Telunjuk Sakti Desa",
|
label: "Pelayanan Telunjuk Sakti Desa",
|
||||||
value: "pelayanantelunjuksaktidesa",
|
value: "pelayanantelunjuksaktidesa",
|
||||||
href: "/admin/desa/layanan/pelayanan_telunjuk_sakti_desa"
|
href: "/admin/desa/layanan/pelayanan_telunjuk_sakti_desa",
|
||||||
|
icon: <IconSparkles size={18} stroke={1.8} />,
|
||||||
|
tooltip: "Layanan inovasi khusus desa"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Pelayanan Penduduk Non-Permanent",
|
label: "Pelayanan Penduduk Non-Permanent",
|
||||||
value: "pelayanantelunjuknonpermanent",
|
value: "pelayanannonpermanent",
|
||||||
href: "/admin/desa/layanan/pelayanan_penduduk_non_permanent"
|
href: "/admin/desa/layanan/pelayanan_penduduk_non_permanent",
|
||||||
|
icon: <IconUsers size={18} stroke={1.8} />,
|
||||||
|
tooltip: "Pendataan penduduk non-permanent"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
const curentTab = tabs.find(tab => tab.href === pathname)
|
|
||||||
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
|
const currentTab = tabs.find(tab => tab.href === pathname)
|
||||||
|
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
|
||||||
|
|
||||||
const handleTabChange = (value: string | null) => {
|
const handleTabChange = (value: string | null) => {
|
||||||
const tab = tabs.find(t => t.value === value)
|
const tab = tabs.find(t => t.value === value)
|
||||||
@@ -49,24 +59,72 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
|
|||||||
}, [pathname])
|
}, [pathname])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack gap="lg">
|
||||||
<Title order={3}>Layanan</Title>
|
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Layanan</Title>
|
||||||
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
|
<Tabs
|
||||||
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
|
color={colors["blue-button"]}
|
||||||
{tabs.map((e, i) => (
|
variant="pills"
|
||||||
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
|
value={activeTab}
|
||||||
))}
|
onChange={handleTabChange}
|
||||||
</TabsList>
|
radius="lg"
|
||||||
{tabs.map((e, i) => (
|
keepMounted={false}
|
||||||
<TabsPanel key={i} value={e.value}>
|
>
|
||||||
{/* Konten dummy, bisa diganti tergantung routing */}
|
{/* ✅ Scroll horizontal wrapper */}
|
||||||
<></>
|
<ScrollArea type="auto" offsetScrollbars>
|
||||||
|
<TabsList
|
||||||
|
p="sm"
|
||||||
|
style={{
|
||||||
|
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||||
|
borderRadius: "1rem",
|
||||||
|
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||||
|
display: "flex",
|
||||||
|
flexWrap: "nowrap",
|
||||||
|
gap: "0.5rem",
|
||||||
|
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tabs.map((tab, i) => (
|
||||||
|
<Tooltip
|
||||||
|
key={i}
|
||||||
|
label={tab.tooltip}
|
||||||
|
position="bottom"
|
||||||
|
withArrow
|
||||||
|
transitionProps={{ transition: 'pop', duration: 200 }}
|
||||||
|
>
|
||||||
|
<TabsTab
|
||||||
|
value={tab.value}
|
||||||
|
leftSection={tab.icon}
|
||||||
|
style={{
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: "0.9rem",
|
||||||
|
transition: "all 0.2s ease",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</TabsTab>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
|
{tabs.map((tab, i) => (
|
||||||
|
<TabsPanel
|
||||||
|
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>
|
</TabsPanel>
|
||||||
))}
|
))}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
{children}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LayoutTabsLayanan;
|
export default LayoutTabsLayanan;
|
||||||
|
|||||||
@@ -1,63 +1,117 @@
|
|||||||
/* 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 } 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';
|
||||||
|
|
||||||
function LayoutTabsBerita({ children }: { children: React.ReactNode }) {
|
function LayoutTabsBerita({ children }: { children: React.ReactNode }) {
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
const pathname = usePathname()
|
const pathname = usePathname();
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
label: "List Berita",
|
label: "List Berita",
|
||||||
value: "list_berita",
|
value: "list_berita",
|
||||||
href: "/admin/desa/berita/list-berita"
|
href: "/admin/desa/berita/list-berita",
|
||||||
|
icon: <IconNews size={18} stroke={1.8} />,
|
||||||
|
tooltip: "Lihat dan kelola semua berita desa"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Kategori Berita",
|
label: "Kategori Berita",
|
||||||
value: "kategori_berita",
|
value: "kategori_berita",
|
||||||
href: "/admin/desa/berita/kategori-berita"
|
href: "/admin/desa/berita/kategori-berita",
|
||||||
|
icon: <IconCategory size={18} stroke={1.8} />,
|
||||||
|
tooltip: "Kelola kategori berita desa"
|
||||||
},
|
},
|
||||||
|
|
||||||
];
|
];
|
||||||
const curentTab = tabs.find(tab => tab.href === pathname)
|
|
||||||
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
|
const currentTab = tabs.find(tab => tab.href === pathname);
|
||||||
|
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
|
||||||
|
|
||||||
const handleTabChange = (value: string | null) => {
|
const handleTabChange = (value: string | null) => {
|
||||||
const tab = tabs.find(t => t.value === value)
|
const tab = tabs.find(t => t.value === value);
|
||||||
if (tab) {
|
if (tab) {
|
||||||
router.push(tab.href)
|
router.push(tab.href);
|
||||||
}
|
}
|
||||||
setActiveTab(value)
|
setActiveTab(value);
|
||||||
}
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const match = tabs.find(tab => tab.href === pathname)
|
const match = tabs.find(tab => tab.href === pathname);
|
||||||
if (match) {
|
if (match) {
|
||||||
setActiveTab(match.value)
|
setActiveTab(match.value);
|
||||||
}
|
}
|
||||||
}, [pathname])
|
}, [pathname]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack gap="lg">
|
||||||
<Title order={3}>Gallery</Title>
|
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Berita Desa</Title>
|
||||||
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
|
<Tabs
|
||||||
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
|
color={colors["blue-button"]}
|
||||||
{tabs.map((e, i) => (
|
variant="pills"
|
||||||
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
|
value={activeTab}
|
||||||
))}
|
onChange={handleTabChange}
|
||||||
</TabsList>
|
radius="lg"
|
||||||
{tabs.map((e, i) => (
|
keepMounted={false}
|
||||||
<TabsPanel key={i} value={e.value}>
|
>
|
||||||
{/* Konten dummy, bisa diganti tergantung routing */}
|
{/* ✅ Scroll horizontal wrapper */}
|
||||||
<></>
|
<ScrollArea type="auto" offsetScrollbars>
|
||||||
|
<TabsList
|
||||||
|
p="sm"
|
||||||
|
style={{
|
||||||
|
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||||
|
borderRadius: "1rem",
|
||||||
|
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||||
|
display: "flex",
|
||||||
|
flexWrap: "nowrap",
|
||||||
|
gap: "0.5rem",
|
||||||
|
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tabs.map((tab, i) => (
|
||||||
|
<Tooltip
|
||||||
|
key={i}
|
||||||
|
label={tab.tooltip}
|
||||||
|
position="bottom"
|
||||||
|
withArrow
|
||||||
|
transitionProps={{ transition: 'pop', duration: 200 }}
|
||||||
|
>
|
||||||
|
<TabsTab
|
||||||
|
value={tab.value}
|
||||||
|
leftSection={tab.icon}
|
||||||
|
style={{
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: "0.9rem",
|
||||||
|
transition: "all 0.2s ease",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</TabsTab>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
|
{tabs.map((tab, i) => (
|
||||||
|
<TabsPanel
|
||||||
|
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>
|
</TabsPanel>
|
||||||
))}
|
))}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
{children}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LayoutTabsBerita;
|
export default LayoutTabsBerita;
|
||||||
|
|||||||
@@ -2,7 +2,16 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
|
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Tooltip
|
||||||
|
} from '@mantine/core';
|
||||||
import { IconArrowBack } from '@tabler/icons-react';
|
import { IconArrowBack } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
@@ -10,67 +19,102 @@ import { toast } from 'react-toastify';
|
|||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
function EditKategoriBerita() {
|
function EditKategoriBerita() {
|
||||||
const editState = useProxy(stateDashboardBerita.kategoriBerita)
|
const editState = useProxy(stateDashboardBerita.kategoriBerita);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: editState.update.form.name || '',
|
name: editState.update.form.name || '',
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadKategori = async () => {
|
const loadKategori = async () => {
|
||||||
const id = params?.id as string;
|
const id = params?.id as string;
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
|
||||||
try {
|
|
||||||
const data = await editState.update.load(id); // akses langsung, bukan dari proxy
|
|
||||||
if (data) {
|
|
||||||
setFormData({
|
|
||||||
name: data.name || '',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error loading kategori Berita:", error);
|
|
||||||
toast.error("Gagal memuat data kategori Berita");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loadKategori();
|
|
||||||
}, [params?.id]);
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
try {
|
try {
|
||||||
editState.update.form = {
|
const data = await editState.update.load(id);
|
||||||
...editState.update.form,
|
if (data) {
|
||||||
name: formData.name,
|
setFormData({
|
||||||
};
|
name: data.name || '',
|
||||||
await editState.update.update();
|
});
|
||||||
toast.success('Kategori Berita berhasil diperbarui!');
|
}
|
||||||
router.push('/admin/desa/berita/kategori-berita');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating kategori Berita:', error);
|
console.error('Error loading kategori Berita:', error);
|
||||||
toast.error('Terjadi kesalahan saat memperbarui kategori Berita');
|
toast.error('Gagal memuat data kategori Berita');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
loadKategori();
|
||||||
|
}, [params?.id]);
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
editState.update.form = {
|
||||||
|
...editState.update.form,
|
||||||
|
name: formData.name,
|
||||||
|
};
|
||||||
|
|
||||||
|
await editState.update.update();
|
||||||
|
toast.success('Kategori Berita berhasil diperbarui!');
|
||||||
|
router.push('/admin/desa/berita/kategori-berita');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating kategori Berita:', error);
|
||||||
|
toast.error('Terjadi kesalahan saat memperbarui kategori Berita');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
{/* Back Button + Title */}
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
<Group mb="md">
|
||||||
<IconArrowBack color={colors["blue-button"]} size={30} />
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
</Button>
|
<Button
|
||||||
</Box>
|
variant="subtle"
|
||||||
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}>
|
onClick={() => router.back()}
|
||||||
<Stack gap={"xs"}>
|
p="xs"
|
||||||
<Title order={3}>Edit Kategori Berita</Title>
|
radius="md"
|
||||||
|
>
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<Title order={4} ml="sm" c="dark">
|
||||||
|
Edit Kategori Berita
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{/* Form Wrapper */}
|
||||||
|
<Paper
|
||||||
|
w={{ base: '100%', md: '50%' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
|
label="Nama Kategori Berita"
|
||||||
|
placeholder="Masukkan nama kategori berita"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Nama Kategori Berita</Text>}
|
required
|
||||||
placeholder="masukkan nama kategori Berita"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button onClick={handleSubmit}>Simpan</Button>
|
<Group justify="right">
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
|
color: '#fff',
|
||||||
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,50 +1,87 @@
|
|||||||
'use client'
|
'use client';
|
||||||
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
|
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mantine/core';
|
||||||
import { IconArrowBack } from '@tabler/icons-react';
|
import { IconArrowBack } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function CreateKategoriBerita() {
|
function CreateKategoriBerita() {
|
||||||
const createState = useProxy(stateDashboardBerita.kategoriBerita)
|
const createState = useProxy(stateDashboardBerita.kategoriBerita);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
createState.create.form = {
|
createState.create.form = {
|
||||||
name: "",
|
name: '',
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
await createState.create.create();
|
await createState.create.create();
|
||||||
resetForm();
|
resetForm();
|
||||||
router.push("/admin/desa/berita/kategori-berita")
|
router.push('/admin/desa/berita/kategori-berita');
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
{/* Header dengan back button */}
|
||||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
<Group mb="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
</Button>
|
<Button
|
||||||
</Box>
|
variant="subtle"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
p="xs"
|
||||||
|
radius="md"
|
||||||
|
>
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<Title order={4} ml="sm" c="dark">
|
||||||
|
Tambah Kategori Berita
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
{/* Form utama */}
|
||||||
<Stack gap={"xs"}>
|
<Paper
|
||||||
<Title order={4}>Create Kategori Berita</Title>
|
w={{ base: '100%', md: '50%' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Berita</Text>}
|
label={<Text fw="bold" fz="sm">Nama Kategori Berita</Text>}
|
||||||
placeholder='Masukkan nama kategori Berita'
|
placeholder="Masukkan nama kategori berita"
|
||||||
value={createState.create.form.name}
|
value={createState.create.form.name || ''}
|
||||||
onChange={(val) => {
|
onChange={(e) => (createState.create.form.name = e.target.value)}
|
||||||
createState.create.form.name = val.target.value;
|
required
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<Group>
|
|
||||||
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
|
<Group justify="right">
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
|
color: '#fff',
|
||||||
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -1,25 +1,40 @@
|
|||||||
/* 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 { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
import {
|
||||||
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
|
Box,
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Group,
|
||||||
|
Pagination,
|
||||||
|
Paper,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
TableTbody,
|
||||||
|
TableTd,
|
||||||
|
TableTh,
|
||||||
|
TableThead,
|
||||||
|
TableTr,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
Tooltip
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { IconEdit, IconPlus, IconSearch, IconTrash } 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';
|
||||||
import HeaderSearch from '../../../_com/header';
|
import HeaderSearch from '../../../_com/header';
|
||||||
import JudulList from '../../../_com/judulList';
|
|
||||||
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
||||||
import stateDashboardBerita from '../../../_state/desa/berita';
|
import stateDashboardBerita from '../../../_state/desa/berita';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function KategoriBerita() {
|
function KategoriBerita() {
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
title='Kategori Berita'
|
title="Kategori Berita"
|
||||||
placeholder='pencarian'
|
placeholder="Cari nama kategori berita..."
|
||||||
searchIcon={<IconSearch size={20} />}
|
searchIcon={<IconSearch size={20} />}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
@@ -30,99 +45,155 @@ function KategoriBerita() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ListKategoriBerita({ search }: { search: string }) {
|
function ListKategoriBerita({ search }: { search: string }) {
|
||||||
const listDataState = useProxy(stateDashboardBerita.kategoriBerita)
|
const listDataState = useProxy(stateDashboardBerita.kategoriBerita);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [modalHapus, setModalHapus] = useState(false)
|
const [modalHapus, setModalHapus] = useState(false);
|
||||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
loading,
|
||||||
|
load,
|
||||||
|
page,
|
||||||
|
totalPages,
|
||||||
|
} = listDataState.findMany;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
listDataState.findMany.load()
|
load(page, 10, search);
|
||||||
}, [])
|
}, [page, search]);
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
if (selectedId) {
|
if (selectedId) {
|
||||||
listDataState.delete.delete(selectedId)
|
listDataState.delete.delete(selectedId);
|
||||||
setModalHapus(false)
|
setModalHapus(false);
|
||||||
setSelectedId(null)
|
setSelectedId(null);
|
||||||
|
load(page, 10, search);
|
||||||
listDataState.findMany.load()
|
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const filteredData = (listDataState.findMany.data || []).filter(item => {
|
const filteredData = data || [];
|
||||||
const keyword = search.toLowerCase();
|
|
||||||
return (
|
|
||||||
item.name.toLowerCase().includes(keyword)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!listDataState.findMany.data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton h={500} />
|
<Skeleton height={600} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={10}>
|
||||||
<Paper bg={colors['white-1']} p="md">
|
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||||
<Stack>
|
<Group justify="space-between" mb="md">
|
||||||
<JudulList
|
<Title order={4}>Daftar Kategori Berita</Title>
|
||||||
title='List Kategori Berita'
|
<Tooltip label="Tambah Kategori Berita" withArrow>
|
||||||
href='/admin/desa/berita/kategori-berita/create'
|
<Button
|
||||||
/>
|
leftSection={<IconPlus size={18} />}
|
||||||
<Box style={{ overflowX: 'auto' }}>
|
color="blue"
|
||||||
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
|
variant="light"
|
||||||
<TableThead>
|
onClick={() =>
|
||||||
<TableTr>
|
router.push('/admin/desa/berita/kategori-berita/create')
|
||||||
<TableTh>No</TableTh>
|
}
|
||||||
<TableTh>Nama</TableTh>
|
>
|
||||||
<TableTh>Edit</TableTh>
|
Tambah Baru
|
||||||
<TableTh>Hapus</TableTh>
|
</Button>
|
||||||
</TableTr>
|
</Tooltip>
|
||||||
</TableThead>
|
</Group>
|
||||||
<TableTbody>
|
|
||||||
{filteredData.map((item, index) => (
|
<Box style={{ overflowX: 'auto' }}>
|
||||||
|
<Table highlightOnHover>
|
||||||
|
<TableThead>
|
||||||
|
<TableTr>
|
||||||
|
<TableTh style={{ width: '10%' }}>No</TableTh>
|
||||||
|
<TableTh style={{ width: '50%' }}>Nama</TableTh>
|
||||||
|
<TableTh style={{ width: '20%' }}>Edit</TableTh>
|
||||||
|
<TableTh style={{ width: '20%' }}>Hapus</TableTh>
|
||||||
|
</TableTr>
|
||||||
|
</TableThead>
|
||||||
|
<TableTbody>
|
||||||
|
{filteredData.length > 0 ? (
|
||||||
|
filteredData.map((item, index) => (
|
||||||
<TableTr key={item.id}>
|
<TableTr key={item.id}>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Box w={100}>
|
<Text fz="sm">{index + 1}</Text>
|
||||||
<Text truncate="end" fz={"sm"}>{index + 1}</Text>
|
|
||||||
</Box>
|
|
||||||
</TableTd>
|
|
||||||
<TableTd>{item.name}</TableTd>
|
|
||||||
<TableTd>
|
|
||||||
<Button color='green' onClick={() => router.push(`/admin/pendidikan/perpustakaan-digital/kategori-Berita/${item.id}`)}>
|
|
||||||
<IconEdit size={20} />
|
|
||||||
</Button>
|
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Button
|
<Text fw={500} truncate="end" lineClamp={1}>
|
||||||
color='red'
|
{item.name}
|
||||||
disabled={listDataState.delete.loading}
|
</Text>
|
||||||
onClick={() => {
|
</TableTd>
|
||||||
setSelectedId(item.id)
|
<TableTd>
|
||||||
setModalHapus(true)
|
<Tooltip label="Edit Kategori Berita" withArrow>
|
||||||
}}>
|
<Button
|
||||||
<IconTrash size={20} />
|
variant="light"
|
||||||
</Button>
|
color="green"
|
||||||
|
onClick={() =>
|
||||||
|
router.push(
|
||||||
|
`/admin/desa/berita/kategori-berita/${item.id}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<IconEdit size={18} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Tooltip label="Hapus Kategori Berita" withArrow>
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
color="red"
|
||||||
|
disabled={listDataState.delete.loading}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedId(item.id);
|
||||||
|
setModalHapus(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconTrash size={18} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
))}
|
))
|
||||||
</TableTbody>
|
) : (
|
||||||
</Table>
|
<TableTr>
|
||||||
</Box>
|
<TableTd colSpan={4}>
|
||||||
</Stack>
|
<Center py={20}>
|
||||||
|
<Text color="dimmed">
|
||||||
|
Tidak ada data kategori berita yang cocok
|
||||||
|
</Text>
|
||||||
|
</Center>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
)}
|
||||||
|
</TableTbody>
|
||||||
|
</Table>
|
||||||
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
|
<Center>
|
||||||
|
<Pagination
|
||||||
|
value={page}
|
||||||
|
onChange={(newPage) => {
|
||||||
|
load(newPage, 10, search);
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
}}
|
||||||
|
total={totalPages}
|
||||||
|
mt="md"
|
||||||
|
mb="md"
|
||||||
|
color="blue"
|
||||||
|
radius="md"
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
|
||||||
{/* Modal Konfirmasi Hapus */}
|
{/* Modal Konfirmasi Hapus */}
|
||||||
<ModalKonfirmasiHapus
|
<ModalKonfirmasiHapus
|
||||||
opened={modalHapus}
|
opened={modalHapus}
|
||||||
onClose={() => setModalHapus(false)}
|
onClose={() => setModalHapus(false)}
|
||||||
onConfirm={handleDelete}
|
onConfirm={handleDelete}
|
||||||
text='Apakah anda yakin ingin menghapus kategori Berita ini?'
|
text="Apakah anda yakin ingin menghapus kategori berita ini?"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default KategoriBerita;
|
export default KategoriBerita;
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title
|
Title,
|
||||||
|
Tooltip,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { Dropzone } from "@mantine/dropzone";
|
import { Dropzone } from "@mantine/dropzone";
|
||||||
import { IconArrowBack, IconPhoto, IconUpload, IconX } from "@tabler/icons-react";
|
import { IconArrowBack, IconPhoto, IconUpload, IconX } from "@tabler/icons-react";
|
||||||
@@ -24,7 +25,6 @@ import { useEffect, useState } from "react";
|
|||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { useProxy } from "valtio/utils";
|
import { useProxy } from "valtio/utils";
|
||||||
|
|
||||||
|
|
||||||
function EditBerita() {
|
function EditBerita() {
|
||||||
const beritaState = useProxy(stateDashboardBerita);
|
const beritaState = useProxy(stateDashboardBerita);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -33,29 +33,29 @@ function EditBerita() {
|
|||||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
judul: beritaState.berita.edit.form.judul || '',
|
judul: beritaState.berita.edit.form.judul || "",
|
||||||
deskripsi: beritaState.berita.edit.form.deskripsi || '',
|
deskripsi: beritaState.berita.edit.form.deskripsi || "",
|
||||||
kategoriBeritaId: beritaState.berita.edit.form.kategoriBeritaId || '',
|
kategoriBeritaId: beritaState.berita.edit.form.kategoriBeritaId || "",
|
||||||
content: beritaState.berita.edit.form.content || '',
|
content: beritaState.berita.edit.form.content || "",
|
||||||
imageId: beritaState.berita.edit.form.imageId || ''
|
imageId: beritaState.berita.edit.form.imageId || "",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load berita by id saat pertama kali
|
// Load berita by id saat pertama kali
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
beritaState.kategoriBerita.findMany.load()
|
beritaState.kategoriBerita.findMany.load();
|
||||||
const loadBerita = async () => {
|
const loadBerita = async () => {
|
||||||
const id = params?.id as string;
|
const id = params?.id as string;
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await stateDashboardBerita.berita.edit.load(id); // akses langsung, bukan dari proxy
|
const data = await stateDashboardBerita.berita.edit.load(id);
|
||||||
if (data) {
|
if (data) {
|
||||||
setFormData({
|
setFormData({
|
||||||
judul: data.judul || '',
|
judul: data.judul || "",
|
||||||
deskripsi: data.deskripsi || '',
|
deskripsi: data.deskripsi || "",
|
||||||
kategoriBeritaId: data.kategoriBeritaId || '',
|
kategoriBeritaId: data.kategoriBeritaId || "",
|
||||||
content: data.content || '',
|
content: data.content || "",
|
||||||
imageId: data.imageId || '',
|
imageId: data.imageId || "",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data?.image?.link) {
|
if (data?.image?.link) {
|
||||||
@@ -69,31 +69,26 @@ function EditBerita() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
loadBerita();
|
loadBerita();
|
||||||
}, [params?.id]); // ✅ hapus beritaState dari dependency
|
}, [params?.id]);
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Update global state with form data
|
|
||||||
beritaState.berita.edit.form = {
|
beritaState.berita.edit.form = {
|
||||||
...beritaState.berita.edit.form,
|
...beritaState.berita.edit.form,
|
||||||
judul: formData.judul,
|
...formData,
|
||||||
deskripsi: formData.deskripsi,
|
|
||||||
content: formData.content,
|
|
||||||
kategoriBeritaId: formData.kategoriBeritaId || '',
|
|
||||||
imageId: formData.imageId // Keep existing imageId if not changed
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Jika ada file baru, upload
|
|
||||||
if (file) {
|
if (file) {
|
||||||
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
|
const res = await ApiFetch.api.fileStorage.create.post({
|
||||||
|
file,
|
||||||
|
name: file.name,
|
||||||
|
});
|
||||||
const uploaded = res.data?.data;
|
const uploaded = res.data?.data;
|
||||||
|
|
||||||
if (!uploaded?.id) {
|
if (!uploaded?.id) {
|
||||||
return toast.error("Gagal upload gambar");
|
return toast.error("Gagal upload gambar");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update imageId in global state
|
|
||||||
beritaState.berita.edit.form.imageId = uploaded.id;
|
beritaState.berita.edit.form.imageId = uploaded.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,87 +102,112 @@ function EditBerita() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: "sm", md: "lg" }} py="md">
|
||||||
<Box mb={10}>
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
<IconArrowBack color={colors["blue-button"]} size={30} />
|
<Button
|
||||||
</Button>
|
variant="subtle"
|
||||||
</Box>
|
onClick={() => router.back()}
|
||||||
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}>
|
p="xs"
|
||||||
<Stack gap={"xs"}>
|
radius="md"
|
||||||
<Title order={3}>Edit Berita</Title>
|
>
|
||||||
|
<IconArrowBack color={colors["blue-button"]} size={24} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<Title order={4} ml="sm" c="dark">
|
||||||
|
Edit Berita
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Paper
|
||||||
|
w={{ base: "100%", md: "50%" }}
|
||||||
|
bg={colors["white-1"]}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: "1px solid #e0e0e0" }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
|
label="Judul"
|
||||||
|
placeholder="Masukkan judul"
|
||||||
value={formData.judul}
|
value={formData.judul}
|
||||||
onChange={(e) => setFormData({ ...formData, judul: e.target.value })}
|
onChange={(e) =>
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
|
setFormData({ ...formData, judul: e.target.value })
|
||||||
placeholder="masukkan judul"
|
}
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
|
label="Deskripsi"
|
||||||
|
placeholder="Masukkan deskripsi"
|
||||||
value={formData.deskripsi}
|
value={formData.deskripsi}
|
||||||
onChange={(e) => setFormData({ ...formData, deskripsi: e.target.value })}
|
onChange={(e) =>
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>}
|
setFormData({ ...formData, deskripsi: e.target.value })
|
||||||
placeholder="masukkan deskripsi"
|
}
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"md"} fw={"bold"}>Gambar</Text>
|
<Text fw="bold" fz="sm" mb={6}>
|
||||||
<Box>
|
Gambar Berita
|
||||||
<Dropzone
|
</Text>
|
||||||
onDrop={(files) => {
|
<Dropzone
|
||||||
const selectedFile = files[0]; // Ambil file pertama
|
onDrop={(files) => {
|
||||||
if (selectedFile) {
|
const selectedFile = files[0];
|
||||||
setFile(selectedFile);
|
if (selectedFile) {
|
||||||
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
|
setFile(selectedFile);
|
||||||
}
|
setPreviewImage(URL.createObjectURL(selectedFile));
|
||||||
}}
|
}
|
||||||
onReject={() => toast.error('File tidak valid.')}
|
}}
|
||||||
maxSize={5 * 1024 ** 2} // Maks 5MB
|
onReject={() => toast.error("File tidak valid, gunakan format gambar")}
|
||||||
accept={{ 'image/*': [] }}
|
maxSize={5 * 1024 ** 2}
|
||||||
>
|
accept={{ "image/*": [] }}
|
||||||
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
radius="md"
|
||||||
<Dropzone.Accept>
|
p="xl"
|
||||||
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
>
|
||||||
</Dropzone.Accept>
|
<Group justify="center" gap="xl" mih={180}>
|
||||||
<Dropzone.Reject>
|
<Dropzone.Accept>
|
||||||
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
<IconUpload size={48} color={colors["blue-button"]} stroke={1.5} />
|
||||||
</Dropzone.Reject>
|
</Dropzone.Accept>
|
||||||
<Dropzone.Idle>
|
<Dropzone.Reject>
|
||||||
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
<IconX size={48} color="red" stroke={1.5} />
|
||||||
</Dropzone.Idle>
|
</Dropzone.Reject>
|
||||||
|
<Dropzone.Idle>
|
||||||
|
<IconPhoto size={48} color="#868e96" stroke={1.5} />
|
||||||
|
</Dropzone.Idle>
|
||||||
|
<Stack gap="xs" align="center">
|
||||||
|
<Text size="md" fw={500}>
|
||||||
|
Seret gambar atau klik untuk memilih file
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
Maksimal 5MB, format gambar wajib
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Group>
|
||||||
|
</Dropzone>
|
||||||
|
|
||||||
<div>
|
{previewImage && (
|
||||||
<Text size="xl" inline>
|
<Box mt="sm" style={{ display: "flex", justifyContent: "center" }}>
|
||||||
Drag gambar ke sini atau klik untuk pilih file
|
<Image
|
||||||
</Text>
|
src={previewImage}
|
||||||
<Text size="sm" c="dimmed" inline mt={7}>
|
alt="Preview Gambar"
|
||||||
Maksimal 5MB dan harus format gambar
|
radius="md"
|
||||||
</Text>
|
style={{
|
||||||
</div>
|
maxHeight: 220,
|
||||||
</Group>
|
objectFit: "contain",
|
||||||
</Dropzone>
|
border: `1px solid ${colors["blue-button"]}`,
|
||||||
|
}}
|
||||||
{/* Tampilkan preview kalau ada */}
|
loading="lazy"
|
||||||
{previewImage && (
|
/>
|
||||||
<Box mt="sm">
|
</Box>
|
||||||
<Image
|
)}
|
||||||
src={previewImage}
|
|
||||||
alt="Preview"
|
|
||||||
style={{
|
|
||||||
maxWidth: '100%',
|
|
||||||
maxHeight: '200px',
|
|
||||||
objectFit: 'contain',
|
|
||||||
borderRadius: '8px',
|
|
||||||
border: '1px solid #ddd',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"sm"} fw={"bold"}>Konten</Text>
|
<Text fz="sm" fw="bold">
|
||||||
|
Konten
|
||||||
|
</Text>
|
||||||
<EditEditor
|
<EditEditor
|
||||||
value={formData.content}
|
value={formData.content}
|
||||||
onChange={(htmlContent) => {
|
onChange={(htmlContent) => {
|
||||||
@@ -199,13 +219,15 @@ function EditBerita() {
|
|||||||
|
|
||||||
<Select
|
<Select
|
||||||
value={formData.kategoriBeritaId}
|
value={formData.kategoriBeritaId}
|
||||||
onChange={(val) => setFormData({ ...formData, kategoriBeritaId: val || "" })}
|
onChange={(val) =>
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>}
|
setFormData({ ...formData, kategoriBeritaId: val || "" })
|
||||||
placeholder='Pilih kategori'
|
}
|
||||||
|
label="Kategori"
|
||||||
|
placeholder="Pilih kategori"
|
||||||
data={
|
data={
|
||||||
beritaState.kategoriBerita.findMany.data?.map((v) => ({
|
beritaState.kategoriBerita.findMany.data?.map((v) => ({
|
||||||
value: v.id,
|
value: v.id,
|
||||||
label: v.name
|
label: v.name,
|
||||||
})) || []
|
})) || []
|
||||||
}
|
}
|
||||||
clearable
|
clearable
|
||||||
@@ -214,7 +236,20 @@ function EditBerita() {
|
|||||||
error={!formData.kategoriBeritaId ? "Pilih kategori" : undefined}
|
error={!formData.kategoriBeritaId ? "Pilih kategori" : undefined}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button onClick={handleSubmit}>Edit Berita</Button>
|
<Group justify="right">
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${colors["blue-button"]}, #4facfe)`,
|
||||||
|
color: "#fff",
|
||||||
|
boxShadow: "0 4px 15px rgba(79, 172, 254, 0.4)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
|
||||||
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
|
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
@@ -12,107 +11,147 @@ import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirma
|
|||||||
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
|
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
|
||||||
|
|
||||||
function DetailBerita() {
|
function DetailBerita() {
|
||||||
const beritaState = useProxy(stateDashboardBerita)
|
const beritaState = useProxy(stateDashboardBerita);
|
||||||
const [modalHapus, setModalHapus] = useState(false)
|
const [modalHapus, setModalHapus] = useState(false);
|
||||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||||
const params = useParams()
|
const params = useParams();
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
beritaState.berita.findUnique.load(params?.id as string)
|
beritaState.berita.findUnique.load(params?.id as string);
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
const handleHapus = () => {
|
const handleHapus = () => {
|
||||||
if (selectedId) {
|
if (selectedId) {
|
||||||
beritaState.berita.delete.byId(selectedId)
|
beritaState.berita.delete.byId(selectedId);
|
||||||
setModalHapus(false)
|
setModalHapus(false);
|
||||||
setSelectedId(null)
|
setSelectedId(null);
|
||||||
router.push("/admin/desa/berita/list-berita")
|
router.push("/admin/desa/berita/list-berita");
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
if (!beritaState.berita.findUnique.data) {
|
if (!beritaState.berita.findUnique.data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton h={40} />
|
<Skeleton height={500} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = beritaState.berita.findUnique.data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box py={10}>
|
||||||
<Box mb={10}>
|
{/* Tombol Back */}
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
<Button
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
variant="subtle"
|
||||||
</Button>
|
onClick={() => router.back()}
|
||||||
</Box>
|
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
|
||||||
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
|
mb={15}
|
||||||
<Stack>
|
>
|
||||||
<Text fz={"xl"} fw={"bold"}>Detail Berita</Text>
|
Kembali
|
||||||
{beritaState.berita.findUnique.data ? (
|
</Button>
|
||||||
<Paper key={beritaState.berita.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
|
|
||||||
<Stack gap={"xs"}>
|
{/* Detail Berita */}
|
||||||
<Box>
|
<Paper
|
||||||
<Text fw={"bold"} fz={"lg"}>Kategori</Text>
|
withBorder
|
||||||
<Text fz={"lg"}>{beritaState.berita.findUnique.data?.kategoriBerita?.name}</Text>
|
w={{ base: "100%", md: "70%" }}
|
||||||
</Box>
|
bg={colors['white-1']}
|
||||||
<Box>
|
p="lg"
|
||||||
<Text fw={"bold"} fz={"lg"}>Judul</Text>
|
radius="md"
|
||||||
<Text fz={"lg"}>{beritaState.berita.findUnique.data?.judul}</Text>
|
shadow="sm"
|
||||||
</Box>
|
>
|
||||||
<Box>
|
<Stack gap="md">
|
||||||
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
|
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
|
||||||
<Text fz={"lg"} >{beritaState.berita.findUnique.data?.deskripsi}</Text>
|
Detail Berita
|
||||||
</Box>
|
</Text>
|
||||||
<Box>
|
|
||||||
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
|
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
|
||||||
<Image w={{ base: 150, md: 150, lg: 150 }} src={beritaState.berita.findUnique.data?.image?.link} alt="gambar" />
|
<Stack gap="sm">
|
||||||
</Box>
|
<Box>
|
||||||
<Box>
|
<Text fz="lg" fw="bold">Kategori</Text>
|
||||||
<Text fw={"bold"} fz={"lg"}>Konten</Text>
|
<Text fz="md" c="dimmed">{data.kategoriBerita?.name || '-'}</Text>
|
||||||
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: beritaState.berita.findUnique.data?.content }} />
|
</Box>
|
||||||
</Box>
|
|
||||||
<Flex gap={"xs"} mt={10}>
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">Judul</Text>
|
||||||
|
<Text fz="md" c="dimmed">{data.judul || '-'}</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">Deskripsi</Text>
|
||||||
|
<Text fz="md" c="dimmed">{data.deskripsi || '-'}</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">Gambar</Text>
|
||||||
|
{data.image?.link ? (
|
||||||
|
<Image
|
||||||
|
src={data.image.link}
|
||||||
|
alt={data.judul || 'Gambar Berita'}
|
||||||
|
w={200}
|
||||||
|
h={200}
|
||||||
|
radius="md"
|
||||||
|
fit="cover"
|
||||||
|
loading='lazy'
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">Konten</Text>
|
||||||
|
<Text
|
||||||
|
fz="md"
|
||||||
|
c="dimmed"
|
||||||
|
dangerouslySetInnerHTML={{ __html: data.content || '-' }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Action Button */}
|
||||||
|
<Group gap="sm">
|
||||||
|
<Tooltip label="Hapus Berita" withArrow position="top">
|
||||||
<Button
|
<Button
|
||||||
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (beritaState.berita.findUnique.data) {
|
setSelectedId(data.id);
|
||||||
setSelectedId(beritaState.berita.findUnique.data.id);
|
setModalHapus(true);
|
||||||
setModalHapus(true);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
disabled={beritaState.berita.delete.loading || !beritaState.berita.findUnique.data}
|
variant="light"
|
||||||
color={"red"}
|
radius="md"
|
||||||
|
size="md"
|
||||||
>
|
>
|
||||||
<IconX size={20} />
|
<IconTrash size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip label="Edit Berita" withArrow position="top">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
color="green"
|
||||||
if (beritaState.berita.findUnique.data) {
|
onClick={() => router.push(`/admin/desa/berita/list-berita/${data.id}/edit`)}
|
||||||
router.push(`/admin/desa/berita/list-berita/${beritaState.berita.findUnique.data.id}/edit`);
|
variant="light"
|
||||||
}
|
radius="md"
|
||||||
}}
|
size="md"
|
||||||
disabled={!beritaState.berita.findUnique.data}
|
|
||||||
color={"green"}
|
|
||||||
>
|
>
|
||||||
<IconEdit size={20} />
|
<IconEdit size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Tooltip>
|
||||||
</Stack>
|
</Group>
|
||||||
</Paper>
|
</Stack>
|
||||||
) : null}
|
</Paper>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
{/* Modal Konfirmasi Hapus */}
|
{/* Modal Hapus */}
|
||||||
<ModalKonfirmasiHapus
|
<ModalKonfirmasiHapus
|
||||||
opened={modalHapus}
|
opened={modalHapus}
|
||||||
onClose={() => setModalHapus(false)}
|
onClose={() => setModalHapus(false)}
|
||||||
onConfirm={handleHapus}
|
onConfirm={handleHapus}
|
||||||
text='Apakah anda yakin ingin menghapus berita ini?'
|
text="Apakah Anda yakin ingin menghapus berita ini?"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DetailBerita;
|
export default DetailBerita;
|
||||||
|
|||||||
@@ -3,7 +3,19 @@ import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
|
|||||||
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
|
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import ApiFetch from '@/lib/api-fetch';
|
import ApiFetch from '@/lib/api-fetch';
|
||||||
import { Box, Button, Group, Image, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Image,
|
||||||
|
Paper,
|
||||||
|
Select,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mantine/core';
|
||||||
import { Dropzone } from '@mantine/dropzone';
|
import { Dropzone } from '@mantine/dropzone';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||||
@@ -12,38 +24,33 @@ import { useState } from 'react';
|
|||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
|
|
||||||
export default function CreateBerita() {
|
export default function CreateBerita() {
|
||||||
const beritaState = useProxy(stateDashboardBerita);
|
const beritaState = useProxy(stateDashboardBerita);
|
||||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
beritaState.kategoriBerita.findMany.load()
|
beritaState.kategoriBerita.findMany.load();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
// Reset state di valtio
|
|
||||||
beritaState.berita.create.form = {
|
beritaState.berita.create.form = {
|
||||||
judul: "",
|
judul: '',
|
||||||
deskripsi: "",
|
deskripsi: '',
|
||||||
kategoriBeritaId: "",
|
kategoriBeritaId: '',
|
||||||
imageId: "",
|
imageId: '',
|
||||||
content: "",
|
content: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Reset state lokal
|
|
||||||
setPreviewImage(null);
|
setPreviewImage(null);
|
||||||
setFile(null);
|
setFile(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return toast.warn("Pilih file gambar terlebih dahulu");
|
return toast.warn('Silakan pilih file gambar terlebih dahulu');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upload gambar dulu
|
|
||||||
const res = await ApiFetch.api.fileStorage.create.post({
|
const res = await ApiFetch.api.fileStorage.create.post({
|
||||||
file,
|
file,
|
||||||
name: file.name,
|
name: file.name,
|
||||||
@@ -51,40 +58,55 @@ export default function CreateBerita() {
|
|||||||
|
|
||||||
const uploaded = res.data?.data;
|
const uploaded = res.data?.data;
|
||||||
if (!uploaded?.id) {
|
if (!uploaded?.id) {
|
||||||
return toast.error("Gagal upload gambar");
|
return toast.error('Gagal mengunggah gambar, silakan coba lagi');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simpan ID gambar ke form
|
|
||||||
beritaState.berita.create.form.imageId = uploaded.id;
|
beritaState.berita.create.form.imageId = uploaded.id;
|
||||||
|
|
||||||
// Submit data berita
|
|
||||||
await beritaState.berita.create.create();
|
await beritaState.berita.create.create();
|
||||||
|
|
||||||
// Reset form setelah submit
|
|
||||||
resetForm();
|
resetForm();
|
||||||
router.push("/admin/desa/berita/list-berita")
|
router.push('/admin/desa/berita/list-berita');
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
{/* Header dengan tombol kembali */}
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
<Group mb="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
</Button>
|
<Button
|
||||||
</Box>
|
variant="subtle"
|
||||||
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
|
onClick={() => router.back()}
|
||||||
<Stack gap={"xs"}>
|
p="xs"
|
||||||
<Title order={3}>Create Berita</Title>
|
radius="md"
|
||||||
|
>
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<Title order={4} ml="sm" c="dark">
|
||||||
|
Tambah Berita
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Paper
|
||||||
|
w={{ base: '100%', md: '50%' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
|
label="Judul"
|
||||||
|
placeholder="Masukkan judul berita"
|
||||||
value={beritaState.berita.create.form.judul}
|
value={beritaState.berita.create.form.judul}
|
||||||
onChange={(val) => {
|
onChange={(e) => (beritaState.berita.create.form.judul = e.target.value)}
|
||||||
beritaState.berita.create.form.judul = val.target.value;
|
required
|
||||||
}}
|
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
|
|
||||||
placeholder="masukkan judul"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Kategori</Text>}
|
label="Kategori"
|
||||||
placeholder="Pilih kategori"
|
placeholder="Pilih kategori"
|
||||||
data={beritaState.kategoriBerita.findMany.data.map((item) => ({
|
data={beritaState.kategoriBerita.findMany.data.map((item) => ({
|
||||||
label: item.name,
|
label: item.name,
|
||||||
@@ -93,85 +115,84 @@ export default function CreateBerita() {
|
|||||||
value={beritaState.berita.create.form.kategoriBeritaId || null}
|
value={beritaState.berita.create.form.kategoriBeritaId || null}
|
||||||
onChange={(val: string | null) => {
|
onChange={(val: string | null) => {
|
||||||
if (val) {
|
if (val) {
|
||||||
const selected = beritaState.kategoriBerita.findMany.data?.find((item) => item.id === val);
|
const selected = beritaState.kategoriBerita.findMany.data?.find(
|
||||||
|
(item) => item.id === val
|
||||||
|
);
|
||||||
if (selected) {
|
if (selected) {
|
||||||
beritaState.berita.create.form.kategoriBeritaId = selected.id;
|
beritaState.berita.create.form.kategoriBeritaId = selected.id;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
beritaState.berita.create.form.kategoriBeritaId = "";
|
beritaState.berita.create.form.kategoriBeritaId = '';
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
searchable
|
searchable
|
||||||
clearable
|
clearable
|
||||||
nothingFoundMessage="Tidak ditemukan"
|
nothingFoundMessage="Tidak ditemukan"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
|
label="Deskripsi Singkat"
|
||||||
|
placeholder="Masukkan deskripsi berita"
|
||||||
value={beritaState.berita.create.form.deskripsi}
|
value={beritaState.berita.create.form.deskripsi}
|
||||||
onChange={(val) => {
|
onChange={(e) => (beritaState.berita.create.form.deskripsi = e.target.value)}
|
||||||
beritaState.berita.create.form.deskripsi = val.target.value;
|
|
||||||
}}
|
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>}
|
|
||||||
placeholder="masukkan deskripsi"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"md"} fw={"bold"}>Gambar</Text>
|
<Text fw="bold" fz="sm" mb={6}>
|
||||||
<Box>
|
Gambar Berita
|
||||||
<Dropzone
|
</Text>
|
||||||
onDrop={(files) => {
|
<Dropzone
|
||||||
const selectedFile = files[0]; // Ambil file pertama
|
onDrop={(files) => {
|
||||||
if (selectedFile) {
|
const selectedFile = files[0];
|
||||||
setFile(selectedFile);
|
if (selectedFile) {
|
||||||
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
|
setFile(selectedFile);
|
||||||
}
|
setPreviewImage(URL.createObjectURL(selectedFile));
|
||||||
}}
|
}
|
||||||
onReject={() => toast.error('File tidak valid.')}
|
}}
|
||||||
maxSize={5 * 1024 ** 2} // Maks 5MB
|
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
||||||
accept={{ 'image/*': [] }}
|
maxSize={5 * 1024 ** 2}
|
||||||
>
|
accept={{ 'image/*': [] }}
|
||||||
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
radius="md"
|
||||||
<Dropzone.Accept>
|
p="xl"
|
||||||
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
>
|
||||||
</Dropzone.Accept>
|
<Group justify="center" gap="xl" mih={180}>
|
||||||
<Dropzone.Reject>
|
<Dropzone.Accept>
|
||||||
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
<IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||||
</Dropzone.Reject>
|
</Dropzone.Accept>
|
||||||
<Dropzone.Idle>
|
<Dropzone.Reject>
|
||||||
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
<IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||||
</Dropzone.Idle>
|
</Dropzone.Reject>
|
||||||
|
<Dropzone.Idle>
|
||||||
|
<IconPhoto size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||||
|
</Dropzone.Idle>
|
||||||
|
</Group>
|
||||||
|
<Text ta="center" mt="sm" size="sm" color="dimmed">
|
||||||
|
Seret gambar atau klik untuk memilih file (maks 5MB)
|
||||||
|
</Text>
|
||||||
|
</Dropzone>
|
||||||
|
|
||||||
<div>
|
{previewImage && (
|
||||||
<Text size="xl" inline>
|
<Box mt="sm" style={{ textAlign: 'center' }}>
|
||||||
Drag gambar ke sini atau klik untuk pilih file
|
<Image
|
||||||
</Text>
|
src={previewImage}
|
||||||
<Text size="sm" c="dimmed" inline mt={7}>
|
alt="Preview Gambar"
|
||||||
Maksimal 5MB dan harus format gambar
|
radius="md"
|
||||||
</Text>
|
style={{
|
||||||
</div>
|
maxHeight: 200,
|
||||||
</Group>
|
objectFit: 'contain',
|
||||||
</Dropzone>
|
border: '1px solid #ddd',
|
||||||
|
}}
|
||||||
{/* Tampilkan preview kalau ada */}
|
loading="lazy"
|
||||||
{previewImage && (
|
/>
|
||||||
<Box mt="sm">
|
</Box>
|
||||||
<Image
|
)}
|
||||||
src={previewImage}
|
|
||||||
alt="Preview"
|
|
||||||
style={{
|
|
||||||
maxWidth: '100%',
|
|
||||||
maxHeight: '200px',
|
|
||||||
objectFit: 'contain',
|
|
||||||
borderRadius: '8px',
|
|
||||||
border: '1px solid #ddd',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"sm"} fw={"bold"}>Konten</Text>
|
<Text fz="sm" fw="bold" mb={6}>
|
||||||
|
Konten
|
||||||
|
</Text>
|
||||||
<CreateEditor
|
<CreateEditor
|
||||||
value={beritaState.berita.create.form.content}
|
value={beritaState.berita.create.form.content}
|
||||||
onChange={(htmlContent) => {
|
onChange={(htmlContent) => {
|
||||||
@@ -179,7 +200,21 @@ export default function CreateBerita() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan Berita</Button>
|
|
||||||
|
<Group justify="right">
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
|
color: '#fff',
|
||||||
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,6 +1,25 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Grid, GridCol, Image, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Group,
|
||||||
|
Image,
|
||||||
|
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 { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
|
import { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
@@ -9,15 +28,13 @@ import { useProxy } from 'valtio/utils';
|
|||||||
import HeaderSearch from '../../../_com/header';
|
import HeaderSearch from '../../../_com/header';
|
||||||
import stateDashboardBerita from '../../../_state/desa/berita';
|
import stateDashboardBerita from '../../../_state/desa/berita';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function Berita() {
|
function Berita() {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
title='Berita'
|
title="Berita"
|
||||||
placeholder='pencarian'
|
placeholder="Cari judul atau kategori berita..."
|
||||||
searchIcon={<IconSearch size={20} />}
|
searchIcon={<IconSearch size={20} />}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
@@ -28,103 +45,127 @@ function Berita() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ListBerita({ search }: { search: string }) {
|
function ListBerita({ search }: { search: string }) {
|
||||||
const beritaState = useProxy(stateDashboardBerita)
|
const beritaState = useProxy(stateDashboardBerita);
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
const {
|
|
||||||
data,
|
|
||||||
page,
|
|
||||||
totalPages,
|
|
||||||
loading,
|
|
||||||
load,
|
|
||||||
} = beritaState.berita.findMany;
|
|
||||||
|
|
||||||
|
const { data, page, totalPages, loading, load } = beritaState.berita.findMany;
|
||||||
|
|
||||||
// Fetch data when page or search changes
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
load(page, 10, search);
|
load(page, 10, search);
|
||||||
}, [page, search]);
|
}, [page, search]);
|
||||||
|
|
||||||
if (loading || !data) {
|
if (loading || !data) {
|
||||||
return <Skeleton h={500} />;
|
return (
|
||||||
|
<Stack py={10}>
|
||||||
|
<Skeleton height={600} radius="md" />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredData = data || [];
|
const filteredData = data || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={10}>
|
||||||
<Paper bg={colors["white-1"]} p={"md"}>
|
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||||
<Stack>
|
<Group justify="space-between" mb="md">
|
||||||
<Grid>
|
<Title order={4}>Daftar Berita</Title>
|
||||||
<GridCol span={{ base: 12, md: 11 }}>
|
<Tooltip label="Tambah Berita" withArrow>
|
||||||
<Text fz={"xl"} fw={"bold"}>
|
<Button
|
||||||
List Berita
|
leftSection={<IconCircleDashedPlus size={18} />}
|
||||||
</Text>
|
color="blue"
|
||||||
</GridCol>
|
variant="light"
|
||||||
<GridCol span={{ base: 12, md: 1 }}>
|
onClick={() => router.push('/admin/desa/berita/list-berita/create')}
|
||||||
<Button
|
|
||||||
onClick={() => router.push("/admin/desa/berita/list-berita/create")}
|
|
||||||
bg={colors["blue-button"]}
|
|
||||||
>
|
|
||||||
<IconCircleDashedPlus size={25} />
|
|
||||||
</Button>
|
|
||||||
</GridCol>
|
|
||||||
</Grid>
|
|
||||||
<Box style={{ overflowX: "auto" }}>
|
|
||||||
<Table
|
|
||||||
striped
|
|
||||||
withRowBorders
|
|
||||||
withTableBorder
|
|
||||||
style={{ minWidth: "700px" }}
|
|
||||||
>
|
>
|
||||||
<TableThead>
|
Tambah Baru
|
||||||
<TableTr>
|
</Button>
|
||||||
<TableTh w={250}>Judul</TableTh>
|
</Tooltip>
|
||||||
<TableTh w={250}>Kategori</TableTh>
|
</Group>
|
||||||
<TableTh w={250}>Image</TableTh>
|
|
||||||
<TableTh w={200}>Detail</TableTh>
|
<Box style={{ overflowX: 'auto' }}>
|
||||||
</TableTr>
|
<Table highlightOnHover>
|
||||||
</TableThead>
|
<TableThead>
|
||||||
<TableTbody>
|
<TableTr>
|
||||||
{filteredData.map((item) => (
|
<TableTh style={{ width: '30%' }}>Judul</TableTh>
|
||||||
|
<TableTh style={{ width: '20%' }}>Kategori</TableTh>
|
||||||
|
<TableTh style={{ width: '25%' }}>Gambar</TableTh>
|
||||||
|
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
|
||||||
|
</TableTr>
|
||||||
|
</TableThead>
|
||||||
|
<TableTbody>
|
||||||
|
{filteredData.length > 0 ? (
|
||||||
|
filteredData.map((item) => (
|
||||||
<TableTr key={item.id}>
|
<TableTr key={item.id}>
|
||||||
<TableTd>
|
<TableTd style={{ width: '30%' }}>
|
||||||
<Box w={100}>
|
<Box w={200}>
|
||||||
<Text truncate="end" fz={"sm"}>
|
<Text fw={500} truncate="end" lineClamp={1}>
|
||||||
{item.judul}
|
{item.judul}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd>{item.kategoriBerita?.name}</TableTd>
|
<TableTd style={{ width: '20%' }}>
|
||||||
<TableTd>
|
<Text fz="sm" c="dimmed">
|
||||||
<Image w={100} src={item.image?.link} alt="gambar" />
|
{item.kategoriBerita?.name || '-'}
|
||||||
|
</Text>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd>
|
<TableTd style={{ width: '25%' }}>
|
||||||
|
<Box
|
||||||
|
w={80}
|
||||||
|
h={80}
|
||||||
|
style={{ borderRadius: 8, overflow: 'hidden' }}
|
||||||
|
>
|
||||||
|
{item.image?.link ? (
|
||||||
|
<Image loading='lazy' src={item.image.link} alt="gambar" fit="cover" />
|
||||||
|
) : (
|
||||||
|
<Box bg={colors['blue-button']} w="100%" h="100%" />
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd style={{ width: '15%' }}>
|
||||||
<Button
|
<Button
|
||||||
bg={"green"}
|
variant="light"
|
||||||
|
color="blue"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
router.push(`/admin/desa/berita/list-berita/${item.id}`)
|
router.push(`/admin/desa/berita/list-berita/${item.id}`)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<IconDeviceImacCog size={25} />
|
<IconDeviceImacCog size={20} />
|
||||||
|
<Text ml={5}>Detail</Text>
|
||||||
</Button>
|
</Button>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
))}
|
))
|
||||||
</TableTbody>
|
) : (
|
||||||
</Table>
|
<TableTr>
|
||||||
</Box>
|
<TableTd colSpan={4}>
|
||||||
</Stack>
|
<Center py={20}>
|
||||||
|
<Text color="dimmed">
|
||||||
|
Tidak ada data berita yang cocok
|
||||||
|
</Text>
|
||||||
|
</Center>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
)}
|
||||||
|
</TableTbody>
|
||||||
|
</Table>
|
||||||
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
<Center>
|
<Center>
|
||||||
<Pagination
|
<Pagination
|
||||||
value={page}
|
value={page}
|
||||||
onChange={(newPage) => load(newPage)} // ini penting!
|
onChange={(newPage) => {
|
||||||
|
load(newPage, 10);
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
}}
|
||||||
total={totalPages}
|
total={totalPages}
|
||||||
mt="md"
|
mt="md"
|
||||||
mb="md"
|
mb="md"
|
||||||
|
color="blue"
|
||||||
|
radius="md"
|
||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Berita;
|
export default Berita;
|
||||||
|
|||||||
@@ -1,161 +0,0 @@
|
|||||||
'use client'
|
|
||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
|
||||||
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
|
||||||
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
|
|
||||||
import colors from '@/con/colors';
|
|
||||||
import ApiFetch from '@/lib/api-fetch';
|
|
||||||
import { Box, Button, Center, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
|
||||||
import { Dropzone } from '@mantine/dropzone';
|
|
||||||
import { IconArrowBack, IconImageInPicture, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { toast } from 'react-toastify';
|
|
||||||
import { useProxy } from 'valtio/utils';
|
|
||||||
|
|
||||||
|
|
||||||
function EditFoto() {
|
|
||||||
const fotoState = useProxy(stateGallery.foto)
|
|
||||||
const router = useRouter();
|
|
||||||
const params = useParams();
|
|
||||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
|
||||||
const [file, setFile] = useState<File | null>(null);
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
name: fotoState.update.form.name || '',
|
|
||||||
deskripsi: fotoState.update.form.deskripsi || '',
|
|
||||||
imagesId: fotoState.update.form.imagesId || ''
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const loadFoto = async () => {
|
|
||||||
const id = params?.id as string;
|
|
||||||
if (!id) return;
|
|
||||||
try {
|
|
||||||
const data = await fotoState.update.load(id);
|
|
||||||
if (data) {
|
|
||||||
setFormData({
|
|
||||||
name: data.name || '',
|
|
||||||
deskripsi: data.deskripsi || '',
|
|
||||||
imagesId: data.imageGalleryFoto?.id || ''
|
|
||||||
});
|
|
||||||
if (data?.imageGalleryFoto?.link) {
|
|
||||||
setPreviewImage(data.imageGalleryFoto.link);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading foto:', error);
|
|
||||||
toast.error('Gagal memuat data foto');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
loadFoto();
|
|
||||||
}, [params?.id]);
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
try {
|
|
||||||
fotoState.update.form = {
|
|
||||||
...fotoState.update.form,
|
|
||||||
name: formData.name,
|
|
||||||
deskripsi: formData.deskripsi,
|
|
||||||
imagesId: formData.imagesId
|
|
||||||
};
|
|
||||||
if (file) {
|
|
||||||
const res = await ApiFetch.api.fileStorage.create.post({
|
|
||||||
file,
|
|
||||||
name: file.name,
|
|
||||||
});
|
|
||||||
const uploaded = res.data?.data;
|
|
||||||
if (!uploaded?.id) {
|
|
||||||
return toast.error("Gagal upload gambar");
|
|
||||||
}
|
|
||||||
fotoState.update.form.imagesId = uploaded.id;
|
|
||||||
}
|
|
||||||
await fotoState.update.update();
|
|
||||||
toast.success('Foto berhasil diperbarui!');
|
|
||||||
router.push('/admin/desa/gallery/foto');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error updating foto:', error);
|
|
||||||
toast.error('Terjadi kesalahan saat memperbarui foto');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Box mb={10}>
|
|
||||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
|
||||||
<Stack gap={"xs"}>
|
|
||||||
<Title order={4}>Edit Foto</Title>
|
|
||||||
<TextInput
|
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Judul Foto</Text>}
|
|
||||||
placeholder='Masukkan judul foto'
|
|
||||||
value={formData.name}
|
|
||||||
onChange={(e) =>
|
|
||||||
(formData.name = e.target.value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Box>
|
|
||||||
<Text>Upload Foto</Text>
|
|
||||||
<Dropzone
|
|
||||||
onDrop={(files) => {
|
|
||||||
const selectedFile = files[0]; // Ambil file pertama
|
|
||||||
if (selectedFile) {
|
|
||||||
setFile(selectedFile);
|
|
||||||
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onReject={() => toast.error('File tidak valid.')}
|
|
||||||
maxSize={5 * 1024 ** 2} // Maks 5MB
|
|
||||||
accept={{ 'image/*': [] }}
|
|
||||||
>
|
|
||||||
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
|
||||||
<Dropzone.Accept>
|
|
||||||
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
|
||||||
</Dropzone.Accept>
|
|
||||||
<Dropzone.Reject>
|
|
||||||
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
|
||||||
</Dropzone.Reject>
|
|
||||||
<Dropzone.Idle>
|
|
||||||
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
|
||||||
</Dropzone.Idle>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Text size="xl" inline>
|
|
||||||
Drag gambar ke sini atau klik untuk pilih file
|
|
||||||
</Text>
|
|
||||||
<Text size="sm" c="dimmed" inline mt={7}>
|
|
||||||
Maksimal 5MB dan harus format gambar
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</Group>
|
|
||||||
</Dropzone>
|
|
||||||
|
|
||||||
{previewImage ? (
|
|
||||||
<Image alt="" src={previewImage} w={200} h={200} />
|
|
||||||
) : (
|
|
||||||
<Center w={200} h={200} bg={"gray"}>
|
|
||||||
<IconImageInPicture />
|
|
||||||
</Center>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Text fw={"bold"} fz={"sm"}>Deskripsi Foto</Text>
|
|
||||||
<EditEditor
|
|
||||||
value={fotoState.update.form.deskripsi}
|
|
||||||
onChange={(val) => {
|
|
||||||
fotoState.update.form.deskripsi = val;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<Group>
|
|
||||||
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
</Paper>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default EditFoto;
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
'use client'
|
|
||||||
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
|
|
||||||
import React from 'react';
|
|
||||||
import { useProxy } from 'valtio/utils';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
|
||||||
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
|
||||||
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
|
|
||||||
import colors from '@/con/colors';
|
|
||||||
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
|
||||||
|
|
||||||
function DetailFoto() {
|
|
||||||
const fotoState = useProxy(stateGallery.foto)
|
|
||||||
const [modalHapus, setModalHapus] = useState(false);
|
|
||||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
|
||||||
const params = useParams()
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
useShallowEffect(() => {
|
|
||||||
fotoState.findUnique.load(params?.id as string)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleHapus = () => {
|
|
||||||
if (selectedId) {
|
|
||||||
fotoState.delete.byId(selectedId)
|
|
||||||
setModalHapus(false)
|
|
||||||
setSelectedId(null)
|
|
||||||
router.push("/admin/desa/gallery/foto")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fotoState.findUnique.data) {
|
|
||||||
return (
|
|
||||||
<Stack py={10}>
|
|
||||||
<Skeleton h={500} />
|
|
||||||
</Stack>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Box mb={10}>
|
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
|
|
||||||
<Stack>
|
|
||||||
<Text fz={"xl"} fw={"bold"}>Detail Foto</Text>
|
|
||||||
{fotoState.findUnique.data ? (
|
|
||||||
<Paper key={fotoState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
|
|
||||||
<Stack gap={"xs"}>
|
|
||||||
<Box>
|
|
||||||
<Text fw={"bold"} fz={"lg"}>Judul</Text>
|
|
||||||
<Text fz={"lg"}>{fotoState.findUnique.data?.name}</Text>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Text fw={"bold"} fz={"lg"}>Tanggal Foto</Text>
|
|
||||||
<Text fz={"lg"}>{new Date(fotoState.findUnique.data?.createdAt).toDateString()}</Text>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
|
|
||||||
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: fotoState.findUnique.data?.deskripsi }} />
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
|
|
||||||
<Image w={{ base: 300, md: 350}} src={fotoState.findUnique.data?.imageGalleryFoto?.link} alt="gambar" />
|
|
||||||
</Box>
|
|
||||||
<Flex gap={"xs"} mt={10}>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
if (fotoState.findUnique.data) {
|
|
||||||
setSelectedId(fotoState.findUnique.data.id);
|
|
||||||
setModalHapus(true);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={fotoState.delete.loading || !fotoState.findUnique.data}
|
|
||||||
color={"red"}
|
|
||||||
>
|
|
||||||
<IconX size={20} />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
if (fotoState.findUnique.data) {
|
|
||||||
router.push(`/admin/desa/gallery/foto/${fotoState.findUnique.data.id}/edit`);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={!fotoState.findUnique.data}
|
|
||||||
color={"green"}
|
|
||||||
>
|
|
||||||
<IconEdit size={20} />
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
|
||||||
</Stack>
|
|
||||||
</Paper>
|
|
||||||
) : null}
|
|
||||||
</Stack>
|
|
||||||
</Paper>
|
|
||||||
|
|
||||||
{/* Modal Konfirmasi Hapus */}
|
|
||||||
<ModalKonfirmasiHapus
|
|
||||||
opened={modalHapus}
|
|
||||||
onClose={() => setModalHapus(false)}
|
|
||||||
onConfirm={handleHapus}
|
|
||||||
text='Apakah anda yakin ingin menghapus berita ini?'
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DetailFoto;
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
'use client'
|
|
||||||
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
|
|
||||||
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
|
|
||||||
import colors from '@/con/colors';
|
|
||||||
import ApiFetch from '@/lib/api-fetch';
|
|
||||||
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
|
||||||
import { Dropzone } from '@mantine/dropzone';
|
|
||||||
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { toast } from 'react-toastify';
|
|
||||||
import { useProxy } from 'valtio/utils';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function CreateFoto() {
|
|
||||||
const fotoState = useProxy(stateGallery.foto)
|
|
||||||
const router = useRouter();
|
|
||||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
|
||||||
const [file, setFile] = useState<File | null>(null);
|
|
||||||
|
|
||||||
const resetForm = () => {
|
|
||||||
fotoState.create.form = {
|
|
||||||
name: "",
|
|
||||||
deskripsi: "",
|
|
||||||
imagesId: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
setPreviewImage(null)
|
|
||||||
setFile(null)
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
if (!file) {
|
|
||||||
return toast.warn("Pilih file gambar terlebih dahulu");
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await ApiFetch.api.fileStorage.create.post({
|
|
||||||
file,
|
|
||||||
name: file.name,
|
|
||||||
});
|
|
||||||
|
|
||||||
const uploaded = res.data?.data;
|
|
||||||
if (!uploaded?.id) {
|
|
||||||
return toast.error("Gagal upload gambar");
|
|
||||||
}
|
|
||||||
|
|
||||||
fotoState.create.form.imagesId = uploaded.id;
|
|
||||||
await fotoState.create.create();
|
|
||||||
resetForm();
|
|
||||||
router.push("/admin/desa/gallery/foto")
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Box mb={10}>
|
|
||||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
|
||||||
<Stack gap={"xs"}>
|
|
||||||
<Title order={4}>Create Foto</Title>
|
|
||||||
<TextInput
|
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Judul Foto</Text>}
|
|
||||||
placeholder='Masukkan judul foto'
|
|
||||||
value={fotoState.create.form.name}
|
|
||||||
onChange={(val) => {
|
|
||||||
fotoState.create.form.name = val.target.value;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Box>
|
|
||||||
<Text fz={"md"} fw={"bold"}>Gambar</Text>
|
|
||||||
<Box>
|
|
||||||
<Dropzone
|
|
||||||
onDrop={(files) => {
|
|
||||||
const selectedFile = files[0]; // Ambil file pertama
|
|
||||||
if (selectedFile) {
|
|
||||||
setFile(selectedFile);
|
|
||||||
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onReject={() => toast.error('File tidak valid.')}
|
|
||||||
maxSize={5 * 1024 ** 2} // Maks 5MB
|
|
||||||
accept={{ 'image/*': [] }}
|
|
||||||
>
|
|
||||||
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
|
||||||
<Dropzone.Accept>
|
|
||||||
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
|
||||||
</Dropzone.Accept>
|
|
||||||
<Dropzone.Reject>
|
|
||||||
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
|
||||||
</Dropzone.Reject>
|
|
||||||
<Dropzone.Idle>
|
|
||||||
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
|
||||||
</Dropzone.Idle>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Text size="xl" inline>
|
|
||||||
Drag gambar ke sini atau klik untuk pilih file
|
|
||||||
</Text>
|
|
||||||
<Text size="sm" c="dimmed" inline mt={7}>
|
|
||||||
Maksimal 5MB dan harus format gambar
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</Group>
|
|
||||||
</Dropzone>
|
|
||||||
|
|
||||||
{/* Tampilkan preview kalau ada */}
|
|
||||||
{previewImage && (
|
|
||||||
<Box mt="sm">
|
|
||||||
<Image
|
|
||||||
src={previewImage}
|
|
||||||
alt="Preview"
|
|
||||||
style={{
|
|
||||||
maxWidth: '100%',
|
|
||||||
maxHeight: '200px',
|
|
||||||
objectFit: 'contain',
|
|
||||||
borderRadius: '8px',
|
|
||||||
border: '1px solid #ddd',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Text fw={"bold"} fz={"sm"}>Deskripsi Foto</Text>
|
|
||||||
<CreateEditor
|
|
||||||
value={fotoState.create.form.deskripsi}
|
|
||||||
onChange={(val) => {
|
|
||||||
fotoState.create.form.deskripsi = val;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<Group>
|
|
||||||
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
</Paper>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CreateFoto;
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import colors from "@/con/colors";
|
|
||||||
import stateFileStorage from "@/state/state-list-image";
|
import stateFileStorage from "@/state/state-list-image";
|
||||||
import {
|
import {
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
Box,
|
Box,
|
||||||
|
Card,
|
||||||
Flex,
|
Flex,
|
||||||
Group,
|
Group,
|
||||||
Image,
|
Image,
|
||||||
@@ -13,7 +13,8 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title
|
Title,
|
||||||
|
Tooltip,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useShallowEffect } from "@mantine/hooks";
|
import { useShallowEffect } from "@mantine/hooks";
|
||||||
import { IconSearch, IconTrash, IconX } from "@tabler/icons-react";
|
import { IconSearch, IconTrash, IconX } from "@tabler/icons-react";
|
||||||
@@ -29,95 +30,130 @@ export default function ListImage() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
let timeOut: NodeJS.Timer;
|
let timeOut: NodeJS.Timer;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack p={"lg"}>
|
<Stack p="lg" gap="lg">
|
||||||
<Flex justify="space-between">
|
<Flex justify="space-between" align="center" wrap="wrap" gap="md">
|
||||||
<Title order={3}>List Foto</Title>
|
<Title order={2} fw={700}>
|
||||||
|
Galeri Foto
|
||||||
|
</Title>
|
||||||
<TextInput
|
<TextInput
|
||||||
radius={"lg"}
|
radius="xl"
|
||||||
leftSection={<IconSearch />}
|
size="md"
|
||||||
|
placeholder="Cari foto berdasarkan nama..."
|
||||||
|
leftSection={<IconSearch size={18} />}
|
||||||
rightSection={
|
rightSection={
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="transparent"
|
variant="light"
|
||||||
onClick={() => {
|
color="gray"
|
||||||
stateFileStorage.load();
|
radius="xl"
|
||||||
}}
|
onClick={() => stateFileStorage.load()}
|
||||||
>
|
>
|
||||||
<IconX />
|
<IconX size={18} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
}
|
}
|
||||||
placeholder="Pencarian"
|
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
if (timeOut) clearTimeout(timeOut);
|
if (timeOut) clearTimeout(timeOut);
|
||||||
timeOut = setTimeout(() => {
|
timeOut = setTimeout(() => {
|
||||||
stateFileStorage.load({ search: e.target.value });
|
stateFileStorage.load({ search: e.target.value });
|
||||||
}, 200);
|
}, 300);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Paper bg={colors['white-1']} p={'md'}>
|
|
||||||
<SimpleGrid
|
<Paper withBorder radius="lg" p="md" shadow="sm">
|
||||||
cols={{
|
{list && list.length > 0 ? (
|
||||||
base: 3,
|
<SimpleGrid
|
||||||
md: 5,
|
cols={{ base: 2, sm: 3, md: 5, lg: 8 }}
|
||||||
lg: 10,
|
spacing="md"
|
||||||
}}
|
verticalSpacing="md"
|
||||||
>
|
>
|
||||||
{list &&
|
{list.map((v, k) => (
|
||||||
list.map((v, k) => {
|
<Card
|
||||||
return (
|
key={k}
|
||||||
<Paper key={k} shadow="sm">
|
withBorder
|
||||||
<Stack pos={"relative"} gap={0} justify="space-between">
|
radius="md"
|
||||||
<motion.div
|
shadow="sm"
|
||||||
onClick={() => {
|
className="hover:shadow-md transition-all duration-200"
|
||||||
// copy to clipboard
|
>
|
||||||
navigator.clipboard.writeText(v.url);
|
<Stack gap="xs">
|
||||||
toast("Berhasil disalin");
|
<motion.div
|
||||||
}}
|
onClick={() => {
|
||||||
whileHover={{ scale: 1.05 }}
|
navigator.clipboard.writeText(v.url);
|
||||||
whileTap={{ scale: 0.8 }}
|
toast("Tautan foto berhasil disalin");
|
||||||
>
|
}}
|
||||||
<Image
|
whileHover={{ scale: 1.05 }}
|
||||||
h={100}
|
whileTap={{ scale: 0.95 }}
|
||||||
src={v.url + "?size=100"}
|
style={{ cursor: "pointer" }}
|
||||||
alt={v.name}
|
>
|
||||||
fit="cover"
|
<Image
|
||||||
loading="lazy"
|
src={`${v.url}?size=200`}
|
||||||
style={{
|
alt={v.name}
|
||||||
objectFit: "cover",
|
radius="md"
|
||||||
objectPosition: "center",
|
h={120}
|
||||||
}}
|
fit="cover"
|
||||||
/>
|
loading="lazy"
|
||||||
</motion.div>
|
/>
|
||||||
<Box p={"md"} h={54}>
|
</motion.div>
|
||||||
<Text lineClamp={2} fz={"xs"}>
|
|
||||||
{v.name}
|
<Box>
|
||||||
</Text>
|
<Text size="sm" fw={500} lineClamp={2}>
|
||||||
</Box>
|
{v.name}
|
||||||
<Group justify="end">
|
</Text>
|
||||||
<IconTrash
|
</Box>
|
||||||
|
|
||||||
|
<Group justify="space-between" align="center" pt="xs">
|
||||||
|
<Tooltip label="Hapus foto" withArrow>
|
||||||
|
<ActionIcon
|
||||||
|
variant="subtle"
|
||||||
color="red"
|
color="red"
|
||||||
onClick={() => {
|
radius="md"
|
||||||
stateFileStorage.del({ name: v.name }).finally(() => {
|
onClick={() => {
|
||||||
toast("Berhasil dihapus");
|
stateFileStorage
|
||||||
});
|
.del({ id: v.id })
|
||||||
}}
|
.finally(() => toast("Foto berhasil dihapus"));
|
||||||
/>
|
}}
|
||||||
</Group>
|
>
|
||||||
</Stack>
|
<IconTrash size={18} />
|
||||||
</Paper>
|
</ActionIcon>
|
||||||
);
|
</Tooltip>
|
||||||
})}
|
</Group>
|
||||||
</SimpleGrid>
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</SimpleGrid>
|
||||||
|
) : (
|
||||||
|
<Stack align="center" justify="center" py="xl" gap="sm">
|
||||||
|
<Image
|
||||||
|
src="https://cdn-icons-png.flaticon.com/512/4076/4076549.png"
|
||||||
|
alt="Kosong"
|
||||||
|
w={120}
|
||||||
|
h={120}
|
||||||
|
fit="contain"
|
||||||
|
opacity={0.7}
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
<Text c="dimmed" ta="center">
|
||||||
|
Belum ada foto yang tersedia
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
</Paper>
|
</Paper>
|
||||||
{total && (
|
|
||||||
<Pagination
|
{total && total > 1 && (
|
||||||
total={total}
|
<Flex justify="center">
|
||||||
onChange={(e) => {
|
<Pagination
|
||||||
stateFileStorage.page = e;
|
total={total}
|
||||||
stateFileStorage.load();
|
value={stateFileStorage.page} // Changed from page to value
|
||||||
}}
|
size="md"
|
||||||
/>
|
radius="md"
|
||||||
|
withEdges
|
||||||
|
onChange={(page) => {
|
||||||
|
stateFileStorage.load({ page });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</Flex>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } 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';
|
||||||
|
|
||||||
function LayoutTabsGallery({ children }: { children: React.ReactNode }) {
|
function LayoutTabsGallery({ children }: { children: React.ReactNode }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -12,16 +13,21 @@ function LayoutTabsGallery({ children }: { children: React.ReactNode }) {
|
|||||||
{
|
{
|
||||||
label: "Foto",
|
label: "Foto",
|
||||||
value: "foto",
|
value: "foto",
|
||||||
href: "/admin/desa/gallery/foto"
|
href: "/admin/desa/gallery/foto",
|
||||||
|
icon: <IconPhoto size={18} stroke={1.8} />,
|
||||||
|
tooltip: "Kelola foto-foto galeri desa"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Video",
|
label: "Video",
|
||||||
value: "video",
|
value: "video",
|
||||||
href: "/admin/desa/gallery/video"
|
href: "/admin/desa/gallery/video",
|
||||||
|
icon: <IconVideo size={18} stroke={1.8} />,
|
||||||
|
tooltip: "Kelola video galeri desa"
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const curentTab = tabs.find(tab => tab.href === pathname)
|
|
||||||
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
|
const currentTab = tabs.find(tab => tab.href === pathname)
|
||||||
|
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
|
||||||
|
|
||||||
const handleTabChange = (value: string | null) => {
|
const handleTabChange = (value: string | null) => {
|
||||||
const tab = tabs.find(t => t.value === value)
|
const tab = tabs.find(t => t.value === value)
|
||||||
@@ -39,24 +45,71 @@ function LayoutTabsGallery({ children }: { children: React.ReactNode }) {
|
|||||||
}, [pathname])
|
}, [pathname])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack gap="lg">
|
||||||
<Title order={3}>Gallery</Title>
|
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Gallery</Title>
|
||||||
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
|
<Tabs
|
||||||
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
|
color={colors["blue-button"]}
|
||||||
{tabs.map((e, i) => (
|
variant="pills"
|
||||||
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
|
value={activeTab}
|
||||||
))}
|
onChange={handleTabChange}
|
||||||
</TabsList>
|
radius="lg"
|
||||||
{tabs.map((e, i) => (
|
keepMounted={false}
|
||||||
<TabsPanel key={i} value={e.value}>
|
>
|
||||||
{/* Konten dummy, bisa diganti tergantung routing */}
|
{/* ✅ Scroll horizontal wrapper */}
|
||||||
<></>
|
<ScrollArea type="auto" offsetScrollbars>
|
||||||
|
<TabsList
|
||||||
|
p="sm"
|
||||||
|
style={{
|
||||||
|
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||||
|
borderRadius: "1rem",
|
||||||
|
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||||
|
display: "flex",
|
||||||
|
flexWrap: "nowrap",
|
||||||
|
gap: "0.5rem",
|
||||||
|
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tabs.map((tab, i) => (
|
||||||
|
<Tooltip
|
||||||
|
key={i}
|
||||||
|
label={tab.tooltip}
|
||||||
|
position="bottom"
|
||||||
|
withArrow
|
||||||
|
transitionProps={{ transition: 'pop', duration: 200 }}
|
||||||
|
>
|
||||||
|
<TabsTab
|
||||||
|
value={tab.value}
|
||||||
|
leftSection={tab.icon}
|
||||||
|
style={{
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: "0.9rem",
|
||||||
|
transition: "all 0.2s ease",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</TabsTab>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
|
{tabs.map((tab, i) => (
|
||||||
|
<TabsPanel
|
||||||
|
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)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<>{children}</>
|
||||||
</TabsPanel>
|
</TabsPanel>
|
||||||
))}
|
))}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
{children}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LayoutTabsGallery;
|
export default LayoutTabsGallery;
|
||||||
|
|||||||
@@ -3,7 +3,16 @@
|
|||||||
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
||||||
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
|
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Tooltip
|
||||||
|
} from '@mantine/core';
|
||||||
import { IconArrowBack } from '@tabler/icons-react';
|
import { IconArrowBack } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
@@ -12,9 +21,9 @@ import { useProxy } from 'valtio/utils';
|
|||||||
import { convertYoutubeUrlToEmbed } from '../../../lib/youtube-utils';
|
import { convertYoutubeUrlToEmbed } from '../../../lib/youtube-utils';
|
||||||
|
|
||||||
function EditVideo() {
|
function EditVideo() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const videoState = useProxy(stateGallery.video)
|
const videoState = useProxy(stateGallery.video);
|
||||||
const params = useParams()
|
const params = useParams();
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
@@ -30,7 +39,7 @@ function EditVideo() {
|
|||||||
const data = await videoState.update.load(id);
|
const data = await videoState.update.load(id);
|
||||||
if (data) {
|
if (data) {
|
||||||
setFormData({
|
setFormData({
|
||||||
name: data.name || '',
|
name: data.name || '',
|
||||||
deskripsi: data.deskripsi || '',
|
deskripsi: data.deskripsi || '',
|
||||||
linkVideo: data.linkVideo || '',
|
linkVideo: data.linkVideo || '',
|
||||||
});
|
});
|
||||||
@@ -66,27 +75,36 @@ function EditVideo() {
|
|||||||
console.error('Error updating video:', error);
|
console.error('Error updating video:', error);
|
||||||
toast.error('Terjadi kesalahan saat memperbarui video');
|
toast.error('Terjadi kesalahan saat memperbarui video');
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
<Group mb="md">
|
||||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
</Button>
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
</Box>
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
<Title order={4} ml="sm" c="dark">
|
||||||
<Stack gap={"xs"}>
|
Edit Video
|
||||||
<Title order={4}>Edit Video</Title>
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Paper
|
||||||
|
w={{ base: '100%', md: '50%' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Judul Video</Text>}
|
label="Judul Video"
|
||||||
placeholder='Masukkan judul video'
|
placeholder="Masukkan judul video"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={(val) => {
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
setFormData({ ...formData, name: val.target.value });
|
required
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
@@ -94,36 +112,46 @@ function EditVideo() {
|
|||||||
label="Link Video YouTube"
|
label="Link Video YouTube"
|
||||||
placeholder="https://www.youtube.com/watch?v=abc123"
|
placeholder="https://www.youtube.com/watch?v=abc123"
|
||||||
value={formData.linkVideo}
|
value={formData.linkVideo}
|
||||||
onChange={(e) => {
|
onChange={(e) => setFormData({ ...formData, linkVideo: e.currentTarget.value })}
|
||||||
setFormData({ ...formData, linkVideo: e.currentTarget.value });
|
|
||||||
}}
|
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{embedLink && (
|
{embedLink && (
|
||||||
<iframe
|
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
|
||||||
className="rounded"
|
<iframe
|
||||||
width="100%"
|
className="rounded"
|
||||||
height="200"
|
width="100%"
|
||||||
src={embedLink}
|
height="220"
|
||||||
title="Preview Video"
|
src={embedLink}
|
||||||
allowFullScreen
|
title="Preview Video"
|
||||||
></iframe>
|
allowFullScreen
|
||||||
|
></iframe>
|
||||||
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw={"bold"} fz={"sm"}>Deskripsi Video</Text>
|
<Title order={6} fw="bold" fz="sm" mb={6}>
|
||||||
|
Deskripsi Video
|
||||||
|
</Title>
|
||||||
<EditEditor
|
<EditEditor
|
||||||
value={formData.deskripsi}
|
value={formData.deskripsi}
|
||||||
onChange={(val) => {
|
onChange={(val) => setFormData({ ...formData, deskripsi: val })}
|
||||||
setFormData({ ...formData, deskripsi: val });
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Group>
|
<Group justify="right">
|
||||||
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
|
color: '#fff',
|
||||||
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -2,107 +2,145 @@
|
|||||||
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||||
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
|
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
|
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
|
|
||||||
function DetailVideo() {
|
function DetailVideo() {
|
||||||
const videoState = useProxy(stateGallery.video)
|
const videoState = useProxy(stateGallery.video);
|
||||||
const [modalHapus, setModalHapus] = useState(false);
|
const [modalHapus, setModalHapus] = useState(false);
|
||||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||||
const params = useParams()
|
const params = useParams();
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
videoState.findUnique.load(params?.id as string)
|
videoState.findUnique.load(params?.id as string);
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
const handleHapus = () => {
|
const handleHapus = () => {
|
||||||
if (selectedId) {
|
if (selectedId) {
|
||||||
videoState.delete.byId(selectedId)
|
videoState.delete.byId(selectedId);
|
||||||
setModalHapus(false)
|
setModalHapus(false);
|
||||||
setSelectedId(null)
|
setSelectedId(null);
|
||||||
router.push("/admin/desa/gallery/video")
|
router.push("/admin/desa/gallery/video");
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
if (!videoState.findUnique.data) {
|
if (!videoState.findUnique.data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton h={500} />
|
<Skeleton height={500} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = videoState.findUnique.data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box py={10}>
|
||||||
<Box mb={10}>
|
{/* Tombol Kembali */}
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
<Button
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
variant="subtle"
|
||||||
</Button>
|
onClick={() => router.back()}
|
||||||
</Box>
|
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
|
||||||
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
|
mb={15}
|
||||||
<Stack>
|
>
|
||||||
<Text fz={"xl"} fw={"bold"}>Detail Video</Text>
|
Kembali
|
||||||
{videoState.findUnique.data ? (
|
</Button>
|
||||||
<Paper key={videoState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
|
|
||||||
<Stack gap={"xs"}>
|
{/* Detail Video */}
|
||||||
<Box>
|
<Paper
|
||||||
<Text fw={"bold"} fz={"lg"}>Judul</Text>
|
withBorder
|
||||||
<Text fz={"lg"}>{videoState.findUnique.data?.name}</Text>
|
w={{ base: "100%", md: "50%" }}
|
||||||
</Box>
|
bg={colors['white-1']}
|
||||||
<Box>
|
p="lg"
|
||||||
<Text fw={"bold"} fz={"lg"}>Video</Text>
|
radius="md"
|
||||||
<Box component="iframe"
|
shadow="sm"
|
||||||
src={convertToEmbedUrl(videoState.findUnique.data?.linkVideo)}
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
|
||||||
|
Detail Video
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
|
||||||
|
<Stack gap="sm">
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">Judul</Text>
|
||||||
|
<Text fz="md" c="dimmed">{data?.name || '-'}</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">Video</Text>
|
||||||
|
{data?.linkVideo ? (
|
||||||
|
<Box
|
||||||
|
component="iframe"
|
||||||
|
src={convertToEmbedUrl(data.linkVideo)}
|
||||||
width="100%"
|
width="100%"
|
||||||
height={300}
|
height={300}
|
||||||
allowFullScreen
|
allowFullScreen
|
||||||
style={{ borderRadius: 8 }}
|
style={{ borderRadius: 8 }}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<Text fz="sm" c="dimmed">Tidak ada video</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
</Box>
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">Tanggal Video</Text>
|
||||||
|
<Text fz="md" c="dimmed">
|
||||||
|
{data?.createdAt ? new Date(data.createdAt).toDateString() : '-'}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw={"bold"} fz={"lg"}>Tanggal Video</Text>
|
<Text fz="lg" fw="bold">Deskripsi</Text>
|
||||||
<Text fz={"lg"}>{new Date(videoState.findUnique.data?.createdAt).toDateString()}</Text>
|
{data?.deskripsi ? (
|
||||||
</Box>
|
<Text
|
||||||
<Box>
|
fz="md"
|
||||||
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
|
c="dimmed"
|
||||||
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: videoState.findUnique.data?.deskripsi }} />
|
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
|
||||||
</Box>
|
/>
|
||||||
<Flex gap={"xs"} mt={10}>
|
) : (
|
||||||
|
<Text fz="sm" c="dimmed">Tidak ada deskripsi</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Tombol Aksi */}
|
||||||
|
<Group gap="sm">
|
||||||
|
<Tooltip label="Hapus Video" withArrow position="top">
|
||||||
<Button
|
<Button
|
||||||
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (videoState.findUnique.data) {
|
setSelectedId(data.id);
|
||||||
setSelectedId(videoState.findUnique.data.id);
|
setModalHapus(true);
|
||||||
setModalHapus(true);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
disabled={videoState.delete.loading || !videoState.findUnique.data}
|
variant="light"
|
||||||
color={"red"}
|
radius="md"
|
||||||
|
size="md"
|
||||||
>
|
>
|
||||||
<IconX size={20} />
|
<IconTrash size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip label="Edit Video" withArrow position="top">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
color="green"
|
||||||
if (videoState.findUnique.data) {
|
onClick={() =>
|
||||||
router.push(`/admin/desa/gallery/video/${videoState.findUnique.data.id}/edit`);
|
router.push(`/admin/desa/gallery/video/${data.id}/edit`)
|
||||||
}
|
}
|
||||||
}}
|
variant="light"
|
||||||
disabled={!videoState.findUnique.data}
|
radius="md"
|
||||||
color={"green"}
|
size="md"
|
||||||
>
|
>
|
||||||
<IconEdit size={20} />
|
<IconEdit size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Tooltip>
|
||||||
</Stack>
|
</Group>
|
||||||
</Paper>
|
</Stack>
|
||||||
) : null}
|
</Paper>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
@@ -111,17 +149,16 @@ function DetailVideo() {
|
|||||||
opened={modalHapus}
|
opened={modalHapus}
|
||||||
onClose={() => setModalHapus(false)}
|
onClose={() => setModalHapus(false)}
|
||||||
onConfirm={handleHapus}
|
onConfirm={handleHapus}
|
||||||
text='Apakah anda yakin ingin menghapus berita ini?'
|
text="Apakah Anda yakin ingin menghapus video ini?"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
function convertToEmbedUrl(youtubeUrl: string): string {
|
function convertToEmbedUrl(youtubeUrl: string): string {
|
||||||
try {
|
try {
|
||||||
const url = new URL(youtubeUrl);
|
const url = new URL(youtubeUrl);
|
||||||
const videoId = url.searchParams.get("v");
|
const videoId = url.searchParams.get("v");
|
||||||
if (!videoId) return youtubeUrl;
|
if (!videoId) return youtubeUrl;
|
||||||
|
|
||||||
return `https://www.youtube.com/embed/${videoId}`;
|
return `https://www.youtube.com/embed/${videoId}`;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error converting YouTube URL to embed:", err);
|
console.error("Error converting YouTube URL to embed:", err);
|
||||||
|
|||||||
@@ -1,8 +1,18 @@
|
|||||||
'use client'
|
'use client';
|
||||||
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
|
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
|
||||||
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
|
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mantine/core';
|
||||||
import { IconArrowBack } from '@tabler/icons-react';
|
import { IconArrowBack } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
@@ -10,77 +20,104 @@ import { toast } from 'react-toastify';
|
|||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import { convertYoutubeUrlToEmbed } from '../../lib/youtube-utils';
|
import { convertYoutubeUrlToEmbed } from '../../lib/youtube-utils';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function CreateVideo() {
|
function CreateVideo() {
|
||||||
const videoState = useProxy(stateGallery.video)
|
const videoState = useProxy(stateGallery.video);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [link, setLink] = useState("");
|
const [link, setLink] = useState('');
|
||||||
const embedLink = convertYoutubeUrlToEmbed(link);
|
const embedLink = convertYoutubeUrlToEmbed(link);
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
videoState.create.form = {
|
videoState.create.form = {
|
||||||
name: "",
|
name: '',
|
||||||
deskripsi: "",
|
deskripsi: '',
|
||||||
linkVideo: "",
|
linkVideo: '',
|
||||||
};
|
};
|
||||||
|
setLink('');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!embedLink) {
|
if (!embedLink) {
|
||||||
toast.error("Link YouTube tidak valid. Pastikan formatnya benar.");
|
toast.error('Link YouTube tidak valid. Pastikan formatnya benar.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
videoState.create.form.linkVideo = embedLink; // pastikan diset di sini juga (jaga-jaga)
|
videoState.create.form.linkVideo = embedLink;
|
||||||
await videoState.create.create();
|
await videoState.create.create();
|
||||||
resetForm();
|
resetForm();
|
||||||
router.push("/admin/desa/gallery/video");
|
router.push('/admin/desa/gallery/video');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
{/* Header Back Button + Title */}
|
||||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
<Group mb="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
</Button>
|
<Button
|
||||||
</Box>
|
variant="subtle"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
p="xs"
|
||||||
|
radius="md"
|
||||||
|
>
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<Title order={4} ml="sm" c="dark">
|
||||||
|
Tambah Video
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
{/* Card Form */}
|
||||||
<Stack gap={"xs"}>
|
<Paper
|
||||||
<Title order={4}>Create Video</Title>
|
w={{ base: '100%', md: '50%' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
{/* Judul */}
|
||||||
<TextInput
|
<TextInput
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Judul Video</Text>}
|
label="Judul Video"
|
||||||
placeholder='Masukkan judul video'
|
placeholder="Masukkan judul video"
|
||||||
value={videoState.create.form.name}
|
value={videoState.create.form.name}
|
||||||
onChange={(val) => {
|
onChange={(e) => {
|
||||||
videoState.create.form.name = val.target.value;
|
videoState.create.form.name = e.currentTarget.value;
|
||||||
}}
|
}}
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<Box>
|
|
||||||
<Stack gap={"xs"}>
|
|
||||||
<TextInput
|
|
||||||
label="Link Video YouTube"
|
|
||||||
placeholder="https://www.youtube.com/watch?v=abc123"
|
|
||||||
value={link}
|
|
||||||
onChange={(e) => {
|
|
||||||
setLink(e.currentTarget.value);
|
|
||||||
}}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
{embedLink && (
|
{/* Link YouTube */}
|
||||||
<iframe
|
<TextInput
|
||||||
style={{ borderRadius: 10, width: "100%", height: 400 }}
|
label="Link Video YouTube"
|
||||||
src={embedLink}
|
placeholder="https://www.youtube.com/watch?v=abc123"
|
||||||
title="Preview Video"
|
value={link}
|
||||||
allowFullScreen
|
onChange={(e) => setLink(e.currentTarget.value)}
|
||||||
></iframe>
|
required
|
||||||
)}
|
/>
|
||||||
</Stack>
|
|
||||||
</Box>
|
{/* Preview Video */}
|
||||||
|
{embedLink && (
|
||||||
|
<Box mt="sm">
|
||||||
|
<iframe
|
||||||
|
style={{
|
||||||
|
borderRadius: 10,
|
||||||
|
width: '100%',
|
||||||
|
height: 400,
|
||||||
|
border: '1px solid #ddd',
|
||||||
|
}}
|
||||||
|
src={embedLink}
|
||||||
|
title="Preview Video"
|
||||||
|
allowFullScreen
|
||||||
|
></iframe>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Deskripsi */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw={"bold"} fz={"sm"}>Deskripsi Video</Text>
|
<Text fw="bold" fz="sm" mb={6}>
|
||||||
|
Deskripsi Video
|
||||||
|
</Text>
|
||||||
<CreateEditor
|
<CreateEditor
|
||||||
value={videoState.create.form.deskripsi}
|
value={videoState.create.form.deskripsi}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
@@ -88,8 +125,21 @@ function CreateVideo() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Group>
|
|
||||||
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
|
{/* Button Submit */}
|
||||||
|
<Group justify="right">
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
|
color: '#fff',
|
||||||
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||