Compare commits
2 Commits
nico/1-jan
...
nico/6-jan
| Author | SHA1 | Date | |
|---|---|---|---|
| 503da91ce6 | |||
| daaed8089b |
30
prisma/data/fetchWithRetry.ts
Normal file
30
prisma/data/fetchWithRetry.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
export default async function fetchWithRetry(
|
||||
url: string,
|
||||
retries = 3,
|
||||
timeoutMs = 20000
|
||||
) {
|
||||
for (let attempt = 1; attempt <= retries; attempt++) {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
try {
|
||||
const res = await fetch(url, { signal: controller.signal });
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP ${res.status} ${res.statusText}`);
|
||||
}
|
||||
|
||||
return res;
|
||||
} catch (err) {
|
||||
console.warn(`⚠️ Download attempt ${attempt} failed`);
|
||||
|
||||
if (attempt === retries) {
|
||||
throw err;
|
||||
}
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Unreachable");
|
||||
}
|
||||
@@ -1,137 +1,120 @@
|
||||
[
|
||||
{
|
||||
"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",
|
||||
"id": "cmk27746i0000vnso2aspwf9g",
|
||||
"name": "Eqlrr1W-pK8ShMGqgPGL3-desktop.webp",
|
||||
"realName": "perbekel.png",
|
||||
"path": "uploads/images",
|
||||
"mimeType": "image/webp",
|
||||
"link": "/api/fileStorage/findUnique/Vbj_osnMJUkGEQGDTLwV--desktop.webp",
|
||||
"link": "/api/fileStorage/findUnique/Eqlrr1W-pK8ShMGqgPGL3-desktop.webp",
|
||||
"category": "image"
|
||||
}
|
||||
,
|
||||
{
|
||||
"id": "cmk20mg320000vnevxy0k73fr",
|
||||
"name": "thpgPSJkBxUIRajZt3AVo-desktop.webp",
|
||||
"realName": "bares.png",
|
||||
"path": "uploads/images",
|
||||
"mimeType": "image/webp",
|
||||
"link": "/api/fileStorage/findUnique/thpgPSJkBxUIRajZt3AVo-desktop.webp",
|
||||
"category": "image"
|
||||
},
|
||||
{
|
||||
"id": "cmff3joae0000vn6h8sgs0ilg",
|
||||
"name": "7hox9spUxj56hY_EBYLnj-desktop.webp",
|
||||
"id": "cmk20nqmu0001vnevfte29rk0",
|
||||
"name": "ubna9N6r7RgVWN5plO5mq-desktop.webp",
|
||||
"realName": "bicara-darma.png",
|
||||
"path": "uploads/images",
|
||||
"mimeType": "image/webp",
|
||||
"link": "/api/fileStorage/findUnique/ubna9N6r7RgVWN5plO5mq-desktop.webp",
|
||||
"category": "image"
|
||||
},
|
||||
{
|
||||
"id": "cmk228urs0007vnevi5b66bqn",
|
||||
"name": "Z4i2RRnnlHq2iWj94ldyo-desktop.webp",
|
||||
"realName": "daves.png",
|
||||
"path": "uploads/images",
|
||||
"mimeType": "image/webp",
|
||||
"link": "/api/fileStorage/findUnique/Z4i2RRnnlHq2iWj94ldyo-desktop.webp",
|
||||
"category": "image"
|
||||
},
|
||||
{
|
||||
"id": "cmk20nyen0002vnevd0hfr3u8",
|
||||
"name": "y4yaE4XdUP1TSUGhWPW9h-desktop.webp",
|
||||
"realName": "mangan.png",
|
||||
"path": "uploads/images",
|
||||
"mimeType": "image/webp",
|
||||
"link": "/api/fileStorage/findUnique/y4yaE4XdUP1TSUGhWPW9h-desktop.webp",
|
||||
"category": "image"
|
||||
},
|
||||
{
|
||||
"id": "cmk20o7mf0003vnevohrksm1d",
|
||||
"name": "Vr7CoaYDpk2dIkHx9PxRj-desktop.webp",
|
||||
"realName": "gelah-melah.png",
|
||||
"path": "uploads/images",
|
||||
"mimeType": "image/webp",
|
||||
"link": "/api/fileStorage/findUnique/Vr7CoaYDpk2dIkHx9PxRj-desktop.webp",
|
||||
"category": "image"
|
||||
},
|
||||
{
|
||||
"id": "cmk20of8m0004vnev9ujy5o0l",
|
||||
"name": "ceoB_sg-HOzljN8j_2nZA-desktop.webp",
|
||||
"realName": "inovasi-desa-darmasaba.png",
|
||||
"path": "uploads/images",
|
||||
"mimeType": "image/webp",
|
||||
"link": "/api/fileStorage/findUnique/ceoB_sg-HOzljN8j_2nZA-desktop.webp",
|
||||
"category": "image"
|
||||
},
|
||||
{
|
||||
"id": "cmk20omzq0005vnevgi6f4edu",
|
||||
"name": "vOy5YVUXfHXfiFOHylIN7-desktop.webp",
|
||||
"realName": "pdkt.png",
|
||||
"path": "uploads/images",
|
||||
"mimeType": "image/webp",
|
||||
"link": "/api/fileStorage/findUnique/vOy5YVUXfHXfiFOHylIN7-desktop.webp",
|
||||
"category": "image"
|
||||
},
|
||||
{
|
||||
"id": "cmk20pf3d0006vnev3mkoqpyy",
|
||||
"name": "gE_qcqIbY0mqI6FV9V4CL-desktop.webp",
|
||||
"realName": "sajjiana-dharma-raksaka.png",
|
||||
"path": "uploads/images",
|
||||
"mimeType": "image/webp",
|
||||
"link": "/api/fileStorage/findUnique/gE_qcqIbY0mqI6FV9V4CL-desktop.webp",
|
||||
"category": "image"
|
||||
},
|
||||
{
|
||||
"id": "cmk2cgqgm0003vn96jun52pik",
|
||||
"name": "q1G995W7cLkC_qquLTlKN-desktop.webp",
|
||||
"realName": "youtube.png",
|
||||
"path": "uploads/images",
|
||||
"mimeType": "image/webp",
|
||||
"link": "/api/fileStorage/findUnique/7hox9spUxj56hY_EBYLnj-desktop.webp",
|
||||
"link": "/api/fileStorage/findUnique/q1G995W7cLkC_qquLTlKN-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",
|
||||
"id": "cmk2cmr000006vn96qepq6gvl",
|
||||
"name": "I6mlQ4nRmPX26gm79C_rM-desktop.webp",
|
||||
"realName": "facebook.png",
|
||||
"path": "uploads/images",
|
||||
"mimeType": "image/webp",
|
||||
"link": "/api/fileStorage/findUnique/z8v9ZREwOJHKGIRYauROt-desktop.webp",
|
||||
"link": "/api/fileStorage/findUnique/I6mlQ4nRmPX26gm79C_rM-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",
|
||||
"id": "cmk2cpeba0009vn966jcrpf3u",
|
||||
"name": "WArLC_yvU33MjoqEnQeQ1-desktop.webp",
|
||||
"realName": "instagram.png",
|
||||
"path": "uploads/images",
|
||||
"mimeType": "image/webp",
|
||||
"link": "/api/fileStorage/findUnique/hkJYAeTNWK_vYaYS20w3I-desktop.webp",
|
||||
"link": "/api/fileStorage/findUnique/WArLC_yvU33MjoqEnQeQ1-desktop.webp",
|
||||
"category": "image"
|
||||
},
|
||||
{
|
||||
"id": "cmff3q12g0005vn6h5ojov2qa",
|
||||
"name": "6XEoZ9SFu59COpil03Gya-desktop.webp",
|
||||
"id": "cmk2crcl1000cvn96j8pmgmo5",
|
||||
"name": "D3RPbNiaNSCjacLjeR_qO-desktop.webp",
|
||||
"realName": "tiktok.png",
|
||||
"path": "uploads/images",
|
||||
"mimeType": "image/webp",
|
||||
"link": "/api/fileStorage/findUnique/6XEoZ9SFu59COpil03Gya-desktop.webp",
|
||||
"link": "/api/fileStorage/findUnique/D3RPbNiaNSCjacLjeR_qO-desktop.webp",
|
||||
"category": "image"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -3,24 +3,24 @@
|
||||
"id": "cmds9023u0008vnbe3oxmhwyf",
|
||||
"name": "Desa Darmasaba",
|
||||
"iconUrl": "https://www.youtube.com/channel/UCtPw9WOQO7d2HIKzKgel4Xg",
|
||||
"imageId": "cmff3joae0000vn6h8sgs0ilg"
|
||||
"imageId": "cmk2cgqgm0003vn96jun52pik"
|
||||
},
|
||||
{
|
||||
"id": "cmds90oul000bvnbe2bqkptoi",
|
||||
"name": "Pemerintah Desa Darmasaba",
|
||||
"iconUrl": "https://www.facebook.com/DarmasabaDesaku",
|
||||
"imageId": "cmff3mtat0002vn6hs8vyyhdd"
|
||||
"imageId": "cmk2cmr000006vn96qepq6gvl"
|
||||
},
|
||||
{
|
||||
"id": "cmds91i4e000evnbe8gtf1gub",
|
||||
"name": "ddarmasaba",
|
||||
"iconUrl": "https://www.instagram.com/ddarmasaba/",
|
||||
"imageId": "cmff3oouh0004vn6hd94brzv9"
|
||||
"imageId": "cmk2cpeba0009vn966jcrpf3u"
|
||||
},
|
||||
{
|
||||
"id": "cmds92de5000hvnbemlu6sq5x",
|
||||
"name": "desa.darmasaba",
|
||||
"iconUrl": "https://www.tiktok.com/@desa.darmasaba?is_from_webapp=1&sender_device=pc",
|
||||
"imageId": "cmff3q12g0005vn6h5ojov2qa"
|
||||
"imageId": "cmk2crcl1000cvn96j8pmgmo5"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
"id": "edit",
|
||||
"name": "I.B Surya Prabhawa Manuaba, S.H., M.H.",
|
||||
"position": "Perbekel Darmasaba periode 2021-2027",
|
||||
"imageId": "cmff2w5ly000avn0telhct71k"
|
||||
"imageId": "cmk2a2dl6001nvngck1n0k8qc"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -4,48 +4,55 @@
|
||||
"name": "Dmangan",
|
||||
"description": "Darmasaba Aman Pangan",
|
||||
"link": "https://darmasaba.desa.id/berita/61452-kader-d-mangan-berhasil-meraih-prestasi-dalam-ajang-lomba-banjar-bali-quis-bbq-tahun-2024",
|
||||
"imageId" : "cmff0z34f0005vn0tjtvq519p"
|
||||
"imageId" : "cmk20nyen0002vnevd0hfr3u8"
|
||||
},
|
||||
{
|
||||
"id": "cmdr76nqk0008vn5rdddvcxnr",
|
||||
"name": "Bicara Darmasaba",
|
||||
"description": "Bicara Darmasaba",
|
||||
"link": "https://darmasaba.desa.id/berita/42506-bicara-darmasaba",
|
||||
"imageId" : "cmff0tnf00003vn0t3kgzi0u0"
|
||||
"imageId" : "cmk20nqmu0001vnevfte29rk0"
|
||||
},
|
||||
{
|
||||
"id": "cmdr77vbw000bvn5rvpmoq31s",
|
||||
"name": "Bares",
|
||||
"description": "Darmasaba Recycling Stock/Exchange",
|
||||
"link": "http://darmasaba.desa.id/berita/56722-bares",
|
||||
"imageId" : "cmff0rr4z0002vn0twp333m2"
|
||||
"imageId" : "cmk20mg320000vnevxy0k73fr"
|
||||
},
|
||||
{
|
||||
"id": "cmdr7bxtp000evn5rmy85wihx",
|
||||
"name": "Sajjana Dharma Raksaka",
|
||||
"description": "Sajjana Dharma Raksaka",
|
||||
"link": "https://ppid.badungkab.go.id/storage/dokumen/5RS9dldGkrgzMQq6bKdZsqsVRHI8gffWv4PGfb3r.pdf",
|
||||
"imageId" : "cmff10cwq0009vn0tse8dzu3j"
|
||||
"imageId" : "cmk20pf3d0006vnev3mkoqpyy"
|
||||
},
|
||||
{
|
||||
"id": "cmdr7dlnk000hvn5r9lur3z35",
|
||||
"name": "PDKT",
|
||||
"description": "Perangkat Desa Kuat Teknologi",
|
||||
"link": "https://darmasaba.desa.id/berita/53752-p-d-k-t",
|
||||
"imageId" : "cmff1013m0008vn0th7t0d64d"
|
||||
"imageId" : "cmk20omzq0005vnevgi6f4edu"
|
||||
},
|
||||
{
|
||||
"id": "cmdr7ftob000mvn5rfhgdtg8v",
|
||||
"name": "GM",
|
||||
"description": "Galah Melah",
|
||||
"link": "https://darmasaba.desa.id/berita/52880-galah-melah",
|
||||
"imageId" : "cmff38cyq000bvn0t9f01cz3f"
|
||||
"imageId" : "cmk20o7mf0003vnevohrksm1d"
|
||||
},
|
||||
{
|
||||
"id": "cmdr7glue000pvn5r6onzslju",
|
||||
"name": "Inovasi Desa Darmasaba",
|
||||
"description": "Inovasi Desa Darmasaba",
|
||||
"link": "https://darmasaba.desa.id/produk-lokal-desa",
|
||||
"imageId" : "cmff0zqvd0007vn0tv6o5hjcq"
|
||||
"imageId" : "cmk20of8m0004vnev9ujy5o0l"
|
||||
},
|
||||
{
|
||||
"id": "cmk228ust0009vnev5p8i377o",
|
||||
"name": "Davest",
|
||||
"description": "<p>DAVEST (Darmasaba Investment) merupakan program inovasi Desa Darmasaba yang bertujuan mempromosikan potensi investasi desa secara terintegrasi melalui media digital dan pendampingan langsung. Program ini menjadi sarana penghubung antara pemerintah desa, pelaku usaha, dan investor dalam rangka mendorong pertumbuhan ekonomi desa yang berkelanjutan.</p><p>DAVEST menyajikan informasi potensi unggulan desa seperti sektor UMKM, pariwisata, ekonomi kreatif, serta peluang investasi berbasis sumber daya lokal dengan prinsip transparansi dan kemudahan akses informasi.</p><p>Di tahun 2024 ini Davest (Darmasaba Village Festival) akan diadakan lagi, dengan berbagai kegiatan pemerdayaan, edukasi dan hiburan yang tentunya lebih waahhhh dari dua tahun lalu. Untuk memantapkan hal tersebut, Pemdes Darmasaba melakukan rapat koordinasi (rakor) Davest 2024 yang dipimpin langsung oleh Perbekel Darmasaba I. B. Surya Prabhawa Manuaba, S.H.,M.H. pada hari Senin (22/1/2024) bertempat di Ruang Shanti Gosana Kantor Perbekel Darmasaba.</p><hr><h3>Tujuan Program</h3><ul><li><p>Meningkatkan daya tarik investasi di Desa Darmasaba</p></li><li><p>Mempromosikan potensi unggulan desa secara profesional</p></li><li><p>Mendorong pertumbuhan ekonomi dan penciptaan lapangan kerja</p></li><li><p>Mendukung visi Desa Darmasaba sebagai desa inovatif dan berdaya saing</p></li></ul><hr><h3>Sasaran Program</h3><ul><li><p>Calon investor lokal dan regional</p></li><li><p>Pelaku UMKM dan kelompok usaha desa</p></li><li><p>Masyarakat Desa Darmasaba</p></li></ul><hr><h3>Bentuk Inovasi</h3><ul><li><p>Inovasi ekonomi desa</p></li><li><p>Inovasi digital</p></li><li><p>Inovasi tata kelola pelayanan investasi</p></li></ul><hr><h3>Ruang Lingkup Kegiatan</h3><ul><li><p>Penyusunan profil potensi investasi desa</p></li><li><p>Digitalisasi informasi investasi desa</p></li><li><p>Promosi peluang investasi melalui media online</p></li><li><p>Fasilitasi komunikasi antara investor dan desa</p></li><li><p>Pendampingan awal investasi berbasis desa</p></li></ul>",
|
||||
"link": "https://darmasaba.desa.id/berita/55862-rakor-davest-2024",
|
||||
"imageId" : "cmk228urs0007vnevi5b66bqn"
|
||||
}
|
||||
]
|
||||
|
||||
11
prisma/data/resolveImageId.ts
Normal file
11
prisma/data/resolveImageId.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import safeImageId from "./safeImageId";
|
||||
|
||||
export default async function resolveImageIdForSeed(
|
||||
existingImageId: string | null | undefined,
|
||||
seedImageId: string | null | undefined
|
||||
) {
|
||||
if (existingImageId) return existingImageId;
|
||||
|
||||
// ✅ Skip validasi saat seed
|
||||
return await safeImageId(seedImageId, true);
|
||||
}
|
||||
24
prisma/data/safeImageId.ts
Normal file
24
prisma/data/safeImageId.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
export default async function safeImageId(
|
||||
imageId?: string | null,
|
||||
skipValidation = false // ✅ tambah param
|
||||
) {
|
||||
if (!imageId) return null;
|
||||
|
||||
if (skipValidation) {
|
||||
console.log(`⚠️ Skipping validation for ${imageId} (seed mode)`);
|
||||
return imageId; // langsung return tanpa cek DB
|
||||
}
|
||||
|
||||
const exists = await prisma.fileStorage.findUnique({
|
||||
where: { id: imageId },
|
||||
});
|
||||
|
||||
if (!exists) {
|
||||
console.warn(`⚠️ imageId ${imageId} not found in FileStorage`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return imageId;
|
||||
}
|
||||
142
prisma/migrations/20260106072549_nico_6_jan2025/migration.sql
Normal file
142
prisma/migrations/20260106072549_nico_6_jan2025/migration.sql
Normal file
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `dokterdanTenagaMedisId` on the `FasilitasKesehatan` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `tarifDanLayananId` on the `FasilitasKesehatan` table. All the data in the column will be lost.
|
||||
- You are about to drop the `User` table. If the table is not empty, all the data it contains will be lost.
|
||||
- You are about to drop the `UserSession` table. If the table is not empty, all the data it contains will be lost.
|
||||
- You are about to drop the `permissions` table. If the table is not empty, all the data it contains will be lost.
|
||||
|
||||
*/
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "FasilitasKesehatan" DROP CONSTRAINT "FasilitasKesehatan_dokterdanTenagaMedisId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "FasilitasKesehatan" DROP CONSTRAINT "FasilitasKesehatan_tarifDanLayananId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "User" DROP CONSTRAINT "User_roleId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "UserSession" DROP CONSTRAINT "UserSession_userId_fkey";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "DokterdanTenagaMedis" ADD COLUMN "jadwalLibur" TEXT,
|
||||
ADD COLUMN "jamBukaLibur" TEXT,
|
||||
ADD COLUMN "jamBukaOperasional" TEXT,
|
||||
ADD COLUMN "jamTutupLibur" TEXT,
|
||||
ADD COLUMN "jamTutupOperasional" TEXT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "FasilitasKesehatan" DROP COLUMN "dokterdanTenagaMedisId",
|
||||
DROP COLUMN "tarifDanLayananId";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "MediaSosial" ADD COLUMN "icon" TEXT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "roles" ALTER COLUMN "permissions" DROP NOT NULL;
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "User";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "UserSession";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "permissions";
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "users" (
|
||||
"id" TEXT NOT NULL,
|
||||
"username" TEXT NOT NULL,
|
||||
"nomor" TEXT NOT NULL,
|
||||
"roleId" TEXT NOT NULL DEFAULT '2',
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT false,
|
||||
"sessionInvalid" BOOLEAN NOT NULL DEFAULT false,
|
||||
"lastLogin" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"permissions" JSONB,
|
||||
|
||||
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "user_sessions" (
|
||||
"id" TEXT NOT NULL,
|
||||
"token" TEXT NOT NULL,
|
||||
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||
"active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"userId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "user_sessions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "UserMenuAccess" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"menuId" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "UserMenuAccess_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "_Tarif" (
|
||||
"A" TEXT NOT NULL,
|
||||
"B" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "_Tarif_AB_pkey" PRIMARY KEY ("A","B")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "_Dokter" (
|
||||
"A" TEXT NOT NULL,
|
||||
"B" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "_Dokter_AB_pkey" PRIMARY KEY ("A","B")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "users_nomor_key" ON "users"("nomor");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "user_sessions_userId_idx" ON "user_sessions"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "user_sessions_token_idx" ON "user_sessions"("token");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "UserMenuAccess_userId_menuId_key" ON "UserMenuAccess"("userId", "menuId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "_Tarif_B_index" ON "_Tarif"("B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "_Dokter_B_index" ON "_Dokter"("B");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "users" ADD CONSTRAINT "users_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "roles"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "user_sessions" ADD CONSTRAINT "user_sessions_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "UserMenuAccess" ADD CONSTRAINT "UserMenuAccess_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_Tarif" ADD CONSTRAINT "_Tarif_A_fkey" FOREIGN KEY ("A") REFERENCES "FasilitasKesehatan"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_Tarif" ADD CONSTRAINT "_Tarif_B_fkey" FOREIGN KEY ("B") REFERENCES "TarifDanLayanan"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_Dokter" ADD CONSTRAINT "_Dokter_A_fkey" FOREIGN KEY ("A") REFERENCES "DokterdanTenagaMedis"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_Dokter" ADD CONSTRAINT "_Dokter_B_fkey" FOREIGN KEY ("B") REFERENCES "FasilitasKesehatan"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -1,30 +1,63 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
// helpers/safeSeedUnique.ts
|
||||
import prisma from "@/lib/prisma";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
type SafeSeedOptions = {
|
||||
skipUpdate?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper generic buat seed dengan upsert aman
|
||||
*/
|
||||
// prisma/safeseedUnique.ts
|
||||
export async function safeSeedUnique<T extends keyof PrismaClient>(
|
||||
model: T,
|
||||
where: Record<string, any>,
|
||||
data: Record<string, any>
|
||||
data: Record<string, any>,
|
||||
options: SafeSeedOptions = {}
|
||||
) {
|
||||
const m = prisma[model];
|
||||
|
||||
if (!m) throw new Error(`Model ${String(model)} tidak ditemukan di PrismaClient`);
|
||||
const m = prisma[model] as any;
|
||||
if (!m) throw new Error(`Model ${String(model)} tidak ditemukan`);
|
||||
|
||||
try {
|
||||
// @ts-expect-error upsert dynamic
|
||||
await m.upsert({
|
||||
// Pastikan `where` berisi field yang benar-benar unique (misal: `id`)
|
||||
const result = await m.upsert({
|
||||
where,
|
||||
update: data,
|
||||
create: { ...where, ...data },
|
||||
update: options.skipUpdate ? {} : data,
|
||||
create: data, // ✅ Jangan duplikasi `where` ke `create`
|
||||
});
|
||||
console.log(`✅ Seeded ${String(model)} -> ${JSON.stringify(where)}`);
|
||||
console.log(`✅ Seed ${String(model)}:`, where);
|
||||
return result;
|
||||
} catch (err) {
|
||||
console.error(`❌ Gagal seed ${String(model)} -> ${JSON.stringify(where)}`, err);
|
||||
console.error(`❌ Gagal seed ${String(model)}:`, where, err);
|
||||
throw err; // ✅ Rethrow agar seeding berhenti jika kritis
|
||||
}
|
||||
}
|
||||
|
||||
// /* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
// import { PrismaClient } from "@prisma/client";
|
||||
|
||||
// const prisma = new PrismaClient();
|
||||
|
||||
// type SafeSeedOptions = {
|
||||
// skipUpdate?: boolean;
|
||||
// };
|
||||
|
||||
// export async function safeSeedUnique<T extends keyof PrismaClient>(
|
||||
// model: T,
|
||||
// where: Record<string, any>,
|
||||
// data: Record<string, any>,
|
||||
// options: SafeSeedOptions = {}
|
||||
// ) {
|
||||
// const m = prisma[model] as any;
|
||||
// if (!m) throw new Error(`Model ${String(model)} tidak ditemukan`);
|
||||
|
||||
// try {
|
||||
// await m.upsert({
|
||||
// where,
|
||||
// update: options.skipUpdate ? {} : data,
|
||||
// create: { ...where, ...data },
|
||||
// });
|
||||
|
||||
// console.log(`✅ Seed ${String(model)}:`, where);
|
||||
// } catch (err) {
|
||||
// console.error(`❌ Gagal seed ${String(model)}:`, where, err);
|
||||
// }
|
||||
// }
|
||||
|
||||
175
prisma/seed.ts
175
prisma/seed.ts
@@ -60,8 +60,37 @@ import jenjangPendidikan from "./data/pendidikan/info-sekolah/jenjang-pendidikan
|
||||
import seedAssets from "./seed_assets";
|
||||
import users from "./data/user/users.json";
|
||||
import { safeSeedUnique } from "./safeseedUnique";
|
||||
import safeImageId from "./data/safeImageId";
|
||||
import resolveImageIdForSeed from "./data/resolveImageId";
|
||||
|
||||
(async () => {
|
||||
// seed assets
|
||||
await prisma.fileStorage.deleteMany();
|
||||
console.log("🗑️ Cleared existing fileStorage records");
|
||||
await seedAssets();
|
||||
|
||||
// // =========== FILE STORAGE ===========
|
||||
console.log("🔄 Seeding file storage...");
|
||||
for (const f of fileStorage) {
|
||||
await safeSeedUnique(
|
||||
"fileStorage",
|
||||
{ name: f.name },
|
||||
{
|
||||
id: f.id,
|
||||
name: f.name,
|
||||
realName: f.realName,
|
||||
path: f.path,
|
||||
mimeType: f.mimeType,
|
||||
link: f.link,
|
||||
category: f.category,
|
||||
deletedAt: null,
|
||||
isActive: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
console.log("✅ File storage seeded");
|
||||
|
||||
console.log("🔄 Seeding roles...");
|
||||
|
||||
for (const r of roles) {
|
||||
@@ -131,112 +160,119 @@ import { safeSeedUnique } from "./safeseedUnique";
|
||||
}
|
||||
}
|
||||
console.log("✅ Users seeding completed");
|
||||
|
||||
// =========== FILE STORAGE ===========
|
||||
console.log("🔄 Seeding file storage...");
|
||||
for (const f of fileStorage) {
|
||||
try {
|
||||
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,
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error(`❌ Failed to seed file storage ${f.name}:`, error.message);
|
||||
}
|
||||
}
|
||||
console.log("✅ File storage seeded");
|
||||
// =========== LANDING PAGE ===========
|
||||
// =========== SUBMENU PROFILE ===========
|
||||
// =========== PROFILE PEJABAT DESA ===========
|
||||
// In your seed.ts file, update the PejabatDesa seeding section to:
|
||||
console.log("🔄 Seeding Pejabat Desa...");
|
||||
for (const p of profilePejabatDesa) {
|
||||
await prisma.pejabatDesa.upsert({
|
||||
where: { id: p.id },
|
||||
update: {
|
||||
name: p.name,
|
||||
position: p.position,
|
||||
imageId: p.imageId,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
position: p.position,
|
||||
imageId: p.imageId,
|
||||
},
|
||||
});
|
||||
try {
|
||||
// First, verify the image exists
|
||||
if (p.imageId) {
|
||||
const imageExists = await prisma.fileStorage.findUnique({
|
||||
where: { id: p.imageId },
|
||||
});
|
||||
|
||||
if (!imageExists) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for PejabatDesa ${p.name}, skipping...`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
await safeSeedUnique(
|
||||
"pejabatDesa",
|
||||
{ id: p.id },
|
||||
{
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
position: p.position,
|
||||
imageId: p.imageId,
|
||||
}
|
||||
);
|
||||
console.log(`✅ Seeded Pejabat Desa -> ${p.name}`);
|
||||
} catch (error: any) {
|
||||
console.error(`❌ Failed to seed Pejabat Desa ${p.name}:`, error.message);
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
"✅ profilePejabatDesa seeded without imageId (editable later via UI)"
|
||||
);
|
||||
console.log("✅ Pejabat Desa seeding completed");
|
||||
|
||||
// =========== PROGRAM INOVASI ===========
|
||||
for (const p of programInovasi) {
|
||||
let imageId: string | null = null;
|
||||
// Add this section after the other seed operations in seed.ts
|
||||
console.log("🔄 Seeding Program Inovasi...");
|
||||
|
||||
if (p.imageId) {
|
||||
const imageExists = await prisma.fileStorage.findUnique({
|
||||
for (const p of programInovasi) {
|
||||
const existing = await prisma.programInovasi.findUnique({
|
||||
where: { id: p.id },
|
||||
select: { imageId: true },
|
||||
});
|
||||
|
||||
let imageId = existing?.imageId; // Pertahankan existing
|
||||
|
||||
// Kalau belum ada imageId, cari berdasarkan name/realName
|
||||
if (!imageId && p.imageId) {
|
||||
// ✅ Cari langsung berdasarkan ID yang ada di p.imageId
|
||||
const fileRecord = await prisma.fileStorage.findUnique({
|
||||
where: { id: p.imageId },
|
||||
select: { id: true, name: true },
|
||||
});
|
||||
|
||||
if (imageExists) {
|
||||
imageId = p.imageId;
|
||||
} else {
|
||||
console.warn(
|
||||
`⚠️ imageId ${p.imageId} tidak ditemukan untuk ProgramInovasi ${p.name}`
|
||||
if (fileRecord) {
|
||||
imageId = fileRecord.id;
|
||||
console.log(
|
||||
`✅ Found file by ID: ${fileRecord.name} (${fileRecord.id})`
|
||||
);
|
||||
} else {
|
||||
console.warn(`⚠️ File with ID ${p.imageId} not found for ${p.name}`);
|
||||
imageId = null;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.programInovasi.upsert({
|
||||
where: { id: p.id },
|
||||
update: {
|
||||
name: p.name,
|
||||
description: p.description,
|
||||
link: p.link,
|
||||
imageId: p.imageId,
|
||||
imageId,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
description: p.description,
|
||||
link: p.link,
|
||||
imageId: p.imageId,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("program inovasi success ...");
|
||||
|
||||
// =========== MEDIA SOSIAL ===========
|
||||
for (const p of mediaSosial) {
|
||||
for (const m of mediaSosial) {
|
||||
const existing = await prisma.mediaSosial.findUnique({
|
||||
where: { id: m.id },
|
||||
select: { imageId: true },
|
||||
});
|
||||
|
||||
const imageId = await resolveImageIdForSeed(existing?.imageId, m.imageId);
|
||||
|
||||
await prisma.mediaSosial.upsert({
|
||||
where: { id: p.id },
|
||||
where: { id: m.id },
|
||||
update: {
|
||||
name: p.name,
|
||||
iconUrl: p.iconUrl,
|
||||
imageId: p.imageId,
|
||||
name: m.name,
|
||||
iconUrl: m.iconUrl,
|
||||
// ⛔ JANGAN overwrite imageId sembarangan
|
||||
imageId,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
iconUrl: p.iconUrl,
|
||||
imageId: p.imageId,
|
||||
id: m.id,
|
||||
name: m.name,
|
||||
iconUrl: m.iconUrl,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("media sosial success ...");
|
||||
|
||||
// =========== SUBMENU DESA ANTI KORUPSI ===========
|
||||
@@ -1245,9 +1281,6 @@ import { safeSeedUnique } from "./safeseedUnique";
|
||||
}
|
||||
|
||||
console.log("✅ Jenjang Pendidikan seeded successfully");
|
||||
|
||||
// seed assets
|
||||
await seedAssets();
|
||||
})()
|
||||
.then(() => prisma.$disconnect())
|
||||
.catch((e) => {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
// prisma/seedAssets.ts
|
||||
import prisma from "@/lib/prisma";
|
||||
import AdmZip from "adm-zip";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import sharp from "sharp";
|
||||
import fetch from "node-fetch";
|
||||
import AdmZip from "adm-zip";
|
||||
import prisma from "@/lib/prisma";
|
||||
import fetchWithRetry from "./data/fetchWithRetry";
|
||||
|
||||
const UPLOADS_DIR =
|
||||
process.env.WIBU_UPLOAD_DIR || path.join(process.cwd(), "uploads");
|
||||
@@ -18,7 +19,10 @@ function detectCategory(filename: string): "image" | "document" | "other" {
|
||||
}
|
||||
|
||||
// --- Helper: recursive walk dir ---
|
||||
async function walkDir(dir: string, fileList: string[] = []): Promise<string[]> {
|
||||
async function walkDir(
|
||||
dir: string,
|
||||
fileList: string[] = []
|
||||
): Promise<string[]> {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
@@ -41,18 +45,45 @@ export default async function seedAssets() {
|
||||
|
||||
// 1. Download zip
|
||||
const url =
|
||||
"https://cld-dkr-makuro-seafile.wibudev.com/f/ffd5a548a04f47939474/?dl=1";
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) throw new Error(`Gagal download assets: ${res.statusText}`);
|
||||
"https://cld-dkr-makuro-seafile.wibudev.com/f/90dd12c9713e42379fcd/?dl=1";
|
||||
const res = await fetchWithRetry(url, 3, 20000);
|
||||
|
||||
// Validasi content-type
|
||||
const contentType = res.headers.get("content-type");
|
||||
if (!contentType?.includes("zip")) {
|
||||
throw new Error(`Invalid content-type (${contentType}). Expected ZIP file`);
|
||||
}
|
||||
|
||||
const buffer = Buffer.from(await res.arrayBuffer());
|
||||
|
||||
// Validasi ukuran file
|
||||
if (buffer.length < 100) {
|
||||
throw new Error("Downloaded ZIP is empty or corrupted");
|
||||
}
|
||||
|
||||
// Validasi signature ZIP ("PK")
|
||||
if (buffer.toString("utf8", 0, 2) !== "PK") {
|
||||
throw new Error("Invalid ZIP signature (PK not found)");
|
||||
}
|
||||
|
||||
// 2. Extract zip ke folder tmp
|
||||
const extractDir = path.join(process.cwd(), "tmp_assets");
|
||||
await fs.rm(extractDir, { recursive: true, force: true });
|
||||
await fs.mkdir(extractDir, { recursive: true });
|
||||
|
||||
const zip = new AdmZip(buffer);
|
||||
zip.extractAllTo(extractDir, true);
|
||||
let zip: AdmZip;
|
||||
|
||||
try {
|
||||
zip = new AdmZip(buffer);
|
||||
} catch (err) {
|
||||
throw new Error("Failed to parse ZIP file (corrupted or invalid)");
|
||||
}
|
||||
|
||||
try {
|
||||
zip.extractAllTo(extractDir, true);
|
||||
} catch (err) {
|
||||
throw new Error("Failed to extract ZIP contents");
|
||||
}
|
||||
|
||||
// 3. Cari semua file valid (recursive)
|
||||
const files = await walkDir(extractDir);
|
||||
@@ -84,18 +115,41 @@ export default async function seedAssets() {
|
||||
await fs.copyFile(filePath, targetPath);
|
||||
}
|
||||
|
||||
// 5. Simpan ke DB
|
||||
await prisma.fileStorage.create({
|
||||
data: {
|
||||
name: finalName,
|
||||
realName: entryName,
|
||||
path: targetPath,
|
||||
mimeType,
|
||||
link: `/uploads/${category}/${finalName}`,
|
||||
category,
|
||||
},
|
||||
const existing = await prisma.fileStorage.findUnique({
|
||||
where: { name: finalName },
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
// Restore kalau soft deleted
|
||||
await prisma.fileStorage.update({
|
||||
where: { name: finalName },
|
||||
data: {
|
||||
path: targetPath,
|
||||
realName: entryName,
|
||||
mimeType,
|
||||
link: `/uploads/${category}/${finalName}`,
|
||||
category,
|
||||
deletedAt: null,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`♻️ restored: ${category}/${finalName}`);
|
||||
} else {
|
||||
await prisma.fileStorage.create({
|
||||
data: {
|
||||
name: finalName,
|
||||
realName: entryName,
|
||||
path: targetPath,
|
||||
mimeType,
|
||||
link: `/uploads/${category}/${finalName}`,
|
||||
category,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`📂 created: ${category}/${finalName}`);
|
||||
}
|
||||
|
||||
console.log(`📂 saved: ${category}/${finalName}`);
|
||||
}
|
||||
|
||||
@@ -103,6 +157,8 @@ export default async function seedAssets() {
|
||||
await fs.rm(extractDir, { recursive: true, force: true });
|
||||
|
||||
console.log("✅ Selesai seed assets!");
|
||||
console.log("DB URL (asset):", process.env.DATABASE_URL);
|
||||
|
||||
}
|
||||
|
||||
// --- Auto run kalau dipanggil langsung ---
|
||||
|
||||
@@ -312,15 +312,15 @@ const kategoriProduk = proxy({
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
loading: false,
|
||||
search2: "",
|
||||
load: async (page = 1, limit = 10, search2 = "") => {
|
||||
search: "",
|
||||
load: async (page = 1, limit = 10, search = "") => {
|
||||
kategoriProduk.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||
kategoriProduk.findMany.page = page;
|
||||
kategoriProduk.findMany.search2 = search2;
|
||||
kategoriProduk.findMany.search = search;
|
||||
|
||||
try {
|
||||
const query: any = { page, limit };
|
||||
if (search2) query.search2 = search2;
|
||||
if (search) query.search = search;
|
||||
|
||||
const res = await ApiFetch.api.ekonomi.kategoriproduk["find-many"].get({ query });
|
||||
|
||||
|
||||
@@ -194,7 +194,7 @@ const posisiOrganisasi = proxy({
|
||||
|
||||
try {
|
||||
this.loading = true;
|
||||
const res = await ApiFetch.api.ekonomi['struktur-organisasi']['posisi-organisasi']['create'].post(this.form);
|
||||
const res = await ApiFetch.api.ekonomi["struktur-organisasi"]["posisi-organisasi"]["create"].post(this.form);
|
||||
if (res.status === 200) {
|
||||
toast.success("Berhasil menambahkan posisi organisasi");
|
||||
posisiOrganisasi.findMany.load();
|
||||
|
||||
@@ -60,13 +60,18 @@ const responden = proxy({
|
||||
totalPages: 1,
|
||||
total: 0,
|
||||
loading: false,
|
||||
load: async (page = 1, limit = 10) => {
|
||||
search: "",
|
||||
load: async (page = 1, limit = 10, search = "") => {
|
||||
// Change to arrow function
|
||||
responden.findMany.loading = true; // Use the full path to access the property
|
||||
responden.findMany.page = page;
|
||||
responden.findMany.search = search;
|
||||
try {
|
||||
const query: any = { page, limit };
|
||||
if (search) query.search = search;
|
||||
|
||||
const res = await ApiFetch.api.landingpage.responden["findMany"].get({
|
||||
query: { page, limit },
|
||||
query,
|
||||
});
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import ApiFetch from "@/lib/api-fetch";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { toast } from "react-toastify";
|
||||
@@ -65,13 +66,46 @@ const dataPendidikan = proxy({
|
||||
select: { id: true; name: true; jumlah: true };
|
||||
}>[]
|
||||
| null,
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
total: 0,
|
||||
loading: false,
|
||||
async load() {
|
||||
const res = await ApiFetch.api.pendidikan.datapendidikan[
|
||||
"findMany"
|
||||
].get();
|
||||
if (res.status === 200) {
|
||||
dataPendidikan.findMany.data = res.data?.data ?? [];
|
||||
search: "",
|
||||
load: async (page = 1, limit = 10, search = "") => {
|
||||
// Change to arrow function
|
||||
dataPendidikan.findMany.loading = true; // Use the full path to access the property
|
||||
dataPendidikan.findMany.page = page;
|
||||
dataPendidikan.findMany.search = search;
|
||||
try {
|
||||
const query: any = { page, limit };
|
||||
if (search) query.search = search;
|
||||
|
||||
const res = await ApiFetch.api.pendidikan.datapendidikan[
|
||||
"findMany"
|
||||
].get({
|
||||
query,
|
||||
});
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
dataPendidikan.findMany.data = res.data.data || [];
|
||||
dataPendidikan.findMany.total = res.data.total || 0;
|
||||
dataPendidikan.findMany.totalPages = res.data.totalPages || 1;
|
||||
} else {
|
||||
console.error(
|
||||
"Failed to load data pendidikan:",
|
||||
res.data?.message
|
||||
);
|
||||
dataPendidikan.findMany.data = [];
|
||||
dataPendidikan.findMany.total = 0;
|
||||
dataPendidikan.findMany.totalPages = 1;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading data pendidikan:", error);
|
||||
dataPendidikan.findMany.data = [];
|
||||
dataPendidikan.findMany.total = 0;
|
||||
dataPendidikan.findMany.totalPages = 1;
|
||||
} finally {
|
||||
dataPendidikan.findMany.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -220,11 +220,34 @@ const roleState = proxy({
|
||||
isActive: true;
|
||||
};
|
||||
}>[],
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
loading: false,
|
||||
async load() {
|
||||
const res = await ApiFetch.api.role["findMany"].get();
|
||||
if (res.status === 200) {
|
||||
roleState.findMany.data = res.data?.data ?? [];
|
||||
search: "",
|
||||
load: async (page = 1, limit = 10, search = "") => {
|
||||
roleState.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||
roleState.findMany.page = page;
|
||||
roleState.findMany.search = search;
|
||||
|
||||
try {
|
||||
const query: any = { page, limit };
|
||||
if (search) query.search = search;
|
||||
|
||||
const res = await ApiFetch.api.role["findMany"].get({ query });
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
roleState.findMany.data = res.data.data ?? [];
|
||||
roleState.findMany.totalPages = res.data.totalPages ?? 1;
|
||||
} else {
|
||||
roleState.findMany.data = [];
|
||||
roleState.findMany.totalPages = 1;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Gagal fetch role paginated:", err);
|
||||
roleState.findMany.data = [];
|
||||
roleState.findMany.totalPages = 1;
|
||||
} finally {
|
||||
roleState.findMany.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -73,17 +73,17 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
|
||||
>
|
||||
{/* ✅ Scroll horizontal wrapper */}
|
||||
<Box visibleFrom='md' pb={10}>
|
||||
<ScrollArea type="auto" offsetScrollbars>
|
||||
<ScrollArea type="auto" offsetScrollbars w="100%">
|
||||
<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
|
||||
width: "max-content", // ⬅️ kunci
|
||||
maxWidth: "100%",
|
||||
}}
|
||||
>
|
||||
{tabs.map((tab, i) => (
|
||||
|
||||
@@ -88,63 +88,65 @@ function ListVideo({ search }: { search: string }) {
|
||||
|
||||
{/* Desktop Table */}
|
||||
<Box visibleFrom="md">
|
||||
<Table highlightOnHover w="100%">
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>Judul Video</TableTh>
|
||||
<TableTh>Tanggal</TableTh>
|
||||
<TableTh>Deskripsi</TableTh>
|
||||
<TableTh>Aksi</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>
|
||||
<Text fz="md" fw={500} lh={1.45} truncate="end" lineClamp={1}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text fz="sm" c="dimmed" lh={1.45}>
|
||||
{new Date(item.createdAt).toLocaleDateString('id-ID', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text fz="sm" lh={1.45} truncate="end" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button
|
||||
variant="light"
|
||||
color="blue"
|
||||
onClick={() => router.push(`/admin/desa/gallery/video/${item.id}`)}
|
||||
fz="sm"
|
||||
px="xs"
|
||||
>
|
||||
<IconDeviceImac size={18} />
|
||||
<Text ml={5}>Detail</Text>
|
||||
</Button>
|
||||
<Box style={{ overflowX: 'auto' }}>
|
||||
<Table highlightOnHover striped verticalSpacing="sm">
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>Judul Video</TableTh>
|
||||
<TableTh>Tanggal</TableTh>
|
||||
<TableTh>Deskripsi</TableTh>
|
||||
<TableTh>Aksi</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd style={{ maxWidth: 250 }}>
|
||||
<Text fz="md" fw={500} lh={1.45} truncate="end" lineClamp={1}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd style={{ maxWidth: 250 }}>
|
||||
<Text fz="sm" c="dimmed" lh={1.45}>
|
||||
{new Date(item.createdAt).toLocaleDateString('id-ID', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd style={{ maxWidth: 250 }}>
|
||||
<Text fz="sm" lh={1.45} truncate="end" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||
</TableTd>
|
||||
<TableTd style={{ maxWidth: 250 }}>
|
||||
<Button
|
||||
variant="light"
|
||||
color="blue"
|
||||
onClick={() => router.push(`/admin/desa/gallery/video/${item.id}`)}
|
||||
fz="sm"
|
||||
px="xs"
|
||||
>
|
||||
<IconDeviceImac size={18} />
|
||||
<Text ml={5}>Detail</Text>
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))
|
||||
) : (
|
||||
<TableTr>
|
||||
<TableTd colSpan={4}>
|
||||
<Center py={24}>
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada video yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))
|
||||
) : (
|
||||
<TableTr>
|
||||
<TableTd colSpan={4}>
|
||||
<Center py={24}>
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada video yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
)}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
)}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Mobile Cards */}
|
||||
|
||||
@@ -5,8 +5,7 @@ import {
|
||||
Button,
|
||||
Center,
|
||||
Divider,
|
||||
Grid,
|
||||
GridCol,
|
||||
Group,
|
||||
Paper,
|
||||
Skeleton,
|
||||
Stack,
|
||||
@@ -43,32 +42,29 @@ function PelayananPendudukNonPermanent() {
|
||||
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
|
||||
<Stack gap="md">
|
||||
{/* Header */}
|
||||
<Grid align="center">
|
||||
<GridCol span={{ base: 12, md: 11 }}>
|
||||
<Title
|
||||
order={3}
|
||||
lh={1.2}
|
||||
c={colors['blue-button']}
|
||||
>
|
||||
Preview Pelayanan Penduduk Non Permanen
|
||||
</Title>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 1 }}>
|
||||
<Button
|
||||
c="green"
|
||||
variant="light"
|
||||
leftSection={<IconEdit size={18} stroke={2} />}
|
||||
radius="md"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/admin/desa/layanan/pelayanan_penduduk_non_permanent/${data.id}`
|
||||
)
|
||||
}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
<Group justify='space-between' align="center">
|
||||
|
||||
<Title
|
||||
order={3}
|
||||
lh={1.2}
|
||||
c={colors['blue-button']}
|
||||
>
|
||||
Preview Pelayanan Penduduk Non Permanen
|
||||
</Title>
|
||||
<Button
|
||||
c="green"
|
||||
variant="light"
|
||||
leftSection={<IconEdit size={18} stroke={2} />}
|
||||
radius="md"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/admin/desa/layanan/pelayanan_penduduk_non_permanent/${data.id}`
|
||||
)
|
||||
}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{/* Content */}
|
||||
<Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs">
|
||||
|
||||
@@ -6,8 +6,6 @@ import {
|
||||
Button,
|
||||
Center,
|
||||
Divider,
|
||||
Grid,
|
||||
GridCol,
|
||||
Group,
|
||||
Paper,
|
||||
Skeleton,
|
||||
@@ -76,28 +74,24 @@ function PerizinanBerusaha() {
|
||||
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
|
||||
<Stack gap="md">
|
||||
{/* Header */}
|
||||
<Grid align="center">
|
||||
<GridCol span={{ base: 12, md: 11 }}>
|
||||
<Title order={3} c={colors['blue-button']} lh={1.2}>
|
||||
Preview Pelayanan Perizinan Berusaha
|
||||
</Title>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 1 }}>
|
||||
<Button
|
||||
c="green"
|
||||
variant="light"
|
||||
leftSection={<IconEdit size={18} stroke={2} />}
|
||||
radius="md"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/admin/desa/layanan/pelayanan_perizinan_berusaha/${data.id}`
|
||||
)
|
||||
}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
<Group justify='space-between' align="center">
|
||||
<Title order={3} c={colors['blue-button']} lh={1.2}>
|
||||
Preview Pelayanan Perizinan Berusaha
|
||||
</Title>
|
||||
<Button
|
||||
c="green"
|
||||
variant="light"
|
||||
leftSection={<IconEdit size={18} stroke={2} />}
|
||||
radius="md"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/admin/desa/layanan/pelayanan_perizinan_berusaha/${data.id}`
|
||||
)
|
||||
}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{/* Content */}
|
||||
<Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs">
|
||||
@@ -136,7 +130,7 @@ function PerizinanBerusaha() {
|
||||
umum:
|
||||
</Text>
|
||||
|
||||
<Box p="xl" w="100%" visibleFrom='md'>
|
||||
<Box p="xl" w="100%" visibleFrom='md'>
|
||||
<Stepper
|
||||
active={active}
|
||||
onStepClick={setActive}
|
||||
@@ -221,37 +215,37 @@ function PerizinanBerusaha() {
|
||||
>
|
||||
<StepperStep label="Langkah Pertama" description="Pendaftaran Akun">
|
||||
<Text fz="sm" lh={1.5}>
|
||||
|
||||
|
||||
</Text>
|
||||
</StepperStep>
|
||||
<StepperStep label="Langkah Kedua" description="Pengisian Data Perusahaan">
|
||||
<Text fz="sm" lh={1.5}>
|
||||
|
||||
|
||||
</Text>
|
||||
</StepperStep>
|
||||
<StepperStep label="Langkah Ketiga" description="Pemilihan KBLI">
|
||||
<Text fz="sm" lh={1.5}>
|
||||
|
||||
|
||||
</Text>
|
||||
</StepperStep>
|
||||
<StepperStep label="Langkah Keempat" description="Pengunggahan Dokumen">
|
||||
<Text fz="sm" lh={1.5}>
|
||||
|
||||
|
||||
</Text>
|
||||
</StepperStep>
|
||||
<StepperStep label="Langkah Kelima" description="Verifikasi dan Persetujuan">
|
||||
<Text fz="sm" lh={1.5}>
|
||||
|
||||
|
||||
</Text>
|
||||
</StepperStep>
|
||||
<StepperStep label="Langkah Keenam" description="Penerimaan NIB">
|
||||
<Text fz="sm" lh={1.5}>
|
||||
|
||||
|
||||
</Text>
|
||||
</StepperStep>
|
||||
<StepperCompleted>
|
||||
<Text fz="sm" lh={1.5}>
|
||||
|
||||
|
||||
</Text>
|
||||
</StepperCompleted>
|
||||
</Stepper>
|
||||
|
||||
@@ -166,7 +166,7 @@ function ListAPBDesa({ search }: { search: string }) {
|
||||
<TableTd>
|
||||
<Button
|
||||
variant="light"
|
||||
color="green"
|
||||
color="blue"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/${item.id}`
|
||||
@@ -243,7 +243,7 @@ function ListAPBDesa({ search }: { search: string }) {
|
||||
</Box>
|
||||
<Button
|
||||
variant="light"
|
||||
color="green"
|
||||
color="blue"
|
||||
fullWidth
|
||||
onClick={() =>
|
||||
router.push(
|
||||
|
||||
@@ -128,10 +128,18 @@ function ListBelanja({ search }: { search: string }) {
|
||||
>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh style={{ width: '35%' }}>Nama</TableTh>
|
||||
<TableTh style={{ width: '25%' }}>Nilai</TableTh>
|
||||
<TableTh style={{ width: '20%' }}>Persentase</TableTh>
|
||||
<TableTh style={{ width: '20%' }}>Aksi</TableTh>
|
||||
<TableTh style={{ width: '40%' }}>
|
||||
<Text fz="sm" fw={600} lh={1.4}>Nama</Text>
|
||||
</TableTh>
|
||||
<TableTh style={{ width: '25%' }}>
|
||||
<Text fz="sm" fw={600} lh={1.4}>Nilai</Text>
|
||||
</TableTh>
|
||||
<TableTh style={{ width: '15%' }}>
|
||||
<Text fz="sm" fw={600} lh={1.4}>Edit</Text>
|
||||
</TableTh>
|
||||
<TableTh style={{ width: '20%' }}>
|
||||
<Text fz="sm" fw={600} lh={1.4}>Delete</Text>
|
||||
</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
|
||||
@@ -120,12 +120,20 @@ function ListPembiayaan({ search }: { search: string }) {
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh style={{ width: '35%' }}>Nama</TableTh>
|
||||
<TableTh style={{ width: '25%' }}>Nilai</TableTh>
|
||||
<TableTh style={{ width: '20%' }}>Persentase</TableTh>
|
||||
<TableTh style={{ width: '20%' }}>Aksi</TableTh>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh style={{ width: '40%' }}>
|
||||
<Text fz="sm" fw={600} lh={1.4}>Nama</Text>
|
||||
</TableTh>
|
||||
<TableTh style={{ width: '25%' }}>
|
||||
<Text fz="sm" fw={600} lh={1.4}>Nilai</Text>
|
||||
</TableTh>
|
||||
<TableTh style={{ width: '15%' }}>
|
||||
<Text fz="sm" fw={600} lh={1.4}>Edit</Text>
|
||||
</TableTh>
|
||||
<TableTh style={{ width: '20%' }}>
|
||||
<Text fz="sm" fw={600} lh={1.4}>Delete</Text>
|
||||
</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
|
||||
@@ -160,7 +160,6 @@ function ListPendapatan({ search }: { search: string }) {
|
||||
px="xs"
|
||||
>
|
||||
<IconEdit size={16} />
|
||||
<Text ml={5}>Edit</Text>
|
||||
</Button>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
@@ -176,7 +175,6 @@ function ListPendapatan({ search }: { search: string }) {
|
||||
px="xs"
|
||||
>
|
||||
<IconTrash size={16} />
|
||||
<Text ml={5}>Hapus</Text>
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
|
||||
@@ -153,9 +153,14 @@ function ListPosisiOrganisasiBumDes({ search }: { search: string }) {
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text fz="sm" fw={500} lh={1.45} c="dimmed" lineClamp={2}>
|
||||
{item.deskripsi || '-'}
|
||||
</Text>
|
||||
<Text
|
||||
fz="sm"
|
||||
fw={500}
|
||||
lh={1.45}
|
||||
c="dimmed"
|
||||
lineClamp={2}
|
||||
dangerouslySetInnerHTML={{ __html: item.deskripsi || '-' }}
|
||||
/>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text fz="md" fw={500} lh={1.45}>{item.hierarki || '-'}</Text>
|
||||
@@ -223,9 +228,14 @@ function ListPosisiOrganisasiBumDes({ search }: { search: string }) {
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Deskripsi
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.4}>
|
||||
{item.deskripsi || '-'}
|
||||
</Text>
|
||||
<Text
|
||||
fz="sm"
|
||||
fw={500}
|
||||
lh={1.45}
|
||||
c="dimmed"
|
||||
lineClamp={2}
|
||||
dangerouslySetInnerHTML={{ __html: item.deskripsi || '-' }}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
Text,
|
||||
Title
|
||||
} from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
@@ -59,6 +59,8 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
|
||||
const [chartData, setChartData] = useState<DemografiPekerjaan[]>([]);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [modalHapus, setModalHapus] = useState(false);
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
|
||||
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
|
||||
const {
|
||||
@@ -79,8 +81,8 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
|
||||
|
||||
useShallowEffect(() => {
|
||||
setMounted(true);
|
||||
load(page, 10, search);
|
||||
}, [page, search]);
|
||||
load(page, 10, debouncedSearch);
|
||||
}, [page, debouncedSearch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
|
||||
@@ -28,27 +28,27 @@ import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
||||
import pasarDesaState from '../../../_state/ekonomi/pasar-desa/pasar-desa';
|
||||
|
||||
function KategoriProduk() {
|
||||
const [search2, setSearch2] = useState('');
|
||||
const [search, setSearch] = useState('');
|
||||
return (
|
||||
<Box>
|
||||
<HeaderSearch
|
||||
title='Kategori Produk'
|
||||
placeholder='Cari nama kategori produk...'
|
||||
searchIcon={<IconSearch size={20} />}
|
||||
value={search2}
|
||||
onChange={(e) => setSearch2(e.currentTarget.value)}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
/>
|
||||
<ListKategoriProduk search2={search2} />
|
||||
<ListKategoriProduk search={search} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function ListKategoriProduk({ search2 }: { search2: string }) {
|
||||
function ListKategoriProduk({ search }: { search: string }) {
|
||||
const statePasar = useProxy(pasarDesaState.kategoriProduk);
|
||||
const [modalHapus, setModalHapus] = useState(false);
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
const router = useRouter();
|
||||
const [debouncedSearch] = useDebouncedValue(search2, 1000);
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||
|
||||
const { data, page, totalPages, loading, load } = statePasar.findMany;
|
||||
|
||||
|
||||
@@ -142,9 +142,7 @@ function ListKolaborasiInovasi({ search }: { search: string }) {
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text fz="sm" fw={500} lh={1.45} truncate="end" lineClamp={1}>
|
||||
{item.slug}
|
||||
</Text>
|
||||
<Text dangerouslySetInnerHTML={{ __html: item.slug }} fz="sm" fw={500} lh={1.45} truncate="end" lineClamp={1} />
|
||||
</TableTd>
|
||||
<TableTd ta="center">
|
||||
<Button
|
||||
@@ -214,9 +212,7 @@ function ListKolaborasiInovasi({ search }: { search: string }) {
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Deskripsi Singkat
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.4}>
|
||||
{item.slug}
|
||||
</Text>
|
||||
<Text dangerouslySetInnerHTML={{ __html: item.slug }} fz="sm" fw={500} lh={1.45} truncate="end" lineClamp={1} />
|
||||
</Box>
|
||||
<Box>
|
||||
<Button
|
||||
|
||||
@@ -141,9 +141,8 @@ function ListJenisLayanan({ search }: { search: string }) {
|
||||
lh={1.5}
|
||||
c={theme.black}
|
||||
lineClamp={2}
|
||||
>
|
||||
{item.deskripsi || '-'}
|
||||
</Text>
|
||||
dangerouslySetInnerHTML={{ __html: item.deskripsi || '-' }}
|
||||
/>
|
||||
</TableTd>
|
||||
<TableTd style={{ width: '15%' }} ta="center">
|
||||
<Button
|
||||
@@ -197,9 +196,13 @@ function ListJenisLayanan({ search }: { search: string }) {
|
||||
<Text fz="sm" fw={600} lh={1.4} c={theme.black}>
|
||||
Deskripsi
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.5} c={theme.black}>
|
||||
{item.deskripsi || '-'}
|
||||
</Text>
|
||||
<Text
|
||||
fz="sm"
|
||||
fw={500}
|
||||
lh={1.5}
|
||||
c={theme.black}
|
||||
dangerouslySetInnerHTML={{ __html: item.deskripsi || '-' }}
|
||||
/>
|
||||
</Box>
|
||||
<Group justify="flex-end" mt="xs">
|
||||
<Button
|
||||
|
||||
@@ -38,7 +38,7 @@ function ListTarifLayanan({ search }: { search: string }) {
|
||||
const router = useRouter();
|
||||
const [modalHapus, setModalHapus] = useState(false);
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
const [debouncedSearch] = useDebouncedValue(search, 10000);
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||
|
||||
const {
|
||||
data,
|
||||
|
||||
@@ -96,15 +96,15 @@ function ListKontakDarurat({ search }: { search: string }) {
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>
|
||||
<TableTd w={335}>
|
||||
<Text fw={500} fz="md" lh={1.45} truncate="end" lineClamp={1}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<TableTd w={335}>
|
||||
<Text fz="sm" lh={1.45} c="dimmed" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<TableTd w={335}>
|
||||
<Button
|
||||
variant="light"
|
||||
color="blue"
|
||||
|
||||
@@ -110,10 +110,10 @@ function ListProgramKesehatan({ search }: { search: string }) {
|
||||
{item.name}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd w={200}>
|
||||
<TableTd>
|
||||
<Text fz="sm" lh={1.5} truncate="end" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsiSingkat }} />
|
||||
</TableTd>
|
||||
<TableTd w={200}>
|
||||
<TableTd>
|
||||
<Text fz="sm" lh={1.5} truncate="end" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
@@ -47,16 +47,14 @@ interface ListRespondenProps {
|
||||
function ListResponden({ search }: ListRespondenProps) {
|
||||
const state = useProxy(indeksKepuasanState.responden);
|
||||
const router = useRouter();
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
|
||||
const { data, page, totalPages, loading, load } = state.findMany;
|
||||
|
||||
useShallowEffect(() => {
|
||||
load(page, 10,);
|
||||
}, [page]);
|
||||
load(page, 10, debouncedSearch);
|
||||
}, [page, debouncedSearch]);
|
||||
|
||||
const filteredData = (data || []).filter((item) => {
|
||||
const keyword = search.toLowerCase();
|
||||
return item.name.toLowerCase().includes(keyword);
|
||||
});
|
||||
const filteredData = data || [];
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
|
||||
@@ -84,7 +84,9 @@ function DetailProgramInovasi() {
|
||||
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">Deskripsi</Text>
|
||||
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.description || '-' }}></Text>
|
||||
<Box pl={5}>
|
||||
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.description || '-' }}></Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
|
||||
@@ -34,8 +34,8 @@ function ListProgramInovasi({ search }: { search: string }) {
|
||||
const { data, page, totalPages, loading, load } = stateProgramInovasi.findMany;
|
||||
|
||||
useShallowEffect(() => {
|
||||
load(page, 10, debouncedSearch);
|
||||
}, [page, debouncedSearch]);
|
||||
load(page, 10, debouncedSearch);
|
||||
}, [page, debouncedSearch]);
|
||||
|
||||
const filteredData = data || [];
|
||||
|
||||
@@ -144,9 +144,7 @@ function ListProgramInovasi({ search }: { search: string }) {
|
||||
{/* Description */}
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>Deskripsi</Text>
|
||||
<Text fz="sm" c="gray.7" lineClamp={2}>
|
||||
{item.description || '-'}
|
||||
</Text>
|
||||
<Text dangerouslySetInnerHTML={{ __html: item.description || '-' }} fz="sm" c="gray.7" lineClamp={2} />
|
||||
</Box>
|
||||
|
||||
{/* Link */}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Group, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
|
||||
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||
import { IconDatabase, IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
@@ -35,6 +35,8 @@ function ListDataPendidikan({ search }: { search: string }) {
|
||||
const [modalHapus, setModalHapus] = useState(false);
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
const router = useRouter();
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
|
||||
const { data, page, totalPages, loading, load } = dataPendidikan.findMany;
|
||||
|
||||
const handleDelete = () => {
|
||||
if (selectedId) {
|
||||
@@ -47,8 +49,8 @@ function ListDataPendidikan({ search }: { search: string }) {
|
||||
|
||||
useShallowEffect(() => {
|
||||
setMounted(true);
|
||||
stateDPM.findMany.load();
|
||||
}, []);
|
||||
load(page, 10, debouncedSearch);
|
||||
}, [page, debouncedSearch]);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
@@ -68,9 +70,9 @@ function ListDataPendidikan({ search }: { search: string }) {
|
||||
return item.name.toLowerCase().includes(keyword) || item.jumlah.toString().includes(keyword);
|
||||
});
|
||||
|
||||
if (!stateDPM.findMany.data) {
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack gap="sm">
|
||||
<Stack py={20}>
|
||||
<Skeleton height={500} radius="md" />
|
||||
</Stack>
|
||||
);
|
||||
@@ -137,6 +139,17 @@ function ListDataPendidikan({ search }: { search: string }) {
|
||||
</TableTbody>
|
||||
</Table>
|
||||
)}
|
||||
<Center mt="md">
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => {
|
||||
load(newPage, 10);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
total={totalPages}
|
||||
color="blue"
|
||||
/>
|
||||
</Center>
|
||||
</Paper>
|
||||
|
||||
<Paper withBorder p="md" bg={colors['white-1']} shadow="sm" radius="md">
|
||||
|
||||
@@ -29,7 +29,7 @@ function Page() {
|
||||
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
|
||||
<Stack gap="md">
|
||||
{/* Header */}
|
||||
<Group align="center">
|
||||
<Group justify='space-between' align="center">
|
||||
<Title order={3} c={colors['blue-button']}>
|
||||
Pratinjau Program Unggulan
|
||||
</Title>
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
'use client'
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
'use client';
|
||||
import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Group, Paper, Select, Stack, TextInput, Title } from '@mantine/core';
|
||||
import { IconArrowBack, IconDeviceFloppy } from '@tabler/icons-react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
Loader,
|
||||
Paper,
|
||||
Select,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { ChangeEvent, useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
@@ -20,9 +32,13 @@ interface FormResponden {
|
||||
function EditResponden() {
|
||||
const router = useRouter();
|
||||
const params = useParams() as { id: string };
|
||||
const state = useProxy(indeksKepuasanState.responden);
|
||||
const id = params.id;
|
||||
|
||||
// ✅ proxy asli untuk mutasi
|
||||
const state = indeksKepuasanState.responden;
|
||||
// ✅ snapshot untuk re-render (read-only)
|
||||
const snapshot = useProxy(indeksKepuasanState.responden);
|
||||
|
||||
const [formData, setFormData] = useState<FormResponden>({
|
||||
name: '',
|
||||
tanggal: '',
|
||||
@@ -31,59 +47,107 @@ function EditResponden() {
|
||||
kelompokUmurId: '',
|
||||
});
|
||||
|
||||
// Helper untuk membuat data Select dari state
|
||||
const mapSelectData = (data: any[] | null | undefined) =>
|
||||
(data || [])
|
||||
.filter(Boolean)
|
||||
.map((v: any) => ({
|
||||
value: v.id || '',
|
||||
label: typeof v.name === 'string' ? v.name : 'Tanpa Nama',
|
||||
}));
|
||||
const [originalData, setOriginalData] = useState<FormResponden>({
|
||||
name: '',
|
||||
tanggal: '',
|
||||
jenisKelaminId: '',
|
||||
ratingId: '',
|
||||
kelompokUmurId: '',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// Load opsi dropdown
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
// 🔹 Load data pilihan select
|
||||
const loadSelectOptions = useCallback(() => {
|
||||
indeksKepuasanState.jenisKelaminResponden.findMany.load();
|
||||
indeksKepuasanState.pilihanRatingResponden.findMany.load();
|
||||
indeksKepuasanState.kelompokUmurResponden.findMany.load();
|
||||
}, []);
|
||||
|
||||
const loadResponden = async () => {
|
||||
if (!id) return;
|
||||
// 🔹 Load data responden by ID
|
||||
const loadResponden = useCallback(async () => {
|
||||
if (!id) return;
|
||||
try {
|
||||
const data = await state.update.load(id);
|
||||
if (!data) return;
|
||||
|
||||
try {
|
||||
const data = await state.update.load(id);
|
||||
if (data) {
|
||||
state.update.id = id;
|
||||
const newForm = {
|
||||
name: data.name || '',
|
||||
tanggal: data.tanggal || '',
|
||||
jenisKelaminId: data.jenisKelaminId || '',
|
||||
ratingId: data.ratingId || '',
|
||||
kelompokUmurId: data.kelompokUmurId || '',
|
||||
};
|
||||
|
||||
// **formData lokal tetap controlled**, tidak overwrite tiap render
|
||||
setFormData({
|
||||
name: data.name || '',
|
||||
tanggal: data.tanggal || '',
|
||||
jenisKelaminId: data.jenisKelaminId || '',
|
||||
ratingId: data.ratingId || '',
|
||||
kelompokUmurId: data.kelompokUmurId || '',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading responden:", error);
|
||||
toast.error("Gagal memuat data responden");
|
||||
}
|
||||
};
|
||||
setFormData(newForm);
|
||||
setOriginalData(newForm);
|
||||
} catch (error) {
|
||||
console.error('Error loading responden:', error);
|
||||
toast.error('Gagal memuat data responden');
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
loadSelectOptions();
|
||||
loadResponden();
|
||||
}, [id, state.update]);
|
||||
|
||||
const handleChange = (field: keyof FormResponden) => (e: ChangeEvent<HTMLInputElement> | string | null) => {
|
||||
const value = typeof e === 'string' || e === null ? e || '' : e.currentTarget.value;
|
||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||
};
|
||||
}, [loadSelectOptions, loadResponden]);
|
||||
|
||||
// 🔹 Submit data
|
||||
const handleSubmit = async () => {
|
||||
state.update.id = id;
|
||||
state.update.form = { ...formData }; // hanya update global state saat submit
|
||||
await state.update.submit();
|
||||
router.push('/admin/ppid/ikm-desa-darmasaba/responden');
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
state.update.id = id;
|
||||
state.update.form = { ...formData }; // mutasi proxy asli ✅
|
||||
await state.update.submit();
|
||||
toast.success('Responden berhasil diperbarui!');
|
||||
router.push('/admin/ppid/indeks-kepuasan-masyarakat/responden');
|
||||
} catch (error) {
|
||||
console.error('Error updating responden:', error);
|
||||
toast.error('Gagal memperbarui responden');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 🔹 Reset form ke data awal
|
||||
const handleResetForm = () => {
|
||||
setFormData({ ...originalData });
|
||||
toast.info('Form dikembalikan ke data awal');
|
||||
};
|
||||
|
||||
// 🔹 Reusable Select component
|
||||
const ControlledSelect = ({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
options,
|
||||
error,
|
||||
placeholder = 'Pilih',
|
||||
loading = false,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
onChange: (val: string) => void;
|
||||
options: { value: string; label: string }[];
|
||||
error?: string;
|
||||
placeholder?: string;
|
||||
loading?: boolean;
|
||||
}) => (
|
||||
<Select
|
||||
label={<Text fw="bold" fz="sm" mb={4}>{label}</Text>}
|
||||
value={value}
|
||||
onChange={(val) => onChange(val || '')}
|
||||
data={options}
|
||||
placeholder={placeholder}
|
||||
disabled={loading}
|
||||
clearable
|
||||
searchable
|
||||
required
|
||||
radius="md"
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
<Group mb="md">
|
||||
@@ -108,72 +172,72 @@ function EditResponden() {
|
||||
label="Nama Responden"
|
||||
placeholder="Masukkan nama responden"
|
||||
value={formData.name}
|
||||
onChange={handleChange('name')}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.currentTarget.value })}
|
||||
radius="md"
|
||||
required
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label="Tanggal"
|
||||
type="date"
|
||||
value={formData.tanggal ? new Date(formData.tanggal).toISOString().split('T')[0] : ''}
|
||||
onChange={handleChange('tanggal')}
|
||||
onChange={(e) => setFormData({ ...formData, tanggal: e.currentTarget.value })}
|
||||
radius="md"
|
||||
required
|
||||
/>
|
||||
|
||||
<Select
|
||||
<ControlledSelect
|
||||
label="Jenis Kelamin"
|
||||
placeholder="Pilih jenis kelamin"
|
||||
value={formData.jenisKelaminId}
|
||||
onChange={handleChange('jenisKelaminId')}
|
||||
data={mapSelectData(indeksKepuasanState.jenisKelaminResponden.findMany.data)}
|
||||
disabled={indeksKepuasanState.jenisKelaminResponden.findMany.loading}
|
||||
clearable
|
||||
searchable
|
||||
required
|
||||
radius="md"
|
||||
error={!formData.jenisKelaminId ? "Pilih jenis kelamin" : undefined}
|
||||
onChange={(val) => setFormData({ ...formData, jenisKelaminId: val })}
|
||||
options={(indeksKepuasanState.jenisKelaminResponden.findMany.data || [])
|
||||
.map((v) => ({ value: v.id || '', label: v.name || 'Tanpa Nama' }))}
|
||||
loading={indeksKepuasanState.jenisKelaminResponden.findMany.loading}
|
||||
error={!formData.jenisKelaminId ? 'Pilih jenis kelamin' : undefined}
|
||||
/>
|
||||
|
||||
<Select
|
||||
<ControlledSelect
|
||||
label="Rating"
|
||||
placeholder="Pilih rating"
|
||||
value={formData.ratingId}
|
||||
onChange={handleChange('ratingId')}
|
||||
data={mapSelectData(indeksKepuasanState.pilihanRatingResponden.findMany.data)}
|
||||
disabled={indeksKepuasanState.pilihanRatingResponden.findMany.loading}
|
||||
clearable
|
||||
searchable
|
||||
required
|
||||
radius="md"
|
||||
error={!formData.ratingId ? "Pilih rating" : undefined}
|
||||
onChange={(val) => setFormData({ ...formData, ratingId: val })}
|
||||
options={(indeksKepuasanState.pilihanRatingResponden.findMany.data || [])
|
||||
.map((v) => ({ value: v.id || '', label: v.name || 'Tanpa Nama' }))}
|
||||
loading={indeksKepuasanState.pilihanRatingResponden.findMany.loading}
|
||||
error={!formData.ratingId ? 'Pilih rating' : undefined}
|
||||
/>
|
||||
|
||||
<Select
|
||||
<ControlledSelect
|
||||
label="Kelompok Umur"
|
||||
placeholder="Pilih kelompok umur"
|
||||
value={formData.kelompokUmurId}
|
||||
onChange={handleChange('kelompokUmurId')}
|
||||
data={mapSelectData(indeksKepuasanState.kelompokUmurResponden.findMany.data)}
|
||||
disabled={indeksKepuasanState.kelompokUmurResponden.findMany.loading}
|
||||
clearable
|
||||
searchable
|
||||
required
|
||||
radius="md"
|
||||
error={!formData.kelompokUmurId ? "Pilih kelompok umur" : undefined}
|
||||
onChange={(val) => setFormData({ ...formData, kelompokUmurId: val })}
|
||||
options={(indeksKepuasanState.kelompokUmurResponden.findMany.data || [])
|
||||
.map((v) => ({ value: v.id || '', label: v.name || 'Tanpa Nama' }))}
|
||||
loading={indeksKepuasanState.kelompokUmurResponden.findMany.loading}
|
||||
error={!formData.kelompokUmurId ? 'Pilih kelompok umur' : undefined}
|
||||
/>
|
||||
|
||||
<Group justify="flex-end" mt="md">
|
||||
<Button variant="light" color="red" onClick={() => router.back()}>
|
||||
<Group justify="right">
|
||||
<Button
|
||||
variant="outline"
|
||||
color="gray"
|
||||
radius="md"
|
||||
size="md"
|
||||
onClick={handleResetForm}
|
||||
>
|
||||
Batal
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
leftSection={<IconDeviceFloppy size={20} />}
|
||||
onClick={handleSubmit}
|
||||
loading={state.update.loading}
|
||||
color={colors['blue-button']}
|
||||
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
|
||||
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
|
||||
@@ -26,7 +26,7 @@ export default function DetailResponden() {
|
||||
stateDetail.delete.byId(selectedId)
|
||||
setModalHapus(false)
|
||||
setSelectedId(null)
|
||||
router.push("/admin/ppid/ikm-desa-darmasaba/responden")
|
||||
router.push("/admin/ppid/indeks-kepuasan-masyarakat/responden")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ export default function DetailResponden() {
|
||||
variant="light"
|
||||
onClick={() => {
|
||||
if (stateDetail.findUnique.data) {
|
||||
router.push(`/admin/ppid/ikm-desa-darmasaba/responden/${stateDetail.findUnique.data.id}/edit`);
|
||||
router.push(`/admin/ppid/indeks-kepuasan-masyarakat/responden/${stateDetail.findUnique.data.id}/edit`);
|
||||
}
|
||||
}}
|
||||
disabled={!stateDetail.findUnique.data}
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
'use client'
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import React from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import grafikBerdasarkanJenisKelamin from '@/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanJenisKelamin';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import { useState } from 'react';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Paper, Stack, Title, TextInput, Select, Text } from '@mantine/core';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Group, Loader, Paper, Select, Stack, TextInput, Title } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
function RespondenCreate() {
|
||||
const router = useRouter();
|
||||
const stategrafikBerdasarkanResponden = useProxy(indeksKepuasanState.responden)
|
||||
const [donutData, setDonutData] = useState<any[]>([]);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const resetForm = () => {
|
||||
stategrafikBerdasarkanResponden.create.form = {
|
||||
@@ -35,6 +34,7 @@ function RespondenCreate() {
|
||||
})
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const id = await stategrafikBerdasarkanResponden.create.create();
|
||||
if (typeof id !== 'undefined') {
|
||||
@@ -45,13 +45,15 @@ function RespondenCreate() {
|
||||
}
|
||||
}
|
||||
resetForm();
|
||||
router.push("/admin/ppid/ikm-desa-darmasaba/responden");
|
||||
router.push("/admin/ppid/indeks-kepuasan-masyarakat/responden");
|
||||
} catch (error) {
|
||||
console.error('Error submitting form:', error);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
<Box>
|
||||
<Box mb={10}>
|
||||
<Button variant="subtle" onClick={() => router.back()}>
|
||||
<IconArrowBack size={20} />
|
||||
@@ -132,13 +134,32 @@ function RespondenCreate() {
|
||||
}
|
||||
disabled={indeksKepuasanState.kelompokUmurResponden.findMany.loading}
|
||||
/>
|
||||
<Button
|
||||
mt={10}
|
||||
bg={colors['blue-button']}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
<Group justify="right">
|
||||
{/* Tombol Batal */}
|
||||
<Button
|
||||
variant="outline"
|
||||
color="gray"
|
||||
radius="md"
|
||||
size="md"
|
||||
onClick={resetForm}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
|
||||
{/* Tombol Simpan */}
|
||||
<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)',
|
||||
}}
|
||||
>
|
||||
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
'use client';
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
@@ -16,10 +20,7 @@ import {
|
||||
Text,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
|
||||
import HeaderSearch from '../../../_com/header';
|
||||
import indeksKepuasanState from '../../../_state/landing-page/indeks-kepuasan';
|
||||
|
||||
@@ -28,7 +29,7 @@ function Responden() {
|
||||
return (
|
||||
<Box>
|
||||
<HeaderSearch
|
||||
title="Responden"
|
||||
title="Data Responden"
|
||||
placeholder="Cari nama responden..."
|
||||
searchIcon={<IconSearch size={18} />}
|
||||
value={search}
|
||||
@@ -46,193 +47,182 @@ interface ListRespondenProps {
|
||||
function ListResponden({ search }: ListRespondenProps) {
|
||||
const state = useProxy(indeksKepuasanState.responden);
|
||||
const router = useRouter();
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
|
||||
const { data, page, totalPages, loading, load } = state.findMany;
|
||||
|
||||
const filteredData = (data || []).filter((item) => {
|
||||
const keyword = search.toLowerCase();
|
||||
return item.name.toLowerCase().includes(keyword);
|
||||
});
|
||||
useShallowEffect(() => {
|
||||
load(page, 10, debouncedSearch);
|
||||
}, [page, debouncedSearch]);
|
||||
|
||||
const filteredData = data || [];
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py="xl">
|
||||
<Skeleton height={750} radius="lg" />
|
||||
<Stack py={{ base: 'md', md: 'lg' }}>
|
||||
<Skeleton height={650} radius="lg" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<Paper withBorder bg="white" p={{ base: 'md', sm: 'lg' }} radius="md" shadow="sm">
|
||||
<Stack gap="md">
|
||||
<Title order={4} lh={1.2}>
|
||||
Data Responden
|
||||
</Title>
|
||||
<Box visibleFrom="md">
|
||||
<Table striped withRowBorders>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh ta="center">No</TableTh>
|
||||
<TableTh>Nama</TableTh>
|
||||
<TableTh>Tanggal</TableTh>
|
||||
<TableTh>Jenis Kelamin</TableTh>
|
||||
<TableTh ta="center">Aksi</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
</Table>
|
||||
</Box>
|
||||
<Text c="dimmed" ta="center" py="md" fz={{ base: 'sm', md: 'md' }} lh={1.4}>
|
||||
Belum ada data responden yang tersedia
|
||||
</Text>
|
||||
</Stack>
|
||||
</Paper>
|
||||
<Box py={{ base: 'md', md: 'lg' }}>
|
||||
<Paper p={{ base: 'md', md: 'lg' }} radius="lg" shadow="md" withBorder>
|
||||
<Stack align="center" gap="sm">
|
||||
<Title order={2} lh={1.2}>
|
||||
Data Responden
|
||||
</Title>
|
||||
<Text c="dimmed" ta="center" fz={{ base: 'sm', md: 'md' }} lh={1.5}>
|
||||
Belum ada data responden yang tersedia
|
||||
</Text>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Paper withBorder bg="white" p={{ base: 'md', sm: 'lg' }} radius="md" shadow="sm">
|
||||
<Stack gap="md">
|
||||
<Title order={4} lh={1.2}>
|
||||
Data Responden
|
||||
</Title>
|
||||
|
||||
<Box>
|
||||
<Stack gap={'lg'}>
|
||||
{/* Desktop Table */}
|
||||
<Box visibleFrom="md">
|
||||
<Table striped withRowBorders>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh w="5%" ta="center">
|
||||
No
|
||||
</TableTh>
|
||||
<TableTh w="25%">Nama</TableTh>
|
||||
<TableTh w="25%">Tanggal</TableTh>
|
||||
<TableTh w="20%">Jenis Kelamin</TableTh>
|
||||
<TableTh w="15%" ta="center">
|
||||
Aksi
|
||||
</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.length === 0 ? (
|
||||
<Paper p="lg" radius="lg" shadow="md" withBorder>
|
||||
<Title order={4} size="lg" mb="md" lh={1.2}>
|
||||
Daftar Responden
|
||||
</Title>
|
||||
<Table
|
||||
striped
|
||||
highlightOnHover
|
||||
withRowBorders
|
||||
verticalSpacing="sm"
|
||||
>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTd colSpan={5}>
|
||||
<Text c="dimmed" ta="center" py="md" fz="sm" lh={1.4}>
|
||||
Tidak ada data yang cocok dengan pencarian
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTh fz="sm" fw={600} w={60}>No</TableTh>
|
||||
<TableTh fz="sm" fw={600}>Nama</TableTh>
|
||||
<TableTh fz="sm" fw={600}>Tanggal</TableTh>
|
||||
<TableTh fz="sm" fw={600}>Jenis Kelamin</TableTh>
|
||||
<TableTh fz="sm" fw={600} w={120}>Aksi</TableTh>
|
||||
</TableTr>
|
||||
) : (
|
||||
filteredData.map((item, index) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd ta="center">{index + 1}</TableTd>
|
||||
<TableTd>{item.name}</TableTd>
|
||||
<TableTd>
|
||||
{item.tanggal ? new Date(item.tanggal).toLocaleDateString('id-ID') : '-'}
|
||||
</TableTd>
|
||||
<TableTd>{item.jenisKelamin.name}</TableTd>
|
||||
<TableTd ta="center">
|
||||
<Button
|
||||
size="xs"
|
||||
radius="md"
|
||||
variant="light"
|
||||
color="blue"
|
||||
leftSection={<IconDeviceImacCog size={16} />}
|
||||
onClick={() =>
|
||||
router.push(`/admin/ppid/ikm-desa-darmasaba/responden/${item.id}`)
|
||||
}
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.length === 0 ? (
|
||||
<TableTr>
|
||||
<TableTd colSpan={5}>
|
||||
<Text ta="center" c="dimmed" fz="sm" lh={1.5}>
|
||||
Tidak ditemukan data dengan kata kunci pencarian
|
||||
</Text>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))
|
||||
)}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
) : (
|
||||
filteredData.map((item, index) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd fz="md" lh={1.5}>{index + 1}</TableTd>
|
||||
<TableTd fz="md" lh={1.5}>{item.name}</TableTd>
|
||||
<TableTd fz="md" lh={1.5}>
|
||||
{item.tanggal
|
||||
? new Date(item.tanggal).toLocaleDateString('id-ID', {
|
||||
day: '2-digit',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
})
|
||||
: '-'}
|
||||
</TableTd>
|
||||
<TableTd fz="md" lh={1.5}>{item.jenisKelamin.name}</TableTd>
|
||||
<TableTd>
|
||||
<Button
|
||||
size="xs"
|
||||
radius="md"
|
||||
variant="light"
|
||||
color="blue"
|
||||
leftSection={<IconDeviceImac size={16} />}
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/admin/ppid/indeks-kepuasan-masyarakat/responden/${item.id}`
|
||||
)
|
||||
}
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))
|
||||
)}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
{/* Mobile Card View */}
|
||||
{/* Mobile Cards */}
|
||||
<Box hiddenFrom="md">
|
||||
{filteredData.length === 0 ? (
|
||||
<Text c="dimmed" ta="center" py="md" fz="sm" lh={1.4}>
|
||||
Tidak ada data yang cocok dengan pencarian
|
||||
</Text>
|
||||
) : (
|
||||
<Stack gap="sm">
|
||||
{filteredData.map((item, index) => (
|
||||
<Paper key={item.id} withBorder p="sm" radius="md">
|
||||
<Stack gap="sm">
|
||||
<Title order={4} size="md" lh={1.2} px="md">
|
||||
Daftar Responden
|
||||
</Title>
|
||||
{filteredData.length === 0 ? (
|
||||
<Paper p="md" radius="lg" shadow="sm" mx="md">
|
||||
<Text ta="center" c="dimmed" fz="sm" lh={1.5}>
|
||||
Tidak ditemukan data dengan kata kunci pencarian
|
||||
</Text>
|
||||
</Paper>
|
||||
) : (
|
||||
filteredData.map((item) => (
|
||||
<Paper key={item.id} p="md" radius="lg" shadow="sm" mx="md">
|
||||
<Stack gap={'xs'}>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
No
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.4}>
|
||||
{index + 1}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Nama
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.4}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Tanggal
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.4}>
|
||||
{item.tanggal
|
||||
? new Date(item.tanggal).toLocaleDateString('id-ID')
|
||||
: '-'}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Jenis Kelamin
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.4}>
|
||||
{item.jenisKelamin.name}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box ta="center">
|
||||
<Button
|
||||
size="xs"
|
||||
radius="md"
|
||||
variant="light"
|
||||
color="blue"
|
||||
leftSection={<IconDeviceImacCog size={16} />}
|
||||
onClick={() =>
|
||||
router.push(`/admin/ppid/ikm-desa-darmasaba/responden/${item.id}`)
|
||||
}
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
</Box>
|
||||
<Text fz="sm" c="dimmed" lh={1.4}>Nama</Text>
|
||||
<Text fz="md" lh={1.5}>{item.name}</Text>
|
||||
|
||||
<Text fz="sm" c="dimmed" lh={1.4}>Tanggal</Text>
|
||||
<Text fz="md" lh={1.5}>
|
||||
{item.tanggal
|
||||
? new Date(item.tanggal).toLocaleDateString('id-ID', {
|
||||
day: '2-digit',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
})
|
||||
: '-'}
|
||||
</Text>
|
||||
|
||||
<Text fz="sm" c="dimmed" lh={1.4}>Jenis Kelamin</Text>
|
||||
<Text fz="md" lh={1.5}>{item.jenisKelamin.name}</Text>
|
||||
|
||||
<Button
|
||||
size="xs"
|
||||
radius="md"
|
||||
variant="light"
|
||||
color="blue"
|
||||
leftSection={<IconDeviceImac size={16} />}
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/admin/ppid/indeks-kepuasan-masyarakat/responden/${item.id}`
|
||||
)
|
||||
}
|
||||
mt="xs"
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
))
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{filteredData.length > 0 && (
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
total={totalPages}
|
||||
onChange={(newPage) => {
|
||||
load(newPage, 10);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
size="md"
|
||||
radius="md"
|
||||
/>
|
||||
</Center>
|
||||
)}
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
total={totalPages}
|
||||
onChange={(newPage) => {
|
||||
load(newPage, 10);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
size="md"
|
||||
radius="md"
|
||||
mt={{ base: 'md', md: 'lg' }}
|
||||
/>
|
||||
</Center>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box, ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
|
||||
import { IconBrush, IconForms, IconUser } from '@tabler/icons-react';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
function LayoutTabs({ children }: { children: React.ReactNode }) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
label: "User",
|
||||
value: "user",
|
||||
href: "/admin/user&role/user",
|
||||
icon: <IconUser size={18} stroke={1.8} />,
|
||||
},
|
||||
{
|
||||
label: "Role",
|
||||
value: "role",
|
||||
href: "/admin/user&role/role",
|
||||
icon: <IconForms size={18} stroke={1.8} />,
|
||||
},
|
||||
{
|
||||
label: "Menu Access",
|
||||
value: "menu-access",
|
||||
href: "/admin/user&role/menu-access",
|
||||
icon: <IconBrush size={18} stroke={1.8} />,
|
||||
}
|
||||
];
|
||||
|
||||
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 tab = tabs.find(t => t.value === value);
|
||||
if (tab) {
|
||||
router.push(tab.href);
|
||||
}
|
||||
setActiveTab(value);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const match = tabs.find(tab => tab.href === pathname);
|
||||
if (match) {
|
||||
setActiveTab(match.value);
|
||||
}
|
||||
}, [pathname]);
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
|
||||
User & Role
|
||||
</Title>
|
||||
<Tabs
|
||||
color={colors['blue-button']}
|
||||
variant="pills"
|
||||
value={activeTab}
|
||||
onChange={handleTabChange}
|
||||
radius="lg"
|
||||
keepMounted={false}
|
||||
>
|
||||
<Box visibleFrom='md' pb={10}>
|
||||
<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) => (
|
||||
<TabsTab
|
||||
key={i}
|
||||
value={tab.value}
|
||||
leftSection={tab.icon}
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "0.9rem",
|
||||
transition: "all 0.2s ease",
|
||||
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTab>
|
||||
))}
|
||||
</TabsList>
|
||||
</ScrollArea>
|
||||
</Box>
|
||||
|
||||
<Box hiddenFrom='md' pb={10}>
|
||||
<ScrollArea
|
||||
type="auto"
|
||||
offsetScrollbars={false}
|
||||
w="100%"
|
||||
>
|
||||
|
||||
<TabsList
|
||||
p="xs" // lebih kecil
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||
borderRadius: "1rem",
|
||||
display: "flex",
|
||||
flexWrap: "nowrap",
|
||||
gap: "0.5rem",
|
||||
width: "max-content", // ⬅️ kunci
|
||||
maxWidth: "100%", // ⬅️ penting
|
||||
}}
|
||||
>
|
||||
{tabs.map((tab, i) => (
|
||||
<TabsTab
|
||||
key={i}
|
||||
value={tab.value}
|
||||
leftSection={tab.icon}
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "0.9rem",
|
||||
paddingInline: "0.75rem", // ⬅️ lebih ramping
|
||||
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTab>
|
||||
))}
|
||||
</TabsList>
|
||||
</ScrollArea>
|
||||
</Box>
|
||||
|
||||
{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>
|
||||
))}
|
||||
</Tabs>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default LayoutTabs;
|
||||
@@ -1,10 +1,11 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
Group,
|
||||
Pagination,
|
||||
Paper,
|
||||
Skeleton,
|
||||
Stack,
|
||||
@@ -17,9 +18,10 @@ import {
|
||||
Text,
|
||||
Title
|
||||
} from '@mantine/core';
|
||||
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import HeaderSearch from '../../_com/header';
|
||||
import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus';
|
||||
@@ -46,10 +48,12 @@ function ListRole({ search }: { search: string }) {
|
||||
const router = useRouter();
|
||||
const [modalHapus, setModalHapus] = useState(false);
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
|
||||
const { data, page, totalPages, loading, load } = listDataState.findMany;
|
||||
|
||||
useEffect(() => {
|
||||
listDataState.findMany.load();
|
||||
}, []);
|
||||
useShallowEffect(() => {
|
||||
load(page, 10, debouncedSearch);
|
||||
}, [page, debouncedSearch]);
|
||||
|
||||
const handleDelete = () => {
|
||||
if (selectedId) {
|
||||
@@ -65,13 +69,14 @@ function ListRole({ search }: { search: string }) {
|
||||
return item.name.toLowerCase().includes(keyword);
|
||||
});
|
||||
|
||||
if (!listDataState.findMany.data) {
|
||||
return (
|
||||
<Stack py={{ base: 'sm', md: 'md' }}>
|
||||
<Skeleton h={500} />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py={20}>
|
||||
<Skeleton height={500} radius="md" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Box py={{ base: 'sm', md: 'md' }}>
|
||||
@@ -199,6 +204,18 @@ function ListRole({ search }: { search: string }) {
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
<Center mt="md">
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => {
|
||||
load(newPage, 10);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
total={totalPages}
|
||||
color="blue"
|
||||
/>
|
||||
</Center>
|
||||
|
||||
{/* Modal Konfirmasi Hapus */}
|
||||
<ModalKonfirmasiHapus
|
||||
opened={modalHapus}
|
||||
|
||||
@@ -14,23 +14,23 @@ export const devBar = [
|
||||
name: "Desa Anti Korupsi",
|
||||
path: "/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi"
|
||||
},
|
||||
// {
|
||||
// id: "Landing_Page_3",
|
||||
// name: "Indeks Kepuasan Masyarakat",
|
||||
// path: "/admin/landing-page/indeks-kepuasan-masyarakat/grafik-kepuasan-masyarakat"
|
||||
// },
|
||||
{
|
||||
id: "Landing_Page_3",
|
||||
name: "Indeks Kepuasan Masyarakat",
|
||||
path: "/admin/landing-page/indeks-kepuasan-masyarakat/grafik-kepuasan-masyarakat"
|
||||
},
|
||||
{
|
||||
id: "Landing_Page_4",
|
||||
name: "SDGs",
|
||||
path: "/admin/landing-page/SDGs"
|
||||
},
|
||||
{
|
||||
id: "Landing_Page_5",
|
||||
id: "Landing_Page_4",
|
||||
name: "APBDes",
|
||||
path: "/admin/landing-page/apbdes"
|
||||
},
|
||||
{
|
||||
id: "Landing_Page_6",
|
||||
id: "Landing_Page_5",
|
||||
name: "Prestasi Desa",
|
||||
path: "/admin/landing-page/prestasi-desa/list-prestasi-desa"
|
||||
}
|
||||
@@ -354,7 +354,7 @@ export const devBar = [
|
||||
{
|
||||
id: "Pendidikan_3",
|
||||
name: "Program Pendidikan Anak",
|
||||
path: "/admin/pendidikan/program-pendidikan-anak/program-unggulan"
|
||||
path: "/admin/pendidikan/program-pendidikan-anak/tujuan-program"
|
||||
},
|
||||
{
|
||||
id: "Pendidikan_4",
|
||||
@@ -418,23 +418,23 @@ export const navBar = [
|
||||
name: "Desa Anti Korupsi",
|
||||
path: "/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi"
|
||||
},
|
||||
// {
|
||||
// id: "Landing_Page_3",
|
||||
// name: "Indeks Kepuasan Masyarakat",
|
||||
// path: "/admin/landing-page/indeks-kepuasan-masyarakat/grafik-kepuasan-masyarakat"
|
||||
// },
|
||||
{
|
||||
id: "Landing_Page_3",
|
||||
name: "Indeks Kepuasan Masyarakat",
|
||||
path: "/admin/landing-page/indeks-kepuasan-masyarakat/grafik-kepuasan-masyarakat"
|
||||
},
|
||||
{
|
||||
id: "Landing_Page_4",
|
||||
name: "SDGs",
|
||||
path: "/admin/landing-page/SDGs"
|
||||
},
|
||||
{
|
||||
id: "Landing_Page_5",
|
||||
id: "Landing_Page_4",
|
||||
name: "APBDes",
|
||||
path: "/admin/landing-page/apbdes"
|
||||
},
|
||||
{
|
||||
id: "Landing_Page_6",
|
||||
id: "Landing_Page_5",
|
||||
name: "Prestasi Desa",
|
||||
path: "/admin/landing-page/prestasi-desa/list-prestasi-desa"
|
||||
}
|
||||
@@ -758,7 +758,7 @@ export const navBar = [
|
||||
{
|
||||
id: "Pendidikan_3",
|
||||
name: "Program Pendidikan Anak",
|
||||
path: "/admin/pendidikan/program-pendidikan-anak/program-unggulan"
|
||||
path: "/admin/pendidikan/program-pendidikan-anak/tujuan-program"
|
||||
},
|
||||
{
|
||||
id: "Pendidikan_4",
|
||||
@@ -822,23 +822,23 @@ export const role1 = [
|
||||
name: "Desa Anti Korupsi",
|
||||
path: "/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi"
|
||||
},
|
||||
// {
|
||||
// id: "Landing_Page_3",
|
||||
// name: "Indeks Kepuasan Masyarakat",
|
||||
// path: "/admin/landing-page/indeks-kepuasan-masyarakat/grafik-kepuasan-masyarakat"
|
||||
// },
|
||||
{
|
||||
id: "Landing_Page_3",
|
||||
name: "Indeks Kepuasan Masyarakat",
|
||||
path: "/admin/landing-page/indeks-kepuasan-masyarakat/grafik-kepuasan-masyarakat"
|
||||
},
|
||||
{
|
||||
id: "Landing_Page_4",
|
||||
name: "SDGs",
|
||||
path: "/admin/landing-page/SDGs"
|
||||
},
|
||||
{
|
||||
id: "Landing_Page_5",
|
||||
id: "Landing_Page_4",
|
||||
name: "APBDes",
|
||||
path: "/admin/landing-page/apbdes"
|
||||
},
|
||||
{
|
||||
id: "Landing_Page_6",
|
||||
id: "Landing_Page_5",
|
||||
name: "Prestasi Desa",
|
||||
path: "/admin/landing-page/prestasi-desa/list-prestasi-desa"
|
||||
}
|
||||
@@ -1170,7 +1170,7 @@ export const role3 = [
|
||||
{
|
||||
id: "Pendidikan_3",
|
||||
name: "Program Pendidikan Anak",
|
||||
path: "/admin/pendidikan/program-pendidikan-anak/program-unggulan"
|
||||
path: "/admin/pendidikan/program-pendidikan-anak/tujuan-program"
|
||||
},
|
||||
{
|
||||
id: "Pendidikan_4",
|
||||
|
||||
@@ -7,20 +7,20 @@ async function kategoriProdukFindMany(context: Context) {
|
||||
// Ambil parameter dari query
|
||||
const page = Number(context.query.page) || 1;
|
||||
const limit = Number(context.query.limit) || 10;
|
||||
const search2 = (context.query.search as string) || '';
|
||||
const search = (context.query.search as string) || '';
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
// Buat where clause
|
||||
const where: any = { isActive: true };
|
||||
|
||||
// Tambahkan pencarian (jika ada)
|
||||
if (search2) {
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ nama: { contains: search2, mode: 'insensitive' } },
|
||||
{ nama: { contains: search, mode: 'insensitive' } },
|
||||
{KategoriToPasar : {
|
||||
some: {
|
||||
kategori: {
|
||||
nama: { contains: search2, mode: 'insensitive' }
|
||||
nama: { contains: search, mode: 'insensitive' }
|
||||
}
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -5,19 +5,19 @@ import { Context } from "elysia";
|
||||
|
||||
async function kategoriProdukFindManyAll(context: Context) {
|
||||
// Ambil query search (opsional)
|
||||
const search2 = (context.query.search as string) || "";
|
||||
const search = (context.query.search as string) || "";
|
||||
|
||||
// Buat where clause
|
||||
const where: any = { isActive: true };
|
||||
|
||||
if (search2) {
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ nama: { contains: search2, mode: "insensitive" } },
|
||||
{ nama: { contains: search, mode: "insensitive" } },
|
||||
{
|
||||
KategoriToPasar: {
|
||||
some: {
|
||||
kategori: {
|
||||
nama: { contains: search2, mode: "insensitive" },
|
||||
nama: { contains: search, mode: "insensitive" },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,48 +1,72 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
// Di findMany.ts
|
||||
export default async function pegawaiFindMany(context: Context) {
|
||||
const page = Number(context.query.page) || 1;
|
||||
const limit = Number(context.query.limit) || 10;
|
||||
const search = (context.query.search as string) || "";
|
||||
const skip = (page - 1) * limit;
|
||||
const isActiveParam = context.query.isActive;
|
||||
|
||||
// where clause dinamis
|
||||
const where: any = {};
|
||||
if (isActiveParam !== undefined) {
|
||||
where.isActive = isActiveParam === "true";
|
||||
}
|
||||
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ namaLengkap: { contains: search, mode: "insensitive" } },
|
||||
{ alamat: { contains: search, mode: "insensitive" } },
|
||||
{ posisi: { nama: { contains: search, mode: "insensitive" } } },
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
const [data, total] = await Promise.all([
|
||||
// Ambil semua data terlebih dahulu (tanpa pagination)
|
||||
const [allData, total] = await Promise.all([
|
||||
prisma.pegawaiBumDes.findMany({
|
||||
where: { isActive: true },
|
||||
where,
|
||||
include: {
|
||||
posisi: true,
|
||||
image: true,
|
||||
},
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
}),
|
||||
prisma.pegawaiBumDes.count({
|
||||
where: { isActive: true }
|
||||
})
|
||||
prisma.pegawaiBumDes.count({ where }),
|
||||
]);
|
||||
|
||||
// Sort manual berdasarkan hierarki posisi
|
||||
const sortedData = allData.sort((a, b) => {
|
||||
// Sort berdasarkan hierarki terlebih dahulu
|
||||
if (a.posisi.hierarki !== b.posisi.hierarki) {
|
||||
return a.posisi.hierarki - b.posisi.hierarki;
|
||||
}
|
||||
// Jika hierarki sama, sort berdasarkan nama posisi
|
||||
return a.posisi.nama.localeCompare(b.posisi.nama);
|
||||
});
|
||||
|
||||
// Lakukan pagination manual setelah sorting
|
||||
const paginatedData = sortedData.slice(skip, skip + limit);
|
||||
const totalPages = Math.ceil(total / limit);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Success fetch pegawai with pagination",
|
||||
data,
|
||||
message: "Success fetch pegawai with hierarchy order",
|
||||
data: paginatedData,
|
||||
page,
|
||||
totalPages,
|
||||
total,
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("Find many paginated error:", e);
|
||||
} catch (error) {
|
||||
console.error("Find many pegawai error:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "Failed fetch pegawai with pagination",
|
||||
message: "Failed fetch pegawai",
|
||||
data: [],
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
total: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
export default async function pegawaiFindManyAll(context: Context) {
|
||||
const search = (context.query.search as string) || "";
|
||||
const isActiveParam = context.query.isActive;
|
||||
|
||||
// Buat where clause dinamis
|
||||
const where: any = {};
|
||||
|
||||
if (isActiveParam !== undefined) {
|
||||
where.isActive = isActiveParam === "true";
|
||||
}
|
||||
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ namaLengkap: { contains: search, mode: "insensitive" } },
|
||||
{ alamat: { contains: search, mode: "insensitive" } },
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await prisma.pegawaiBumDes.findMany({
|
||||
where,
|
||||
include: {
|
||||
posisi: true,
|
||||
image: true,
|
||||
},
|
||||
orderBy: { posisi: { hierarki: "asc" } },
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Success fetch all pegawai (non-paginated)",
|
||||
total: data.length,
|
||||
data,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Find many all error:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "Failed fetch all pegawai",
|
||||
total: 0,
|
||||
data: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,49 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
export default async function dataPendidikanFindMany() {
|
||||
const res = await prisma.dataPendidikan.findMany();
|
||||
return {
|
||||
data: res,
|
||||
};
|
||||
}
|
||||
export default async function dataPendidikanFindMany(context: Context) {
|
||||
const page = Number(context.query.page) || 1;
|
||||
const limit = Number(context.query.limit) || 10;
|
||||
const search = (context.query.search as string) || '';
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const where: any = { isActive: true };
|
||||
|
||||
// Tambahkan pencarian (jika ada)
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ name: { contains: search, mode: 'insensitive' } },
|
||||
{ jumlah: { contains: search, mode: 'insensitive' } },
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
const [data, total] = await Promise.all([
|
||||
prisma.dataPendidikan.findMany({
|
||||
where,
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy: { name: "asc" }, // opsional, kalau mau urut berdasarkan waktu
|
||||
}),
|
||||
prisma.dataPendidikan.count({
|
||||
where: { isActive: true },
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Success fetch program inovasi with pagination",
|
||||
data,
|
||||
page,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
total,
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("Find many paginated error:", e);
|
||||
return {
|
||||
success: false,
|
||||
message: "Failed fetch program inovasi with pagination",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,49 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
export default async function roleFindMany() {
|
||||
const data = await prisma.role.findMany();
|
||||
export default async function roleFindMany(context: Context) {
|
||||
const page = Number(context.query.page) || 1;
|
||||
const limit = Number(context.query.limit) || 10;
|
||||
const search = (context.query.search as string) || '';
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Success get all role",
|
||||
data,
|
||||
};
|
||||
const where: any = { isActive: true };
|
||||
|
||||
// Tambahkan pencarian (jika ada)
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ name: { contains: search, mode: 'insensitive' } },
|
||||
{ description: { contains: search, mode: 'insensitive' } },
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
const [data, total] = await Promise.all([
|
||||
prisma.role.findMany({
|
||||
where,
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy: { name: "asc" }, // opsional, kalau mau urut berdasarkan waktu
|
||||
}),
|
||||
prisma.role.count({
|
||||
where: { isActive: true },
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Success fetch role with pagination",
|
||||
data,
|
||||
page,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
total,
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("Find many paginated error:", e);
|
||||
return {
|
||||
success: false,
|
||||
message: "Failed fetch role with pagination",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user