Compare commits

..

5 Commits

Author SHA1 Message Date
503da91ce6 Tambah seeder di bagian landing page 2026-01-06 17:54:21 +08:00
daaed8089b Fix All Search Admin 2026-01-05 17:11:30 +08:00
f436aa2ef0 Fix QC Kak Inno Mobile Done
FIx QC Kak Ayu Mobile Admin Done
Fix Tampilan Admin Mobile Device All Menu Done
2026-01-02 16:33:15 +08:00
50bc54ceca Fix QC Kak Inno 22 Des
Fix QC Kak Ayu 22 Des
Fix Tampilan Admin Menu Inovasi
2025-12-24 14:36:51 +08:00
f0f201c853 Fix QC Kak Inno 22 Des
Fix QC Kak Ayu 22 Des
Fix Tampilan Admin Mobile Device Menu Ekonomi
Fix Search -> useDebounced Menu Ekonomi
2025-12-23 17:18:36 +08:00
264 changed files with 9560 additions and 3745 deletions

View 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");
}

View File

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

View File

@@ -3,24 +3,24 @@
"id": "cmds9023u0008vnbe3oxmhwyf", "id": "cmds9023u0008vnbe3oxmhwyf",
"name": "Desa Darmasaba", "name": "Desa Darmasaba",
"iconUrl": "https://www.youtube.com/channel/UCtPw9WOQO7d2HIKzKgel4Xg", "iconUrl": "https://www.youtube.com/channel/UCtPw9WOQO7d2HIKzKgel4Xg",
"imageId": "cmff3joae0000vn6h8sgs0ilg" "imageId": "cmk2cgqgm0003vn96jun52pik"
}, },
{ {
"id": "cmds90oul000bvnbe2bqkptoi", "id": "cmds90oul000bvnbe2bqkptoi",
"name": "Pemerintah Desa Darmasaba", "name": "Pemerintah Desa Darmasaba",
"iconUrl": "https://www.facebook.com/DarmasabaDesaku", "iconUrl": "https://www.facebook.com/DarmasabaDesaku",
"imageId": "cmff3mtat0002vn6hs8vyyhdd" "imageId": "cmk2cmr000006vn96qepq6gvl"
}, },
{ {
"id": "cmds91i4e000evnbe8gtf1gub", "id": "cmds91i4e000evnbe8gtf1gub",
"name": "ddarmasaba", "name": "ddarmasaba",
"iconUrl": "https://www.instagram.com/ddarmasaba/", "iconUrl": "https://www.instagram.com/ddarmasaba/",
"imageId": "cmff3oouh0004vn6hd94brzv9" "imageId": "cmk2cpeba0009vn966jcrpf3u"
}, },
{ {
"id": "cmds92de5000hvnbemlu6sq5x", "id": "cmds92de5000hvnbemlu6sq5x",
"name": "desa.darmasaba", "name": "desa.darmasaba",
"iconUrl": "https://www.tiktok.com/@desa.darmasaba?is_from_webapp=1&sender_device=pc", "iconUrl": "https://www.tiktok.com/@desa.darmasaba?is_from_webapp=1&sender_device=pc",
"imageId": "cmff3q12g0005vn6h5ojov2qa" "imageId": "cmk2crcl1000cvn96j8pmgmo5"
} }
] ]

View File

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

View File

@@ -4,48 +4,55 @@
"name": "Dmangan", "name": "Dmangan",
"description": "Darmasaba Aman Pangan", "description": "Darmasaba Aman Pangan",
"link": "https://darmasaba.desa.id/berita/61452-kader-d-mangan-berhasil-meraih-prestasi-dalam-ajang-lomba-banjar-bali-quis-bbq-tahun-2024", "link": "https://darmasaba.desa.id/berita/61452-kader-d-mangan-berhasil-meraih-prestasi-dalam-ajang-lomba-banjar-bali-quis-bbq-tahun-2024",
"imageId" : "cmff0z34f0005vn0tjtvq519p" "imageId" : "cmk20nyen0002vnevd0hfr3u8"
}, },
{ {
"id": "cmdr76nqk0008vn5rdddvcxnr", "id": "cmdr76nqk0008vn5rdddvcxnr",
"name": "Bicara Darmasaba", "name": "Bicara Darmasaba",
"description": "Bicara Darmasaba", "description": "Bicara Darmasaba",
"link": "https://darmasaba.desa.id/berita/42506-bicara-darmasaba", "link": "https://darmasaba.desa.id/berita/42506-bicara-darmasaba",
"imageId" : "cmff0tnf00003vn0t3kgzi0u0" "imageId" : "cmk20nqmu0001vnevfte29rk0"
}, },
{ {
"id": "cmdr77vbw000bvn5rvpmoq31s", "id": "cmdr77vbw000bvn5rvpmoq31s",
"name": "Bares", "name": "Bares",
"description": "Darmasaba Recycling Stock/Exchange", "description": "Darmasaba Recycling Stock/Exchange",
"link": "http://darmasaba.desa.id/berita/56722-bares", "link": "http://darmasaba.desa.id/berita/56722-bares",
"imageId" : "cmff0rr4z0002vn0twp333m2" "imageId" : "cmk20mg320000vnevxy0k73fr"
}, },
{ {
"id": "cmdr7bxtp000evn5rmy85wihx", "id": "cmdr7bxtp000evn5rmy85wihx",
"name": "Sajjana Dharma Raksaka", "name": "Sajjana Dharma Raksaka",
"description": "Sajjana Dharma Raksaka", "description": "Sajjana Dharma Raksaka",
"link": "https://ppid.badungkab.go.id/storage/dokumen/5RS9dldGkrgzMQq6bKdZsqsVRHI8gffWv4PGfb3r.pdf", "link": "https://ppid.badungkab.go.id/storage/dokumen/5RS9dldGkrgzMQq6bKdZsqsVRHI8gffWv4PGfb3r.pdf",
"imageId" : "cmff10cwq0009vn0tse8dzu3j" "imageId" : "cmk20pf3d0006vnev3mkoqpyy"
}, },
{ {
"id": "cmdr7dlnk000hvn5r9lur3z35", "id": "cmdr7dlnk000hvn5r9lur3z35",
"name": "PDKT", "name": "PDKT",
"description": "Perangkat Desa Kuat Teknologi", "description": "Perangkat Desa Kuat Teknologi",
"link": "https://darmasaba.desa.id/berita/53752-p-d-k-t", "link": "https://darmasaba.desa.id/berita/53752-p-d-k-t",
"imageId" : "cmff1013m0008vn0th7t0d64d" "imageId" : "cmk20omzq0005vnevgi6f4edu"
}, },
{ {
"id": "cmdr7ftob000mvn5rfhgdtg8v", "id": "cmdr7ftob000mvn5rfhgdtg8v",
"name": "GM", "name": "GM",
"description": "Galah Melah", "description": "Galah Melah",
"link": "https://darmasaba.desa.id/berita/52880-galah-melah", "link": "https://darmasaba.desa.id/berita/52880-galah-melah",
"imageId" : "cmff38cyq000bvn0t9f01cz3f" "imageId" : "cmk20o7mf0003vnevohrksm1d"
}, },
{ {
"id": "cmdr7glue000pvn5r6onzslju", "id": "cmdr7glue000pvn5r6onzslju",
"name": "Inovasi Desa Darmasaba", "name": "Inovasi Desa Darmasaba",
"description": "Inovasi Desa Darmasaba", "description": "Inovasi Desa Darmasaba",
"link": "https://darmasaba.desa.id/produk-lokal-desa", "link": "https://darmasaba.desa.id/produk-lokal-desa",
"imageId" : "cmff0zqvd0007vn0tv6o5hjcq" "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"
} }
] ]

View 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);
}

View 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;
}

View 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;

View File

@@ -1,30 +1,63 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
// helpers/safeSeedUnique.ts import prisma from "@/lib/prisma";
import { PrismaClient } from "@prisma/client"; import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient(); type SafeSeedOptions = {
skipUpdate?: boolean;
};
/** // prisma/safeseedUnique.ts
* Helper generic buat seed dengan upsert aman
*/
export async function safeSeedUnique<T extends keyof PrismaClient>( export async function safeSeedUnique<T extends keyof PrismaClient>(
model: T, model: T,
where: Record<string, any>, where: Record<string, any>,
data: Record<string, any> data: Record<string, any>,
options: SafeSeedOptions = {}
) { ) {
const m = prisma[model]; const m = prisma[model] as any;
if (!m) throw new Error(`Model ${String(model)} tidak ditemukan`);
if (!m) throw new Error(`Model ${String(model)} tidak ditemukan di PrismaClient`);
try { try {
// @ts-expect-error upsert dynamic // Pastikan `where` berisi field yang benar-benar unique (misal: `id`)
await m.upsert({ const result = await m.upsert({
where, where,
update: data, update: options.skipUpdate ? {} : data,
create: { ...where, ...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) { } 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);
// }
// }

View File

@@ -60,8 +60,37 @@ import jenjangPendidikan from "./data/pendidikan/info-sekolah/jenjang-pendidikan
import seedAssets from "./seed_assets"; import seedAssets from "./seed_assets";
import users from "./data/user/users.json"; import users from "./data/user/users.json";
import { safeSeedUnique } from "./safeseedUnique"; import { safeSeedUnique } from "./safeseedUnique";
import safeImageId from "./data/safeImageId";
import resolveImageIdForSeed from "./data/resolveImageId";
(async () => { (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..."); console.log("🔄 Seeding roles...");
for (const r of roles) { for (const r of roles) {
@@ -131,112 +160,119 @@ import { safeSeedUnique } from "./safeseedUnique";
} }
} }
console.log("✅ Users seeding completed"); 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 =========== // =========== LANDING PAGE ===========
// =========== SUBMENU PROFILE =========== // =========== SUBMENU PROFILE ===========
// =========== PROFILE PEJABAT DESA =========== // =========== PROFILE PEJABAT DESA ===========
// In your seed.ts file, update the PejabatDesa seeding section to:
console.log("🔄 Seeding Pejabat Desa...");
for (const p of profilePejabatDesa) { for (const p of profilePejabatDesa) {
await prisma.pejabatDesa.upsert({ try {
where: { id: p.id }, // First, verify the image exists
update: { if (p.imageId) {
name: p.name, const imageExists = await prisma.fileStorage.findUnique({
position: p.position, where: { id: p.imageId },
imageId: p.imageId, });
},
create: { if (!imageExists) {
id: p.id, console.warn(
name: p.name, `⚠️ Image not found for PejabatDesa ${p.name}, skipping...`
position: p.position, );
imageId: p.imageId, 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( console.log("✅ Pejabat Desa seeding completed");
"✅ profilePejabatDesa seeded without imageId (editable later via UI)"
);
// =========== PROGRAM INOVASI =========== // =========== PROGRAM INOVASI ===========
for (const p of programInovasi) { // Add this section after the other seed operations in seed.ts
let imageId: string | null = null; console.log("🔄 Seeding Program Inovasi...");
if (p.imageId) { for (const p of programInovasi) {
const imageExists = await prisma.fileStorage.findUnique({ 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 }, where: { id: p.imageId },
select: { id: true, name: true },
}); });
if (imageExists) { if (fileRecord) {
imageId = p.imageId; imageId = fileRecord.id;
} else { console.log(
console.warn( `✅ Found file by ID: ${fileRecord.name} (${fileRecord.id})`
`⚠️ imageId ${p.imageId} tidak ditemukan untuk ProgramInovasi ${p.name}`
); );
} else {
console.warn(`⚠️ File with ID ${p.imageId} not found for ${p.name}`);
imageId = null;
} }
} }
await prisma.programInovasi.upsert({ await prisma.programInovasi.upsert({
where: { id: p.id }, where: { id: p.id },
update: { update: {
name: p.name, name: p.name,
description: p.description, description: p.description,
link: p.link, link: p.link,
imageId: p.imageId, imageId,
}, },
create: { create: {
id: p.id, id: p.id,
name: p.name, name: p.name,
description: p.description, description: p.description,
link: p.link, link: p.link,
imageId: p.imageId, imageId,
}, },
}); });
} }
console.log("program inovasi success ...");
// =========== MEDIA SOSIAL =========== // =========== 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({ await prisma.mediaSosial.upsert({
where: { id: p.id }, where: { id: m.id },
update: { update: {
name: p.name, name: m.name,
iconUrl: p.iconUrl, iconUrl: m.iconUrl,
imageId: p.imageId, // ⛔ JANGAN overwrite imageId sembarangan
imageId,
}, },
create: { create: {
id: p.id, id: m.id,
name: p.name, name: m.name,
iconUrl: p.iconUrl, iconUrl: m.iconUrl,
imageId: p.imageId, imageId,
}, },
}); });
} }
console.log("media sosial success ..."); console.log("media sosial success ...");
// =========== SUBMENU DESA ANTI KORUPSI =========== // =========== SUBMENU DESA ANTI KORUPSI ===========
@@ -1245,9 +1281,6 @@ import { safeSeedUnique } from "./safeseedUnique";
} }
console.log("✅ Jenjang Pendidikan seeded successfully"); console.log("✅ Jenjang Pendidikan seeded successfully");
// seed assets
await seedAssets();
})() })()
.then(() => prisma.$disconnect()) .then(() => prisma.$disconnect())
.catch((e) => { .catch((e) => {

View File

@@ -1,10 +1,11 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
// prisma/seedAssets.ts // prisma/seedAssets.ts
import prisma from "@/lib/prisma";
import AdmZip from "adm-zip";
import fs from "fs/promises"; import fs from "fs/promises";
import path from "path"; import path from "path";
import sharp from "sharp"; import sharp from "sharp";
import fetch from "node-fetch"; import fetchWithRetry from "./data/fetchWithRetry";
import AdmZip from "adm-zip";
import prisma from "@/lib/prisma";
const UPLOADS_DIR = const UPLOADS_DIR =
process.env.WIBU_UPLOAD_DIR || path.join(process.cwd(), "uploads"); 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 --- // --- 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 }); const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) { for (const entry of entries) {
@@ -41,18 +45,45 @@ export default async function seedAssets() {
// 1. Download zip // 1. Download zip
const url = const url =
"https://cld-dkr-makuro-seafile.wibudev.com/f/ffd5a548a04f47939474/?dl=1"; "https://cld-dkr-makuro-seafile.wibudev.com/f/90dd12c9713e42379fcd/?dl=1";
const res = await fetch(url); const res = await fetchWithRetry(url, 3, 20000);
if (!res.ok) throw new Error(`Gagal download assets: ${res.statusText}`);
// 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()); 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 // 2. Extract zip ke folder tmp
const extractDir = path.join(process.cwd(), "tmp_assets"); const extractDir = path.join(process.cwd(), "tmp_assets");
await fs.rm(extractDir, { recursive: true, force: true }); await fs.rm(extractDir, { recursive: true, force: true });
await fs.mkdir(extractDir, { recursive: true }); await fs.mkdir(extractDir, { recursive: true });
const zip = new AdmZip(buffer); let zip: AdmZip;
zip.extractAllTo(extractDir, true);
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) // 3. Cari semua file valid (recursive)
const files = await walkDir(extractDir); const files = await walkDir(extractDir);
@@ -84,18 +115,41 @@ export default async function seedAssets() {
await fs.copyFile(filePath, targetPath); await fs.copyFile(filePath, targetPath);
} }
// 5. Simpan ke DB const existing = await prisma.fileStorage.findUnique({
await prisma.fileStorage.create({ where: { name: finalName },
data: {
name: finalName,
realName: entryName,
path: targetPath,
mimeType,
link: `/uploads/${category}/${finalName}`,
category,
},
}); });
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}`); console.log(`📂 saved: ${category}/${finalName}`);
} }
@@ -103,6 +157,8 @@ export default async function seedAssets() {
await fs.rm(extractDir, { recursive: true, force: true }); await fs.rm(extractDir, { recursive: true, force: true });
console.log("✅ Selesai seed assets!"); console.log("✅ Selesai seed assets!");
console.log("DB URL (asset):", process.env.DATABASE_URL);
} }
// --- Auto run kalau dipanggil langsung --- // --- Auto run kalau dipanggil langsung ---

View File

@@ -0,0 +1,36 @@
// components/modal/ModalKonfirmasiHapus.tsx
import colors from "@/con/colors"
import { Modal, Text, Button, Flex } from "@mantine/core"
interface ModalKonfirmasiNonAktifProps {
opened: boolean
loading?: boolean
onClose: () => void
onConfirm: () => void
text: string
}
export function ModalKonfirmasiNonAktif({
opened,
loading = false,
onClose,
onConfirm,
text,
}: ModalKonfirmasiNonAktifProps) {
return (
<Modal
opened={opened}
onClose={onClose}
title={<Text fw={"bold"} fz={"xl"}>Konfirmasi Non Aktif</Text>}
centered
>
<Text mb="md">{text}</Text>
<Flex justify="flex-end" gap="sm">
<Button style={{color: "white"}} bg={colors['blue-button']} variant="default" onClick={onClose}>Batal</Button>
<Button color="red" onClick={onConfirm} loading={loading}>
Yakin Non Aktif
</Button>
</Flex>
</Modal>
)
}

View File

@@ -68,7 +68,7 @@ const category = proxy({
const res = await ApiFetch.api.desa.kategoripengumuman[ const res = await ApiFetch.api.desa.kategoripengumuman[
"findMany" "findMany"
].get({ ].get({
query: { page, limit }, query: { page, limit, search },
}); });
if (res.status === 200 && res.data?.success) { if (res.status === 200 && res.data?.success) {

View File

@@ -65,7 +65,7 @@ const potensiDesa = proxy({
const res = await ApiFetch.api.desa.potensi[ const res = await ApiFetch.api.desa.potensi[
"find-many" "find-many"
].get({ ].get({
query: { page, limit }, query: { page, limit, search },
}); });
if (res.status === 200 && res.data?.success) { if (res.status === 200 && res.data?.success) {

View File

@@ -312,15 +312,15 @@ const kategoriProduk = proxy({
page: 1, page: 1,
totalPages: 1, totalPages: 1,
loading: false, loading: false,
search2: "", search: "",
load: async (page = 1, limit = 10, search2 = "") => { load: async (page = 1, limit = 10, search = "") => {
kategoriProduk.findMany.loading = true; // ✅ Akses langsung via nama path kategoriProduk.findMany.loading = true; // ✅ Akses langsung via nama path
kategoriProduk.findMany.page = page; kategoriProduk.findMany.page = page;
kategoriProduk.findMany.search2 = search2; kategoriProduk.findMany.search = search;
try { try {
const query: any = { page, limit }; 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 }); const res = await ApiFetch.api.ekonomi.kategoriproduk["find-many"].get({ query });

View File

@@ -194,7 +194,7 @@ const posisiOrganisasi = proxy({
try { try {
this.loading = true; 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) { if (res.status === 200) {
toast.success("Berhasil menambahkan posisi organisasi"); toast.success("Berhasil menambahkan posisi organisasi");
posisiOrganisasi.findMany.load(); posisiOrganisasi.findMany.load();

View File

@@ -60,13 +60,18 @@ const responden = proxy({
totalPages: 1, totalPages: 1,
total: 0, total: 0,
loading: false, loading: false,
load: async (page = 1, limit = 10) => { search: "",
load: async (page = 1, limit = 10, search = "") => {
// Change to arrow function // Change to arrow function
responden.findMany.loading = true; // Use the full path to access the property responden.findMany.loading = true; // Use the full path to access the property
responden.findMany.page = page; responden.findMany.page = page;
responden.findMany.search = search;
try { try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.landingpage.responden["findMany"].get({ const res = await ApiFetch.api.landingpage.responden["findMany"].get({
query: { page, limit }, query,
}); });
if (res.status === 200 && res.data?.success) { if (res.status === 200 && res.data?.success) {

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch"; import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
@@ -65,13 +66,46 @@ const dataPendidikan = proxy({
select: { id: true; name: true; jumlah: true }; select: { id: true; name: true; jumlah: true };
}>[] }>[]
| null, | null,
page: 1,
totalPages: 1,
total: 0,
loading: false, loading: false,
async load() { search: "",
const res = await ApiFetch.api.pendidikan.datapendidikan[ load: async (page = 1, limit = 10, search = "") => {
"findMany" // Change to arrow function
].get(); dataPendidikan.findMany.loading = true; // Use the full path to access the property
if (res.status === 200) { dataPendidikan.findMany.page = page;
dataPendidikan.findMany.data = res.data?.data ?? []; 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;
} }
}, },
}, },

View File

@@ -220,11 +220,34 @@ const roleState = proxy({
isActive: true; isActive: true;
}; };
}>[], }>[],
page: 1,
totalPages: 1,
loading: false, loading: false,
async load() { search: "",
const res = await ApiFetch.api.role["findMany"].get(); load: async (page = 1, limit = 10, search = "") => {
if (res.status === 200) { roleState.findMany.loading = true; // ✅ Akses langsung via nama path
roleState.findMany.data = res.data?.data ?? []; 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;
} }
}, },
}, },

View File

@@ -73,17 +73,17 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
> >
{/* ✅ Scroll horizontal wrapper */} {/* ✅ Scroll horizontal wrapper */}
<Box visibleFrom='md' pb={10}> <Box visibleFrom='md' pb={10}>
<ScrollArea type="auto" offsetScrollbars> <ScrollArea type="auto" offsetScrollbars w="100%">
<TabsList <TabsList
p="sm" p="sm"
style={{ style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)", background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem", borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex", display: "flex",
flexWrap: "nowrap", flexWrap: "nowrap",
gap: "0.5rem", gap: "0.5rem",
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi width: "max-content", // ⬅️ kunci
maxWidth: "100%",
}} }}
> >
{tabs.map((tab, i) => ( {tabs.map((tab, i) => (

View File

@@ -88,63 +88,65 @@ function ListVideo({ search }: { search: string }) {
{/* Desktop Table */} {/* Desktop Table */}
<Box visibleFrom="md"> <Box visibleFrom="md">
<Table highlightOnHover w="100%"> <Box style={{ overflowX: 'auto' }}>
<TableThead> <Table highlightOnHover striped verticalSpacing="sm">
<TableTr> <TableThead>
<TableTh>Judul Video</TableTh> <TableTr>
<TableTh>Tanggal</TableTh> <TableTh>Judul Video</TableTh>
<TableTh>Deskripsi</TableTh> <TableTh>Tanggal</TableTh>
<TableTh>Aksi</TableTh> <TableTh>Deskripsi</TableTh>
</TableTr> <TableTh>Aksi</TableTh>
</TableThead> </TableTr>
<TableTbody> </TableThead>
{filteredData.length > 0 ? ( <TableTbody>
filteredData.map((item) => ( {filteredData.length > 0 ? (
<TableTr key={item.id}> filteredData.map((item) => (
<TableTd> <TableTr key={item.id}>
<Text fz="md" fw={500} lh={1.45} truncate="end" lineClamp={1}> <TableTd style={{ maxWidth: 250 }}>
{item.name} <Text fz="md" fw={500} lh={1.45} truncate="end" lineClamp={1}>
</Text> {item.name}
</TableTd> </Text>
<TableTd> </TableTd>
<Text fz="sm" c="dimmed" lh={1.45}> <TableTd style={{ maxWidth: 250 }}>
{new Date(item.createdAt).toLocaleDateString('id-ID', { <Text fz="sm" c="dimmed" lh={1.45}>
day: 'numeric', {new Date(item.createdAt).toLocaleDateString('id-ID', {
month: 'long', day: 'numeric',
year: 'numeric', month: 'long',
})} year: 'numeric',
</Text> })}
</TableTd> </Text>
<TableTd> </TableTd>
<Text fz="sm" lh={1.45} truncate="end" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} /> <TableTd style={{ maxWidth: 250 }}>
</TableTd> <Text fz="sm" lh={1.45} truncate="end" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
<TableTd> </TableTd>
<Button <TableTd style={{ maxWidth: 250 }}>
variant="light" <Button
color="blue" variant="light"
onClick={() => router.push(`/admin/desa/gallery/video/${item.id}`)} color="blue"
fz="sm" onClick={() => router.push(`/admin/desa/gallery/video/${item.id}`)}
px="xs" fz="sm"
> px="xs"
<IconDeviceImac size={18} /> >
<Text ml={5}>Detail</Text> <IconDeviceImac size={18} />
</Button> <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> </TableTd>
</TableTr> </TableTr>
)) )}
) : ( </TableTbody>
<TableTr> </Table>
<TableTd colSpan={4}> </Box>
<Center py={24}>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada video yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box> </Box>
{/* Mobile Cards */} {/* Mobile Cards */}

View File

@@ -5,8 +5,7 @@ import {
Button, Button,
Center, Center,
Divider, Divider,
Grid, Group,
GridCol,
Paper, Paper,
Skeleton, Skeleton,
Stack, Stack,
@@ -43,32 +42,29 @@ function PelayananPendudukNonPermanent() {
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm"> <Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
<Stack gap="md"> <Stack gap="md">
{/* Header */} {/* Header */}
<Grid align="center"> <Group justify='space-between' align="center">
<GridCol span={{ base: 12, md: 11 }}>
<Title <Title
order={3} order={3}
lh={1.2} lh={1.2}
c={colors['blue-button']} c={colors['blue-button']}
> >
Preview Pelayanan Penduduk Non Permanen Preview Pelayanan Penduduk Non Permanen
</Title> </Title>
</GridCol> <Button
<GridCol span={{ base: 12, md: 1 }}> c="green"
<Button variant="light"
c="green" leftSection={<IconEdit size={18} stroke={2} />}
variant="light" radius="md"
leftSection={<IconEdit size={18} stroke={2} />} onClick={() =>
radius="md" router.push(
onClick={() => `/admin/desa/layanan/pelayanan_penduduk_non_permanent/${data.id}`
router.push( )
`/admin/desa/layanan/pelayanan_penduduk_non_permanent/${data.id}` }
) >
} Edit
> </Button>
Edit </Group>
</Button>
</GridCol>
</Grid>
{/* Content */} {/* Content */}
<Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs"> <Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs">

View File

@@ -6,8 +6,6 @@ import {
Button, Button,
Center, Center,
Divider, Divider,
Grid,
GridCol,
Group, Group,
Paper, Paper,
Skeleton, Skeleton,
@@ -76,28 +74,24 @@ function PerizinanBerusaha() {
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm"> <Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
<Stack gap="md"> <Stack gap="md">
{/* Header */} {/* Header */}
<Grid align="center"> <Group justify='space-between' align="center">
<GridCol span={{ base: 12, md: 11 }}> <Title order={3} c={colors['blue-button']} lh={1.2}>
<Title order={3} c={colors['blue-button']} lh={1.2}> Preview Pelayanan Perizinan Berusaha
Preview Pelayanan Perizinan Berusaha </Title>
</Title> <Button
</GridCol> c="green"
<GridCol span={{ base: 12, md: 1 }}> variant="light"
<Button leftSection={<IconEdit size={18} stroke={2} />}
c="green" radius="md"
variant="light" onClick={() =>
leftSection={<IconEdit size={18} stroke={2} />} router.push(
radius="md" `/admin/desa/layanan/pelayanan_perizinan_berusaha/${data.id}`
onClick={() => )
router.push( }
`/admin/desa/layanan/pelayanan_perizinan_berusaha/${data.id}` >
) Edit
} </Button>
> </Group>
Edit
</Button>
</GridCol>
</Grid>
{/* Content */} {/* Content */}
<Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs"> <Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs">
@@ -136,7 +130,7 @@ function PerizinanBerusaha() {
umum: umum:
</Text> </Text>
<Box p="xl" w="100%" visibleFrom='md'> <Box p="xl" w="100%" visibleFrom='md'>
<Stepper <Stepper
active={active} active={active}
onStepClick={setActive} onStepClick={setActive}
@@ -221,37 +215,37 @@ function PerizinanBerusaha() {
> >
<StepperStep label="Langkah Pertama" description="Pendaftaran Akun"> <StepperStep label="Langkah Pertama" description="Pendaftaran Akun">
<Text fz="sm" lh={1.5}> <Text fz="sm" lh={1.5}>
</Text> </Text>
</StepperStep> </StepperStep>
<StepperStep label="Langkah Kedua" description="Pengisian Data Perusahaan"> <StepperStep label="Langkah Kedua" description="Pengisian Data Perusahaan">
<Text fz="sm" lh={1.5}> <Text fz="sm" lh={1.5}>
</Text> </Text>
</StepperStep> </StepperStep>
<StepperStep label="Langkah Ketiga" description="Pemilihan KBLI"> <StepperStep label="Langkah Ketiga" description="Pemilihan KBLI">
<Text fz="sm" lh={1.5}> <Text fz="sm" lh={1.5}>
</Text> </Text>
</StepperStep> </StepperStep>
<StepperStep label="Langkah Keempat" description="Pengunggahan Dokumen"> <StepperStep label="Langkah Keempat" description="Pengunggahan Dokumen">
<Text fz="sm" lh={1.5}> <Text fz="sm" lh={1.5}>
</Text> </Text>
</StepperStep> </StepperStep>
<StepperStep label="Langkah Kelima" description="Verifikasi dan Persetujuan"> <StepperStep label="Langkah Kelima" description="Verifikasi dan Persetujuan">
<Text fz="sm" lh={1.5}> <Text fz="sm" lh={1.5}>
</Text> </Text>
</StepperStep> </StepperStep>
<StepperStep label="Langkah Keenam" description="Penerimaan NIB"> <StepperStep label="Langkah Keenam" description="Penerimaan NIB">
<Text fz="sm" lh={1.5}> <Text fz="sm" lh={1.5}>
</Text> </Text>
</StepperStep> </StepperStep>
<StepperCompleted> <StepperCompleted>
<Text fz="sm" lh={1.5}> <Text fz="sm" lh={1.5}>
</Text> </Text>
</StepperCompleted> </StepperCompleted>
</Stepper> </Stepper>

View File

@@ -1,7 +1,7 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Divider, Grid, GridCol, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core'; import { Box, Button, Center, Divider, Grid, GridCol, Group, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
import { IconEdit } from '@tabler/icons-react'; import { IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect } from 'react'; import { useEffect } from 'react';
@@ -31,22 +31,18 @@ function Page() {
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm"> <Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
<Stack gap="md"> <Stack gap="md">
{/* Header + tombol edit */} {/* Header + tombol edit */}
<Grid align="center"> <Group justify="space-between">
<GridCol span={{ base: 12, md: 11 }}> <Title order={2} c={colors['blue-button']} lh={1.2}>Profil Perbekel</Title>
<Title order={2} c={colors['blue-button']} lh={1.2} /> <Button
</GridCol> c="green"
<GridCol span={{ base: 12, md: 1 }}> variant="light"
<Button leftSection={<IconEdit size={18} stroke={2} />}
c="green" radius="md"
variant="light" onClick={() => router.push(`/admin/desa/profil/profil-perbekel/${perbekel.id}`)}
leftSection={<IconEdit size={18} stroke={2} />} >
radius="md" Edit
onClick={() => router.push(`/admin/desa/profil/profil-perbekel/${perbekel.id}`)} </Button>
> </Group>
Edit
</Button>
</GridCol>
</Grid>
{/* Card Profil */} {/* Card Profil */}
<Paper p="xl" bg={colors['white-1']} withBorder radius="md" shadow="xs"> <Paper p="xl" bg={colors['white-1']} withBorder radius="md" shadow="xs">
@@ -60,7 +56,7 @@ function Page() {
<GridCol span={12}> <GridCol span={12}>
<Text <Text
ta="center" ta="center"
fz={{ base: 'sm', md: 'md' }} fz={{ base: 'sm', md: 'xl' }}
fw="bold" fw="bold"
c={colors['blue-button']} c={colors['blue-button']}
lh={{ base: 1.45, md: 1.45 }} lh={{ base: 1.45, md: 1.45 }}

View File

@@ -2,6 +2,7 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { import {
Box,
ScrollArea, ScrollArea,
Stack, Stack,
Tabs, Tabs,
@@ -85,36 +86,76 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
keepMounted={false} keepMounted={false}
> >
{/* ✅ Scroll horizontal wrapper */} {/* ✅ Scroll horizontal wrapper */}
<ScrollArea type="auto" offsetScrollbars> <Box visibleFrom='md' pb={10}>
<TabsList <ScrollArea type="auto" offsetScrollbars>
p="sm" <TabsList
style={{ p="sm"
background: "linear-gradient(135deg, #e7ebf7, #f9faff)", style={{
borderRadius: "1rem", background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)", borderRadius: "1rem",
display: "flex", boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
flexWrap: "nowrap", display: "flex",
gap: "0.5rem", flexWrap: "nowrap",
paddingInline: "0.5rem", 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%"
> >
{tabs.map((tab, i) => (
<TabsTab <TabsList
key={i} p="xs" // lebih kecil
value={tab.value} style={{
leftSection={tab.icon} background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
style={{ borderRadius: "1rem",
fontWeight: 600, display: "flex",
fontSize: "0.9rem", flexWrap: "nowrap",
transition: "all 0.2s ease", gap: "0.5rem",
flexShrink: 0, width: "max-content", // ⬅️ kunci
}} maxWidth: "100%", // ⬅️ penting
> }}
{tab.label} >
</TabsTab> {tabs.map((tab, i) => (
))} <TabsTab
</TabsList> key={i}
</ScrollArea> 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) => ( {tabs.map((tab, i) => (
<TabsPanel <TabsPanel

View File

@@ -139,7 +139,7 @@ function EditAPBDesa() {
} }
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 0, md: 'xs' }} py="xs">
{/* Header */} {/* Header */}
<Group mb="md"> <Group mb="md">
<Button <Button

View File

@@ -57,7 +57,7 @@ function DetailAPBDesa() {
const data = apbState.findUnique.data; const data = apbState.findUnique.data;
return ( return (
<Box py={10}> <Box px={{ base: 0, md: 'xs' }} py="xs">
<Button <Button
variant="subtle" variant="subtle"
onClick={() => router.back()} onClick={() => router.back()}

View File

@@ -51,7 +51,7 @@ function CreateAPBDesa() {
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 0, md: 'xs' }} py="xs">
{/* Header */} {/* Header */}
<Group mb="md"> <Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md"> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">

View File

@@ -15,13 +15,14 @@ import {
TableTh, TableTh,
TableThead, TableThead,
TableTr, TableTr,
Text Text,
Title
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import PendapatanAsliDesa from '../../../_state/ekonomi/PADesa'; import PendapatanAsliDesa from '../../../_state/ekonomi/PADesa';
@@ -44,6 +45,7 @@ function APBDesa() {
function ListAPBDesa({ search }: { search: string }) { function ListAPBDesa({ search }: { search: string }) {
const apbDesaState = useProxy(PendapatanAsliDesa.ApbDesa); const apbDesaState = useProxy(PendapatanAsliDesa.ApbDesa);
const router = useRouter(); const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { const {
data, data,
@@ -61,26 +63,26 @@ function ListAPBDesa({ search }: { search: string }) {
}).format(value); }).format(value);
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search); load(page, 10, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
const filteredData = data || []; const filteredData = data || [];
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={{ base: 'sm', md: 'md' }}>
<Skeleton height={500} radius="md" /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
); );
} }
return ( return (
<Box py={10}> <Box py={{ base: 'sm', md: 'md' }}>
<Paper withBorder bg={colors["white-1"]} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors["white-1"]} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
<Group justify="space-between" mb="md"> <Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
<Text fw={600} fz="lg"> <Title order={4} lh={1.2}>
List APB Desa List APB Desa
</Text> </Title>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
@@ -94,58 +96,88 @@ function ListAPBDesa({ search }: { search: string }) {
Tambah Baru Tambah Baru
</Button> </Button>
</Group> </Group>
<Box style={{ overflowX: "auto" }}>
<Table highlightOnHover> {/* Desktop Table */}
<Box visibleFrom="md">
<Table
highlightOnHover
miw={0}
style={{
tableLayout: 'fixed',
width: '100%',
}}
>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh style={{ width: "15%" }}>Tahun</TableTh> <TableTh style={{ width: "15%" }}>
<TableTh style={{ width: "25%" }}>Pembiayaan</TableTh> <Text fz="sm" fw={600} lh={1.4} ta="left">Tahun</Text>
<TableTh style={{ width: "25%" }}>Belanja</TableTh> </TableTh>
<TableTh style={{ width: "25%" }}>Pendapatan</TableTh> <TableTh style={{ width: "25%" }}>
<TableTh style={{ width: "10%" }}>Aksi</TableTh> <Text fz="sm" fw={600} lh={1.4} ta="left">Pembiayaan</Text>
</TableTh>
<TableTh style={{ width: "25%" }}>
<Text fz="sm" fw={600} lh={1.4} ta="left">Belanja</Text>
</TableTh>
<TableTh style={{ width: "25%" }}>
<Text fz="sm" fw={600} lh={1.4} ta="left">Pendapatan</Text>
</TableTh>
<TableTh style={{ width: "10%" }}>
<Text fz="sm" fw={600} lh={1.4} ta="left">Aksi</Text>
</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.length > 0 ? ( {filteredData.length > 0 ? (
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd>{item.tahun}</TableTd>
<TableTd> <TableTd>
{formatRupiah( <Text fz="md" fw={500} lh={1.5} ta="left">{item.tahun}</Text>
item.pembiayaan.reduce(
(sum, val) => sum + Number(val.value),
0
)
)}
</TableTd> </TableTd>
<TableTd> <TableTd>
{formatRupiah( <Text fz="md" fw={500} lh={1.5} ta="left">
item.belanja.reduce( {formatRupiah(
(sum, val) => sum + Number(val.value), item.pembiayaan.reduce(
0 (sum, val) => sum + Number(val.value),
) 0
)} )
)}
</Text>
</TableTd> </TableTd>
<TableTd> <TableTd>
{formatRupiah( <Text fz="md" fw={500} lh={1.5} ta="left">
item.pendapatan.reduce( {formatRupiah(
(sum, val) => sum + Number(val.value), item.belanja.reduce(
0 (sum, val) => sum + Number(val.value),
) 0
)} )
)}
</Text>
</TableTd>
<TableTd>
<Text fz="md" fw={500} lh={1.5} ta="left">
{formatRupiah(
item.pendapatan.reduce(
(sum, val) => sum + Number(val.value),
0
)
)}
</Text>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button <Button
variant="light" variant="light"
color="green" color="blue"
onClick={() => onClick={() =>
router.push( router.push(
`/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/${item.id}` `/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/${item.id}`
) )
} }
size="compact-sm"
> >
<IconDeviceImacCog size={20} /> <IconDeviceImacCog size={16} />
<Text ml={5}>Detail</Text> <Text ml={5} fz="sm" fw={500} lh={1.4}>
Detail
</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -154,7 +186,7 @@ function ListAPBDesa({ search }: { search: string }) {
<TableTr> <TableTr>
<TableTd colSpan={5}> <TableTd colSpan={5}>
<Center py={20}> <Center py={20}>
<Text color="dimmed"> <Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data APB Desa yang cocok Tidak ada data APB Desa yang cocok
</Text> </Text>
</Center> </Center>
@@ -164,7 +196,81 @@ function ListAPBDesa({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
{/* Mobile Cards */}
<Box hiddenFrom="md">
<Stack gap="sm">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} withBorder p="md" radius="sm">
<Stack gap={"xs"}>
<Box>
<Text fz="xs" fw={600} lh={1.4}>Tahun</Text>
<Text fz="sm" fw={500} lh={1.4}>{item.tahun}</Text>
</Box>
<Box>
<Text fz="xs" fw={600} lh={1.4}>Pembiayaan</Text>
<Text fz="sm" fw={500} lh={1.4}>
{formatRupiah(
item.pembiayaan.reduce(
(sum, val) => sum + Number(val.value),
0
)
)}
</Text>
</Box>
<Box>
<Text fz="xs" fw={600} lh={1.4}>Belanja</Text>
<Text fz="sm" fw={500} lh={1.4}>
{formatRupiah(
item.belanja.reduce(
(sum, val) => sum + Number(val.value),
0
)
)}
</Text>
</Box>
<Box>
<Text fz="xs" fw={600} lh={1.4}>Pendapatan</Text>
<Text fz="sm" fw={500} lh={1.4}>
{formatRupiah(
item.pendapatan.reduce(
(sum, val) => sum + Number(val.value),
0
)
)}
</Text>
</Box>
<Button
variant="light"
color="blue"
fullWidth
onClick={() =>
router.push(
`/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/${item.id}`
)
}
size="sm"
>
<IconDeviceImacCog size={16} />
<Text ml={5} fz="sm" fw={500} lh={1.4}>
Detail
</Text>
</Button>
</Stack>
</Paper>
))
) : (
<Center py={20}>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data APB Desa yang cocok
</Text>
</Center>
)}
</Stack>
</Box>
</Paper> </Paper>
<Center> <Center>
<Pagination <Pagination
value={page} value={page}
@@ -183,4 +289,4 @@ function ListAPBDesa({ search }: { search: string }) {
); );
} }
export default APBDesa; export default APBDesa;

View File

@@ -108,7 +108,7 @@ function EditBelanja() {
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 0, md: 'xs' }} py="xs">
{/* Header */} {/* Header */}
<Group mb="md"> <Group mb="md">
<Button <Button

View File

@@ -64,7 +64,7 @@ function CreateBelanja() {
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 0, md: 'xs' }} py="xs">
{/* Header dengan back button */} {/* Header dengan back button */}
<Group mb="md"> <Group mb="md">
<Button <Button

View File

@@ -19,7 +19,7 @@ import {
Text, Text,
Title Title
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -31,7 +31,7 @@ import PendapatanAsliDesa from '../../../_state/ekonomi/PADesa';
function Belanja() { function Belanja() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
return ( return (
<Box> <Stack gap="xl">
<HeaderSearch <HeaderSearch
title="Belanja" title="Belanja"
placeholder="Cari belanja berdasarkan nama atau nilai..." placeholder="Cari belanja berdasarkan nama atau nilai..."
@@ -40,7 +40,7 @@ function Belanja() {
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
/> />
<ListBelanja search={search} /> <ListBelanja search={search} />
</Box> </Stack>
); );
} }
@@ -49,6 +49,7 @@ function ListBelanja({ search }: { search: string }) {
const router = useRouter(); const router = useRouter();
const [modalHapus, setModalHapus] = useState(false); const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null); const [selectedId, setSelectedId] = useState<string | null>(null);
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { const {
data, data,
@@ -72,29 +73,35 @@ function ListBelanja({ search }: { search: string }) {
belanjaState.delete.byId(selectedId); belanjaState.delete.byId(selectedId);
setModalHapus(false); setModalHapus(false);
setSelectedId(null); setSelectedId(null);
load(page, 10, search); load(page, 10, debouncedSearch);
} }
}; };
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search); load(page, 10, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
const filteredData = data || []; const filteredData = data || [];
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py="md">
<Skeleton height={600} radius="md" /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
); );
} }
return ( return (
<Box py={10}> <Stack gap="xl">
{/* Desktop Table */}
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md"> <Group justify="space-between" mb="md">
<Title order={4}>Daftar Belanja</Title> <Title
order={4}
lh={{ base: 1.2, md: 1.1 }}
>
Daftar Belanja
</Title>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
@@ -107,14 +114,32 @@ function ListBelanja({ search }: { search: string }) {
</Button> </Button>
</Group> </Group>
<Box style={{ overflowX: 'auto' }}> <Box visibleFrom="md">
<Table highlightOnHover striped withTableBorder withRowBorders> <Table
highlightOnHover
miw={0}
striped
withTableBorder
withRowBorders
style={{
tableLayout: 'fixed',
width: '100%',
}}
>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Nama</TableTh> <TableTh style={{ width: '40%' }}>
<TableTh>Nilai</TableTh> <Text fz="sm" fw={600} lh={1.4}>Nama</Text>
<TableTh>Persentase</TableTh> </TableTh>
<TableTh>Aksi</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> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
@@ -123,15 +148,19 @@ function ListBelanja({ search }: { search: string }) {
{filteredData.map((item) => ( {filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Text fw={500} truncate="end" lineClamp={1}> <Text fz="md" fw={500} lh={1.45} truncate="end" lineClamp={1}>
{item.name} {item.name}
</Text> </Text>
</TableTd> </TableTd>
<TableTd>{formatRupiah(item.value)}</TableTd>
<TableTd> <TableTd>
{totalBelanja > 0 <Text fz="md" lh={1.45}>{formatRupiah(item.value)}</Text>
? ((item.value / totalBelanja) * 100).toFixed(0) + '%' </TableTd>
: '0%'} <TableTd>
<Text fz="md" lh={1.45}>
{totalBelanja > 0
? ((item.value / totalBelanja) * 100).toFixed(0) + '%'
: '0%'}
</Text>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Group gap="xs"> <Group gap="xs">
@@ -165,18 +194,20 @@ function ListBelanja({ search }: { search: string }) {
))} ))}
<TableTr> <TableTr>
<TableTd colSpan={2}> <TableTd colSpan={2}>
<Text fw="bold">Total</Text> <Text fz="md" fw={600} lh={1.45}>Total</Text>
</TableTd> </TableTd>
<TableTd colSpan={2}> <TableTd colSpan={2}>
<Text fw="bold">{formatRupiah(totalBelanja)}</Text> <Text fz="md" fw={600} lh={1.45}>{formatRupiah(totalBelanja)}</Text>
</TableTd> </TableTd>
</TableTr> </TableTr>
</> </>
) : ( ) : (
<TableTr> <TableTr>
<TableTd colSpan={4}> <TableTd colSpan={4}>
<Center py={20}> <Center py="xl">
<Text c="dimmed">Tidak ada data belanja yang cocok</Text> <Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data belanja yang cocok
</Text>
</Center> </Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -186,21 +217,107 @@ function ListBelanja({ search }: { search: string }) {
</Box> </Box>
</Paper> </Paper>
{/* Mobile Cards */}
<Stack visibleFrom="xs" hiddenFrom="md" gap="sm">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} withBorder p="md" radius="md">
<Stack gap={'xs'}>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Nama
</Text>
<Text fz="sm" fw={500} lh={1.4} truncate="end">
{item.name}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Nilai
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{formatRupiah(item.value)}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Persentase
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{totalBelanja > 0
? ((item.value / totalBelanja) * 100).toFixed(0) + '%'
: '0%'}
</Text>
</Box>
<Group justify="flex-end" mt="xs">
<Button
size="xs"
variant="light"
color="green"
onClick={() =>
router.push(
`/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja/${item.id}`
)
}
>
<IconEdit size={16} />
</Button>
<Button
size="xs"
variant="light"
color="red"
disabled={belanjaState.delete.loading}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={16} />
</Button>
</Group>
</Stack>
</Paper>
))
) : (
<Paper withBorder p="xl" radius="md">
<Center>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data belanja yang cocok
</Text>
</Center>
</Paper>
)}
{filteredData.length > 0 && (
<Paper withBorder p="md" radius="md">
<Group justify="space-between">
<Text fz="sm" fw={600} lh={1.4}>
Total
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{formatRupiah(totalBelanja)}
</Text>
</Group>
</Paper>
)}
</Stack>
{/* Pagination */} {/* Pagination */}
<Center> {(totalPages > 1 || page > 1) && (
<Pagination <Center>
value={page} <Pagination
onChange={(newPage) => { value={page}
load(newPage, 10, search); onChange={(newPage) => {
window.scrollTo({ top: 0, behavior: 'smooth' }); load(newPage, 10, search);
}} window.scrollTo({ top: 0, behavior: 'smooth' });
total={totalPages} }}
mt="md" total={totalPages}
mb="md" mt="md"
color="blue" color="blue"
radius="md" radius="md"
/> />
</Center> </Center>
)}
{/* Modal Konfirmasi Hapus */} {/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
@@ -209,8 +326,8 @@ function ListBelanja({ search }: { search: string }) {
onConfirm={handleDelete} onConfirm={handleDelete}
text="Apakah anda yakin ingin menghapus belanja ini?" text="Apakah anda yakin ingin menghapus belanja ini?"
/> />
</Box> </Stack>
); );
} }
export default Belanja; export default Belanja;

View File

@@ -1,7 +1,30 @@
'use client'
import React from 'react'; import React from 'react';
import LayoutTabs from './_lib/layoutTabs'; import LayoutTabs from './_lib/layoutTabs';
import { usePathname } from 'next/navigation';
import { Box } from '@mantine/core';
function Layout({ children }: { children: React.ReactNode }) { function Layout({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
// Contoh path:
// - /darmasaba/desa/berita/semua → panjang 5 → list
// - /darmasaba/desa/berita/Pemerintahan → panjang 5 → list
// - /darmasaba/desa/berita/Pemerintahan/123 → panjang 6 → detail
const segments = pathname.split('/').filter(Boolean);
const isDetailPage = segments.length >= 5;
if (isDetailPage) {
// Tampilkan tanpa tab menu
return (
<Box>
{children}
</Box>
);
}
return ( return (
<LayoutTabs> <LayoutTabs>
{children} {children}

View File

@@ -105,7 +105,7 @@ function EditPembiayaan() {
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 0, md: 'xs' }} py="xs">
{/* Header */} {/* Header */}
<Group mb="md"> <Group mb="md">
<Button <Button

View File

@@ -63,7 +63,7 @@ function CreatePembiayaan() {
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 0, md: 'xs' }} py="xs">
{/* Header */} {/* Header */}
<Group mb="md"> <Group mb="md">
<Button <Button

View File

@@ -16,9 +16,9 @@ import {
TableThead, TableThead,
TableTr, TableTr,
Text, Text,
Title Title,
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -48,6 +48,7 @@ function ListPembiayaan({ search }: { search: string }) {
const router = useRouter(); const router = useRouter();
const [modalHapus, setModalHapus] = useState(false); const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null); const [selectedId, setSelectedId] = useState<string | null>(null);
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { const {
data, data,
@@ -71,17 +72,17 @@ function ListPembiayaan({ search }: { search: string }) {
pembiayaanState.delete.byId(selectedId); pembiayaanState.delete.byId(selectedId);
setModalHapus(false); setModalHapus(false);
setSelectedId(null); setSelectedId(null);
load(page, 10, search); load(page, 10, debouncedSearch);
} }
}; };
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search); load(page, 10, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py="lg">
<Skeleton height={500} radius="md" /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
); );
@@ -90,10 +91,10 @@ function ListPembiayaan({ search }: { search: string }) {
const filteredData = data || []; const filteredData = data || [];
return ( return (
<Box py={10}> <Stack gap="xl" py="lg">
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md"> <Group justify="space-between" mb="md">
<Title order={4}>Daftar Pembiayaan</Title> <Title order={4} lh={1.2}>Daftar Pembiayaan</Title>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
@@ -106,14 +107,33 @@ function ListPembiayaan({ search }: { search: string }) {
</Button> </Button>
</Group> </Group>
<Box style={{ overflowX: 'auto' }}> {/* Desktop Table */}
<Table highlightOnHover striped withTableBorder withRowBorders> <Box visibleFrom="md">
<TableThead> <Table
<TableTr> highlightOnHover
<TableTh>Nama</TableTh> striped
<TableTh>Nilai</TableTh> withTableBorder
<TableTh>Persentase</TableTh> withRowBorders
<TableTh style={{ width: '20%' }}>Aksi</TableTh> miw={0}
style={{
tableLayout: 'fixed',
width: '100%',
}}
>
<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> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
@@ -122,15 +142,19 @@ function ListPembiayaan({ search }: { search: string }) {
{filteredData.map((item) => ( {filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Text fw={500} truncate="end" lineClamp={1}> <Text fz="md" fw={500} lh={1.5} truncate="end" lineClamp={1}>
{item.name} {item.name}
</Text> </Text>
</TableTd> </TableTd>
<TableTd>{formatRupiah(item.value)}</TableTd>
<TableTd> <TableTd>
{totalPembiayaan > 0 <Text fz="md" fw={500} lh={1.5}>{formatRupiah(item.value)}</Text>
? ((item.value / totalPembiayaan) * 100).toFixed(0) + '%' </TableTd>
: '0%'} <TableTd>
<Text fz="md" fw={500} lh={1.5}>
{totalPembiayaan > 0
? ((item.value / totalPembiayaan) * 100).toFixed(0) + '%'
: '0%'}
</Text>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Group gap="xs"> <Group gap="xs">
@@ -163,16 +187,20 @@ function ListPembiayaan({ search }: { search: string }) {
{/* Total Row */} {/* Total Row */}
<TableTr> <TableTr>
<TableTd colSpan={2}> <TableTd colSpan={2}>
<Text fw="bold">Total</Text> <Text fz="md" fw={600} lh={1.5}>Total</Text>
</TableTd>
<TableTd colSpan={2}>
<Text fz="md" fw={600} lh={1.5}>{formatRupiah(totalPembiayaan)}</Text>
</TableTd> </TableTd>
<TableTd colSpan={2}>{formatRupiah(totalPembiayaan)}</TableTd>
</TableTr> </TableTr>
</> </>
) : ( ) : (
<TableTr> <TableTr>
<TableTd colSpan={4}> <TableTd colSpan={4}>
<Center py={20}> <Center py={20}>
<Text c="dimmed">Tidak ada data pembiayaan yang cocok</Text> <Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data pembiayaan yang cocok
</Text>
</Center> </Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -180,6 +208,79 @@ function ListPembiayaan({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
{/* Mobile Card View */}
<Stack gap="sm" hiddenFrom="md">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} withBorder p="md" radius="md">
<Stack gap={"xs"}>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Nama</Text>
<Text fz="sm" fw={500} lh={1.4} truncate="end">
{item.name}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Nilai</Text>
<Text fz="sm" fw={500} lh={1.4}>
{formatRupiah(item.value)}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Persentase</Text>
<Text fz="sm" fw={500} lh={1.4}>
{totalPembiayaan > 0
? ((item.value / totalPembiayaan) * 100).toFixed(0) + '%'
: '0%'}
</Text>
</Box>
<Group justify="flex-end" mt="xs">
<Button
color="green"
variant="light"
size="xs"
onClick={() =>
router.push(
`/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/${item.id}`
)
}
>
<IconEdit size={16} />
</Button>
<Button
color="red"
variant="light"
size="xs"
disabled={pembiayaanState.delete.loading}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={16} />
</Button>
</Group>
</Stack>
</Paper>
))
) : (
<Center py={20}>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data pembiayaan yang cocok
</Text>
</Center>
)}
{filteredData.length > 0 && (
<Paper withBorder p="md" radius="md">
<Stack gap={4}>
<Text fz="sm" fw={600} lh={1.4}>Total</Text>
<Text fz="sm" fw={500} lh={1.4}>{formatRupiah(totalPembiayaan)}</Text>
</Stack>
</Paper>
)}
</Stack>
</Paper> </Paper>
{/* Pagination */} {/* Pagination */}
@@ -205,8 +306,8 @@ function ListPembiayaan({ search }: { search: string }) {
onConfirm={handleDelete} onConfirm={handleDelete}
text="Apakah anda yakin ingin menghapus pembiayaan ini?" text="Apakah anda yakin ingin menghapus pembiayaan ini?"
/> />
</Box> </Stack>
); );
} }
export default Pembiayaan; export default Pembiayaan;

View File

@@ -114,7 +114,7 @@ function EditPendapatan() {
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 0, md: 'xs' }} py="xs">
{/* Header with Back Button */} {/* Header with Back Button */}
<Group mb="md"> <Group mb="md">
<Button <Button

View File

@@ -57,7 +57,7 @@ function CreatePendapatan() {
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 0, md: 'xs' }} py="xs">
{/* Header dengan tombol back + judul */} {/* Header dengan tombol back + judul */}
<Group mb="md"> <Group mb="md">
<Button <Button

View File

@@ -19,7 +19,7 @@ import {
Text, Text,
Title Title
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -49,6 +49,7 @@ function ListPendapatan({ search }: { search: string }) {
const router = useRouter(); const router = useRouter();
const [modalHapus, setModalHapus] = useState(false); const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null); const [selectedId, setSelectedId] = useState<string | null>(null);
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { const {
data, data,
@@ -70,19 +71,19 @@ function ListPendapatan({ search }: { search: string }) {
pendapatanState.delete.byId(selectedId); pendapatanState.delete.byId(selectedId);
setModalHapus(false); setModalHapus(false);
setSelectedId(null); setSelectedId(null);
load(page, 10, search); load(page, 10, debouncedSearch);
} }
}; };
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search); load(page, 10, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
const filteredData = data || []; const filteredData = data || [];
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={{ base: 'sm', md: 'md' }}>
<Skeleton height={600} radius="md" /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
); );
@@ -91,10 +92,12 @@ function ListPendapatan({ search }: { search: string }) {
const totalValue = filteredData.reduce((total, item) => total + item.value, 0); const totalValue = filteredData.reduce((total, item) => total + item.value, 0);
return ( return (
<Box py={10}> <Box py={{ base: 'sm', md: 'md' }}>
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
<Group justify="space-between" mb="md"> <Group justify="space-between" mb="md">
<Title order={4}>Daftar Pendapatan</Title> <Title order={2} lh={1.2}>
Daftar Pendapatan
</Title>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
@@ -107,14 +110,30 @@ function ListPendapatan({ search }: { search: string }) {
</Button> </Button>
</Group> </Group>
<Box style={{ overflowX: 'auto' }}> {/* Desktop Table */}
<Table highlightOnHover striped withTableBorder withRowBorders> <Box visibleFrom="md">
<Table
highlightOnHover
miw={0}
style={{
tableLayout: 'fixed',
width: '100%',
}}
>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh style={{ width: '40%' }}>Nama</TableTh> <TableTh style={{ width: '40%' }}>
<TableTh style={{ width: '25%' }}>Nilai</TableTh> <Text fz="sm" fw={600} lh={1.4}>Nama</Text>
<TableTh style={{ width: '15%' }}>Edit</TableTh> </TableTh>
<TableTh style={{ width: '15%' }}>Delete</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> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
@@ -123,11 +142,13 @@ function ListPendapatan({ search }: { search: string }) {
{filteredData.map((item) => ( {filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Text fw={500} truncate="end" lineClamp={1}> <Text fz="md" fw={500} lh={1.5} truncate="end">
{item.name} {item.name}
</Text> </Text>
</TableTd> </TableTd>
<TableTd>{formatRupiah(item.value)}</TableTd> <TableTd>
<Text fz="md" fw={500} lh={1.5}>{formatRupiah(item.value)}</Text>
</TableTd>
<TableTd> <TableTd>
<Button <Button
variant="light" variant="light"
@@ -135,9 +156,10 @@ function ListPendapatan({ search }: { search: string }) {
onClick={() => onClick={() =>
router.push(`/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/${item.id}`) router.push(`/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/${item.id}`)
} }
fz="sm"
px="xs"
> >
<IconEdit size={18} /> <IconEdit size={16} />
<Text ml={5}>Edit</Text>
</Button> </Button>
</TableTd> </TableTd>
<TableTd> <TableTd>
@@ -149,9 +171,10 @@ function ListPendapatan({ search }: { search: string }) {
setSelectedId(item.id); setSelectedId(item.id);
setModalHapus(true); setModalHapus(true);
}} }}
fz="sm"
px="xs"
> >
<IconTrash size={18} /> <IconTrash size={16} />
<Text ml={5}>Hapus</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -159,19 +182,21 @@ function ListPendapatan({ search }: { search: string }) {
{/* Row total */} {/* Row total */}
<TableTr> <TableTr>
<TableTd colSpan={1}> <TableTd>
<Text fw={'bold'}>Total</Text> <Text fz="md" fw={700} lh={1.5}>Total</Text>
</TableTd> </TableTd>
<TableTd colSpan={3}> <TableTd colSpan={3}>
<Text fw={'bold'}>{formatRupiah(totalValue)}</Text> <Text fz="md" fw={700} lh={1.5}>{formatRupiah(totalValue)}</Text>
</TableTd> </TableTd>
</TableTr> </TableTr>
</> </>
) : ( ) : (
<TableTr> <TableTr>
<TableTd colSpan={4}> <TableTd colSpan={4}>
<Center py={20}> <Center py={24}>
<Text color="dimmed">Tidak ada data pendapatan yang cocok</Text> <Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data pendapatan yang cocok
</Text>
</Center> </Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -179,23 +204,85 @@ function ListPendapatan({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
{/* Mobile Cards */}
<Stack gap="xs" hiddenFrom="md">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} withBorder radius="md" p="md">
<Stack gap={4}>
<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}>Nilai</Text>
<Text fz="sm" fw={500} lh={1.4}>{formatRupiah(item.value)}</Text>
</Box>
<Group justify="flex-end" mt="xs">
<Button
variant="light"
color="green"
size="xs"
onClick={() =>
router.push(`/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/${item.id}`)
}
>
<IconEdit size={14} />
<Text ml={4}>Edit</Text>
</Button>
<Button
variant="light"
color="red"
size="xs"
disabled={pendapatanState.delete.loading}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={14} />
<Text ml={4}>Hapus</Text>
</Button>
</Group>
</Stack>
</Paper>
))
) : (
<Center py={24}>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data pendapatan yang cocok
</Text>
</Center>
)}
{filteredData.length > 0 && (
<Paper withBorder radius="md" p="md">
<Box>
<Text fz="xs" fw={600} lh={1.4}>Total</Text>
<Text fz="sm" fw={700} lh={1.4}>{formatRupiah(totalValue)}</Text>
</Box>
</Paper>
)}
</Stack>
</Paper> </Paper>
{/* Pagination */} {/* Pagination */}
<Center> {totalPages > 1 && (
<Pagination <Center mt={{ base: 'sm', md: 'md' }} mb={{ base: 'sm', md: 'md' }}>
value={page} <Pagination
onChange={(newPage) => { value={page}
load(newPage, 10, search); onChange={(newPage) => {
window.scrollTo({ top: 0, behavior: 'smooth' }); load(newPage, 10, search);
}} window.scrollTo({ top: 0, behavior: 'smooth' });
total={totalPages} }}
mt="md" total={totalPages}
mb="md" color="blue"
color="blue" radius="md"
radius="md" size="sm"
/> />
</Center> </Center>
)}
{/* Modal Konfirmasi Hapus */} {/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
@@ -208,4 +295,4 @@ function ListPendapatan({ search }: { search: string }) {
); );
} }
export default Pendapatan; export default Pendapatan;

View File

@@ -0,0 +1,171 @@
/* 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 {
IconBuildingCommunity,
IconHierarchy,
IconUsers
} 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: "Pegawai",
value: "pegawai",
href: "/admin/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai",
icon: <IconUsers size={18} stroke={1.8} />
},
{
label: "Posisi Organisasi",
value: "posisiorganisasi",
href: "/admin/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/posisi-organisasi",
icon: <IconHierarchy size={18} stroke={1.8} />
},
{
label: "Struktur Organisasi",
value: "strukturorganisasi",
href: "/admin/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/struktur-organisasi",
icon: <IconBuildingCommunity 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" }}>
Struktur Organisasi & SK Pengurus BUMDes
</Title>
<Tabs
color={colors["blue-button"]}
variant="pills"
value={activeTab}
onChange={handleTabChange}
radius="lg"
keepMounted={false}
>
{/* ✅ Scroll horizontal biar rapi kalau label panjang */}
<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;

View File

@@ -0,0 +1,33 @@
'use client'
import { usePathname } from "next/navigation";
import LayoutTabs from "./_lib/layoutTabs"
import { Box } from "@mantine/core";
export default function Layout({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
// Contoh path:
// - /darmasaba/desa/berita/semua → panjang 5 → list
// - /darmasaba/desa/berita/Pemerintahan → panjang 5 → list
// - /darmasaba/desa/berita/Pemerintahan/123 → panjang 6 → detail
const segments = pathname.split('/').filter(Boolean);
const isDetailPage = segments.length >= 5;
if (isDetailPage) {
// Tampilkan tanpa tab menu
return (
<Box>
{children}
</Box>
);
}
return (
<LayoutTabs>
{children}
</LayoutTabs>
)
}

View File

@@ -143,7 +143,7 @@ export default function EditPegawaiBumDes() {
if (id && !stateOrganisasi.edit.id) stateOrganisasi.edit.id = id; if (id && !stateOrganisasi.edit.id) stateOrganisasi.edit.id = id;
const success = await stateOrganisasi.edit.submit(); const success = await stateOrganisasi.edit.submit();
if (success) router.push('/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai'); if (success) router.push('/admin/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai');
} catch (error) { } catch (error) {
console.error('Error updating pegawai:', error); console.error('Error updating pegawai:', error);
toast.error(error instanceof Error ? error.message : 'Gagal memperbarui data pegawai'); toast.error(error instanceof Error ? error.message : 'Gagal memperbarui data pegawai');
@@ -153,12 +153,12 @@ export default function EditPegawaiBumDes() {
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 0, md: 'xs' }} py="xs">
<Group mb="md"> <Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md"> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
<Title order={4} ml="sm" c="dark">Edit Data Pegawai PPID</Title> <Title order={4} ml="sm" c="dark">Edit Data Pegawai BumDes</Title>
</Group> </Group>
<Paper <Paper

View File

@@ -1,5 +1,6 @@
'use client' 'use client'
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import { ModalKonfirmasiNonAktif } from '@/app/admin/(dashboard)/_com/modalNonaktif';
import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi'; import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
import colors from '@/con/colors'; import colors from '@/con/colors';
@@ -28,7 +29,7 @@ function DetailPegawai() {
statePegawai.delete.byId(selectedId); statePegawai.delete.byId(selectedId);
setModalHapus(false); setModalHapus(false);
setSelectedId(null); setSelectedId(null);
router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai"); router.push("/admin/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai");
} }
}; };
@@ -37,7 +38,7 @@ function DetailPegawai() {
statePegawai.nonActive.byId(selectedId); statePegawai.nonActive.byId(selectedId);
setModalNonActive(false); setModalNonActive(false);
setSelectedId(null); setSelectedId(null);
router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai"); router.push("/admin/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai");
} }
}; };
@@ -52,7 +53,7 @@ function DetailPegawai() {
const data = statePegawai.findUnique.data; const data = statePegawai.findUnique.data;
return ( return (
<Box> <Box px={{ base: 0, md: 'xs' }} py="xs">
<Box mb={10}> <Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}> <Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} /> <IconArrowBack color={colors['blue-button']} size={25} />
@@ -60,7 +61,7 @@ function DetailPegawai() {
</Box> </Box>
<Paper <Paper
withBorder withBorder
w={{ base: "100%", md: "60%" }} w={{ base: "100%", md: "70%" }}
bg={colors['white-1']} bg={colors['white-1']}
p="lg" p="lg"
radius="md" radius="md"
@@ -68,7 +69,7 @@ function DetailPegawai() {
> >
<Stack gap="md"> <Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}> <Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Pegawai PPID Detail Pegawai BumDes
</Text> </Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs"> <Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
@@ -165,7 +166,7 @@ function DetailPegawai() {
<Button <Button
color="green" color="green"
onClick={() => router.push(`/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/${data.id}/edit`)} onClick={() => router.push(`/admin/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai/${data.id}/edit`)}
variant="light" variant="light"
radius="md" radius="md"
size="md" size="md"
@@ -187,7 +188,7 @@ function DetailPegawai() {
/> />
{/* Modal NonActive */} {/* Modal NonActive */}
<ModalKonfirmasiHapus <ModalKonfirmasiNonAktif
opened={modalNonActive} opened={modalNonActive}
onClose={() => setModalNonActive(false)} onClose={() => setModalNonActive(false)}
onConfirm={handleNonActive} onConfirm={handleNonActive}

View File

@@ -72,7 +72,7 @@ function CreatePegawaiBumDes() {
// Reset form dan redirect // Reset form dan redirect
resetForm(); resetForm();
toast.success("Data pegawai berhasil ditambahkan"); toast.success("Data pegawai berhasil ditambahkan");
router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai"); router.push("/admin/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai");
} catch (error) { } catch (error) {
console.error("Error creating pegawai:", error); console.error("Error creating pegawai:", error);
toast.error("Terjadi kesalahan saat menambahkan pegawai"); toast.error("Terjadi kesalahan saat menambahkan pegawai");
@@ -82,13 +82,13 @@ function CreatePegawaiBumDes() {
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 0, md: 'xs' }} py="xs">
<Group mb="md"> <Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md"> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
<Title order={4} ml="sm" c="dark"> <Title order={4} ml="sm" c="dark">
Tambah Pegawai BUMDesa Tambah Pegawai BUMDes
</Title> </Title>
</Group> </Group>

View File

@@ -0,0 +1,232 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Badge, Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, ThemeIcon, Title } from '@mantine/core';
import { IconCheck, IconDeviceImacCog, IconPlus, IconSearch, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import stateStrukturBumDes from '../../../_state/ekonomi/struktur-organisasi/struktur-organisasi';
import { useDebouncedValue } from '@mantine/hooks';
function PegawaiBumDes() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Pegawai BUMDes'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListPegawaiBumdes search={search} />
</Box>
);
}
function ListPegawaiBumdes({ search }: { search: string }) {
const stateOrganisasi = useProxy(stateStrukturBumDes.pegawai);
const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const {
data,
page,
totalPages,
loading,
load,
} = stateOrganisasi.findMany;
useEffect(() => {
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
const filteredData = data || [];
// Handle loading state
if (loading || !data) {
return (
<Stack py="md">
<Skeleton height={300} />
</Stack>
);
}
if (data.length === 0) {
return (
<Box py="md">
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={2} visibleFrom="md">Daftar Pegawai BUMDes</Title>
<Title order={3} hiddenFrom="md">Daftar Pegawai BUMDes</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai/create')}
>
Tambah Baru
</Button>
</Group>
<Center py="xl">
<Text c="dimmed" fz={{ base: 'sm', md: 'md' }} ta="center">
Tidak ada data pegawai yang ditemukan
</Text>
</Center>
</Paper>
</Box>
);
}
const sortedData = [...filteredData].sort((a, b) => {
if (a.isActive === b.isActive) {
return a.namaLengkap.localeCompare(b.namaLengkap);
}
return Number(b.isActive) - Number(a.isActive);
});
return (
<Box py="md">
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={2} visibleFrom="md">Daftar Pegawai BUMDes</Title>
<Title order={3} hiddenFrom="md">Daftar Pegawai BUMDes</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai/create')}
>
Tambah Baru
</Button>
</Group>
{/* Desktop: Table */}
<Box visibleFrom="md">
<Table
highlightOnHover
miw={0}
style={{
tableLayout: 'fixed',
width: '100%',
}}
>
<TableThead>
<TableTr>
<TableTh style={{ width: '35%' }}>Nama Lengkap</TableTh>
<TableTh style={{ width: '30%' }}>Posisi</TableTh>
<TableTh style={{ width: '20%' }}>Status</TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{sortedData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Text fw={500} fz="md" lh={1.45} truncate="end">
{item.namaLengkap}
</Text>
</TableTd>
<TableTd>
<Badge variant="light" color="blue" fz="sm" lh={1.4}>
{item.posisi?.nama || 'Belum diatur'}
</Badge>
</TableTd>
<TableTd>
<Badge color={item.isActive ? "green" : "red"} fz="sm" lh={1.4}>
{item.isActive ? "Aktif" : "Tidak Aktif"}
</Badge>
</TableTd>
<TableTd>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() => router.push(`/admin/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai/${item.id}`)}
>
Detail
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
{/* Mobile: Card List */}
<Stack gap="sm" hiddenFrom="md">
{sortedData.map((item) => (
<Paper key={item.id} withBorder p="md" radius="md">
<Stack gap="xs">
<Box>
<Text fz="sm" fw={600} lh={1.4}>Nama Lengkap</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.namaLengkap}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Posisi</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.posisi?.nama || 'Belum diatur'}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Status</Text>
<Group gap="xs">
{item.isActive ? (
<Group gap="xs">
<ThemeIcon color="green" variant="light" size="sm">
<IconCheck size={14} />
</ThemeIcon>
<Text fz="sm" fw={500} c="green">Aktif</Text>
</Group>
) : (
<Group gap="xs">
<ThemeIcon color="red" variant="light" size="sm">
<IconX size={14} />
</ThemeIcon>
<Text fz="sm" fw={500} c="red">Tidak Aktif</Text>
</Group>
)}
</Group>
</Box>
<Box>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() => router.push(`/admin/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai/${item.id}`)}
>
Detail
</Button>
</Box>
</Stack>
</Paper>
))}
</Stack>
<Center mt="lg">
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
}}
total={totalPages}
withEdges
withControls
radius="md"
/>
</Center>
</Paper>
</Box>
);
}
export default PegawaiBumDes;

View File

@@ -95,7 +95,7 @@ function EditPosisiOrganisasiBumDes() {
const success = await stateOrganisasi.edit.update(); const success = await stateOrganisasi.edit.update();
if (success) { if (success) {
router.push('/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi'); router.push('/admin/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/posisi-organisasi');
} }
} catch (err) { } catch (err) {
console.error('Error updating posisi organisasi:', err); console.error('Error updating posisi organisasi:', err);
@@ -106,7 +106,7 @@ function EditPosisiOrganisasiBumDes() {
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 0, md: 'xs' }} py="xs">
<Group mb="md"> <Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md"> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />

View File

@@ -36,7 +36,7 @@ function CreatePosisiOrganisasiBumDes() {
await stateOrganisasi.create.submit(); await stateOrganisasi.create.submit();
toast.success('Posisi organisasi berhasil ditambahkan'); toast.success('Posisi organisasi berhasil ditambahkan');
router.push('/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi'); router.push('/admin/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/posisi-organisasi');
} catch (error) { } catch (error) {
toast.error('Gagal menambahkan posisi organisasi'); toast.error('Gagal menambahkan posisi organisasi');
console.error('Error:', error); console.error('Error:', error);
@@ -46,7 +46,7 @@ function CreatePosisiOrganisasiBumDes() {
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 0, md: 'xs' }} py="xs">
<Group mb="md"> <Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md"> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />

View File

@@ -0,0 +1,315 @@
'use client'
import colors from '@/con/colors';
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 { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import stateStrukturBumDes from '../../../_state/ekonomi/struktur-organisasi/struktur-organisasi';
function PosisiOrganisasiBumDes() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Posisi Organisasi BUMDes'
placeholder='Cari posisi organisasi...'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListPosisiOrganisasiBumDes search={search} />
</Box>
);
}
function ListPosisiOrganisasiBumDes({ search }: { search: string }) {
const stateOrganisasi = useProxy(stateStrukturBumDes.posisiOrganisasi);
const router = useRouter();
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const [debouncedSearch] = useDebouncedValue(search, 1000);
const {
data,
page,
totalPages,
loading,
load,
} = stateOrganisasi.findMany;
useShallowEffect(() => {
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
const handleHapus = async () => {
if (selectedId) {
await stateOrganisasi.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
}
};
const filteredData = data || [];
if (loading || !data) {
return (
<Stack py={{ base: 'sm', md: 'md' }}>
<Skeleton height={600} radius="md" />
</Stack>
);
}
return (
<Box py={{ base: 'sm', md: 'md' }}>
<Paper withBorder bg={colors['white-1']} p={{ base: 'sm', md: 'lg' }} shadow="md" radius="md">
<Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
<Title
order={4}
lh={{ base: 1.2, md: 1.1 }}
>
Daftar Posisi Organisasi BumDes
</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push(
'/admin/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/posisi-organisasi/create'
)
}
>
Tambah Baru
</Button>
</Group>
{/* Desktop Table */}
<Box visibleFrom="md">
<Table
highlightOnHover
miw={0}
style={{
tableLayout: 'fixed',
width: '100%',
}}
>
<TableThead>
<TableTr>
<TableTh style={{ width: '25%' }}>
<Text fz="sm" fw={600} lh={1.4} ta="left">
Nama Posisi
</Text>
</TableTh>
<TableTh style={{ width: '25%' }}>
<Text fz="sm" fw={600} lh={1.4} ta="left">
Deskripsi
</Text>
</TableTh>
<TableTh style={{ width: '20%' }}>
<Text fz="sm" fw={600} lh={1.4} ta="left">
Hierarki
</Text>
</TableTh>
<TableTh style={{ width: '15%' }}>
<Text fz="sm" fw={600} lh={1.4} ta="center">
Edit
</Text>
</TableTh>
<TableTh style={{ width: '15%' }}>
<Text fz="sm" fw={600} lh={1.4} ta="center">
Hapus
</Text>
</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.nama}
</Text>
</TableTd>
<TableTd>
<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>
</TableTd>
<TableTd ta="center">
<Button
variant="light"
color="green"
size="xs"
onClick={() =>
router.push(
`/admin/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/posisi-organisasi/${item.id}`
)
}
>
<IconEdit size={16} />
</Button>
</TableTd>
<TableTd ta="center">
<Button
variant="light"
color="red"
size="xs"
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={16} />
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={5}>
<Center py={{ base: 'sm', md: 'md' }}>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data posisi organisasi yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
{/* Mobile Cards */}
<Box hiddenFrom="md">
<Stack gap="xs">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} withBorder p="sm" radius="md">
<Stack gap={"xs"}>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Nama Posisi
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.nama}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
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}>
Hierarki
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.hierarki || '-'}
</Text>
</Box>
<Group justify="flex-end" gap="xs" mt="xs">
<Button
variant="light"
color="green"
size="xs"
onClick={() =>
router.push(
`/admin/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/posisi-organisasi/${item.id}`
)
}
>
<IconEdit size={16} />
</Button>
<Button
variant="light"
color="red"
size="xs"
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={16} />
</Button>
</Group>
</Stack>
</Paper>
))
) : (
<Center py="sm">
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data posisi organisasi yang cocok
</Text>
</Center>
)}
</Stack>
</Box>
</Paper>
{totalPages > 1 && (
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10, search);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
)}
{/* Modal Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus posisi organisasi BumDes ini?"
/>
</Box>
);
}
export default PosisiOrganisasiBumDes;

View File

@@ -118,7 +118,7 @@ export default function EditDemografiPekerjaan() {
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 0, md: 'xs' }} py="xs">
{/* Header */} {/* Header */}
<Group mb="md"> <Group mb="md">
<Button <Button

View File

@@ -55,7 +55,7 @@ function CreateDemografiPekerjaan() {
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 0, md: 'xs' }} py="xs">
{/* Header */} {/* Header */}
<Group mb="md"> <Group mb="md">
<Button <Button

View File

@@ -21,7 +21,7 @@ import {
Text, Text,
Title Title
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -59,6 +59,8 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
const [chartData, setChartData] = useState<DemografiPekerjaan[]>([]); const [chartData, setChartData] = useState<DemografiPekerjaan[]>([]);
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
const [modalHapus, setModalHapus] = useState(false); const [modalHapus, setModalHapus] = useState(false);
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const [selectedId, setSelectedId] = useState<string | null>(null); const [selectedId, setSelectedId] = useState<string | null>(null);
const { const {
@@ -79,8 +81,8 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
useShallowEffect(() => { useShallowEffect(() => {
setMounted(true); setMounted(true);
load(page, 10, search); load(page, 10, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
useEffect(() => { useEffect(() => {
if (data) { if (data) {
@@ -106,38 +108,52 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
} }
return ( return (
<Box py={10}> <Box py={{ base: 'sm', md: 'md' }}>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
<Group justify="space-between" mb="md"> <Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
<Title order={4}>List Demografi Pekerjaan</Title> <Title
order={4}
lh={{ base: 1.2, md: 1.15 }}
>
List Demografi Pekerjaan
</Title>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
variant="light" variant="light"
onClick={() => router.push('/admin/ekonomi/demografi-pekerjaan/create')} onClick={() => router.push('/admin/ekonomi/demografi-pekerjaan/create')}
fz={{ base: 'sm', md: 'md' }}
> >
Tambah Baru Tambah Baru
</Button> </Button>
</Group> </Group>
<Box style={{ overflowX: 'auto' }}> {/* Desktop Table */}
<Table highlightOnHover> <Box visibleFrom="md">
<Table
highlightOnHover
miw={0}
style={{
tableLayout: 'fixed',
width: '100%',
}}
>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh style={{ minWidth: 200 }}>Pekerjaan</TableTh> <TableTh style={{ width: '40%' }}>Pekerjaan</TableTh>
<TableTh style={{ minWidth: 200 }}>Laki - Laki</TableTh> <TableTh style={{ width: '20%' }}>Laki - Laki</TableTh>
<TableTh style={{ minWidth: 200 }}>Perempuan</TableTh> <TableTh style={{ width: '20%' }}>Perempuan</TableTh>
<TableTh>Edit</TableTh> <TableTh style={{ width: '10%' }}>Edit</TableTh>
<TableTh>Hapus</TableTh> <TableTh style={{ width: '10%' }}>Hapus</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.length > 0 ? ( {filteredData.length > 0 ? (
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd style={{ minWidth: 200 }}>{item.pekerjaan}</TableTd> <TableTd>{item.pekerjaan}</TableTd>
<TableTd style={{ minWidth: 200 }}>{item.lakiLaki}</TableTd> <TableTd>{item.lakiLaki}</TableTd>
<TableTd style={{ minWidth: 200 }}>{item.perempuan}</TableTd> <TableTd>{item.perempuan}</TableTd>
<TableTd> <TableTd>
<Button <Button
variant="light" variant="light"
@@ -145,8 +161,11 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
onClick={() => onClick={() =>
router.push(`/admin/ekonomi/demografi-pekerjaan/${item.id}`) router.push(`/admin/ekonomi/demografi-pekerjaan/${item.id}`)
} }
fz="sm"
px="xs"
py="xs"
> >
<IconEdit size={18} /> <IconEdit size={16} />
</Button> </Button>
</TableTd> </TableTd>
<TableTd> <TableTd>
@@ -158,17 +177,22 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
setSelectedId(item.id); setSelectedId(item.id);
setModalHapus(true); setModalHapus(true);
}} }}
fz="sm"
px="xs"
py="xs"
> >
<IconTrash size={18} /> <IconTrash size={16} />
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
)) ))
) : ( ) : (
<TableTr> <TableTr>
<TableTd colSpan={4}> <TableTd colSpan={5}>
<Center py={20}> <Center py={20}>
<Text color="dimmed">Tidak ada data demografi pekerjaan yang cocok</Text> <Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data demografi pekerjaan yang cocok
</Text>
</Center> </Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -176,6 +200,78 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
{/* Mobile Card */}
<Box hiddenFrom="md">
<Stack gap="xs">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} withBorder p="sm" radius="sm">
<Stack gap={"xs"}>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Pekerjaan
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.pekerjaan}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Laki - Laki
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.lakiLaki}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Perempuan
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.perempuan}
</Text>
</Box>
<Group justify="flex-end" gap="xs">
<Button
variant="light"
color="green"
onClick={() =>
router.push(`/admin/ekonomi/demografi-pekerjaan/${item.id}`)
}
fz="xs"
px="xs"
py="xs"
>
<IconEdit size={14} />
</Button>
<Button
variant="light"
color="red"
disabled={stateDemografi.delete.loading}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
fz="xs"
px="xs"
py="xs"
>
<IconTrash size={14} />
</Button>
</Group>
</Stack>
</Paper>
))
) : (
<Center py={20}>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data demografi pekerjaan yang cocok
</Text>
</Center>
)}
</Stack>
</Box>
</Paper> </Paper>
{/* Pagination */} {/* Pagination */}
@@ -195,10 +291,13 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
</Center> </Center>
{/* Chart */} {/* Chart */}
<Box mt={30} style={{ width: '100%', minHeight: 400 }}> <Box mt={{ base: 'lg', md: 'xl' }}>
<Paper bg={colors['white-1']} p="md" radius="md" withBorder> <Paper bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} radius="md" withBorder>
<Stack gap={"xs"}> <Stack gap="xs">
<Title pb={10} order={4}> <Title
order={4}
lh={{ base: 1.2, md: 1.15 }}
>
Grafik Demografi Pekerjaan Grafik Demografi Pekerjaan
</Title> </Title>
{mounted && chartData.length > 0 ? ( {mounted && chartData.length > 0 ? (
@@ -213,17 +312,23 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
]} ]}
/> />
) : ( ) : (
<Text c="dimmed">Belum ada data untuk ditampilkan dalam grafik</Text> <Text c="dimmed" fz={{ base: 'sm', md: 'md' }} lh={1.4}>
Belum ada data untuk ditampilkan dalam grafik
</Text>
)} )}
<Box py={10}> <Box py={{ base: 'sm', md: 'md' }}>
<Group justify='center'> <Group justify="center" gap='md'>
<Flex align="center" gap={10}> <Flex align="center" gap={8}>
<Box bg="#5082EE" w={20} h={20} /> <Box bg="#5082EE" w={16} h={16} />
<Text>Laki - Laki</Text> <Text fz={{ base: 'xs', md: 'sm' }} lh={1.4}>
Laki - Laki
</Text>
</Flex> </Flex>
<Flex align="center" gap={10}> <Flex align="center" gap={8}>
<Box bg="#6EDF9C" w={20} h={20} /> <Box bg="#6EDF9C" w={16} h={16} />
<Text>Perempuan</Text> <Text fz={{ base: 'xs', md: 'sm' }} lh={1.4}>
Perempuan
</Text>
</Flex> </Flex>
</Group> </Group>
</Box> </Box>
@@ -242,4 +347,4 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
); );
} }
export default DemografiPekerjaan; export default DemografiPekerjaan;

View File

@@ -100,7 +100,7 @@ function EditJumlahPendudukMiskin() {
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 0, md: 'lg' }} py="xs">
<Group mb="md"> <Group mb="md">
<Button <Button
variant="subtle" variant="subtle"

View File

@@ -45,7 +45,7 @@ export default function CreateJumlahPendudukMiskin() {
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 0, md: 'lg' }} py="xs">
{/* Header */} {/* Header */}
<Group mb="md"> <Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md"> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">

View File

@@ -1,4 +1,4 @@
'use client' 'use client';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { import {
Box, Box,
@@ -16,9 +16,9 @@ import {
TableThead, TableThead,
TableTr, TableTr,
Text, Text,
Title Title,
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -26,12 +26,10 @@ import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header'; import HeaderSearch from '../../_com/header';
import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus';
import jumlahPendudukMiskin from '../../_state/ekonomi/jumlah-penduduk-miskin'; import jumlahPendudukMiskin from '../../_state/ekonomi/jumlah-penduduk-miskin';
// ✅ BarChart Mantine
import { BarChart } from '@mantine/charts'; import { BarChart } from '@mantine/charts';
function JumlahPendudukMiskin() { function JumlahPendudukMiskin() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState('');
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
@@ -54,16 +52,15 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) {
const [modalHapus, setModalHapus] = useState(false); const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null); const [selectedId, setSelectedId] = useState<string | null>(null);
const router = useRouter(); const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { data, page, loading, load, totalPages } = stateJPM.findMany; const { data, page, loading, load, totalPages } = stateJPM.findMany;
// Load data awal
useShallowEffect(() => { useShallowEffect(() => {
setMounted(true); setMounted(true);
load(page, 10, search); load(page, 10, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
// Update chart data
useEffect(() => { useEffect(() => {
if (stateJPM.findMany.data) { if (stateJPM.findMany.data) {
setChartData( setChartData(
@@ -88,18 +85,20 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) {
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={{ base: 'sm', md: 'md' }}>
<Skeleton height={500} radius="md" /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
); );
} }
return ( return (
<Box py={10}> <Stack py={{ base: 'sm', md: 'md' }} gap='lg'>
{/* Tabel */} {/* Main Table/Card Section */}
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p={{ base: 'sm', md: 'lg' }} shadow="md" radius="md">
<Group justify="space-between" mb="md"> <Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
<Title order={4}>Daftar Jumlah Penduduk Miskin</Title> <Title order={4} lh={1.2}>
Daftar Jumlah Penduduk Miskin
</Title>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
@@ -112,22 +111,54 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) {
</Button> </Button>
</Group> </Group>
<Box style={{ overflowX: 'auto' }}> {/* Desktop Table */}
<Table highlightOnHover> <Box visibleFrom="md">
<Table
highlightOnHover
miw={0}
style={{
tableLayout: 'fixed',
width: '100%',
}}
>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh style={{ width: '25%' }}>Tahun</TableTh> <TableTh style={{ width: '25%' }}>
<TableTh style={{ width: '35%' }}>Jumlah Penduduk Miskin</TableTh> <Text fz="sm" fw={600} lh={1.4}>
<TableTh style={{ width: '20%' }}>Edit</TableTh> Tahun
<TableTh style={{ width: '20%' }}>Delete</TableTh> </Text>
</TableTh>
<TableTh style={{ width: '35%' }}>
<Text fz="sm" fw={600} lh={1.4}>
Jumlah Penduduk Miskin
</Text>
</TableTh>
<TableTh style={{ width: '20%' }}>
<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> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.length > 0 ? ( {filteredData.length > 0 ? (
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd>{item.year}</TableTd> <TableTd>
<TableTd>{item.totalPoorPopulation}</TableTd> <Text fz="md" fw={500} lh={1.5}>
{item.year}
</Text>
</TableTd>
<TableTd>
<Text fz="md" fw={500} lh={1.5}>
{item.totalPoorPopulation}
</Text>
</TableTd>
<TableTd> <TableTd>
<Button <Button
variant="light" variant="light"
@@ -158,7 +189,9 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) {
<TableTr> <TableTr>
<TableTd colSpan={4}> <TableTd colSpan={4}>
<Center py={20}> <Center py={20}>
<Text color="dimmed">Tidak ada data yang cocok</Text> <Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data yang cocok
</Text>
</Center> </Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -166,6 +199,64 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
{/* Mobile Card View */}
<Stack hiddenFrom="md" gap="xs">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} withBorder p="sm" radius="sm">
<Stack gap={"xs"}>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Tahun
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.year}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Jumlah Penduduk Miskin
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.totalPoorPopulation}
</Text>
</Box>
<Group justify="flex-end" mt="xs">
<Button
variant="light"
color="green"
size="xs"
onClick={() =>
router.push(`/admin/ekonomi/jumlah-penduduk-miskin/${item.id}`)
}
>
<IconEdit size={16} />
</Button>
<Button
variant="light"
color="red"
size="xs"
disabled={stateJPM.delete.loading}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={16} />
</Button>
</Group>
</Stack>
</Paper>
))
) : (
<Center py={20}>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data yang cocok
</Text>
</Center>
)}
</Stack>
</Paper> </Paper>
{/* Pagination */} {/* Pagination */}
@@ -185,9 +276,9 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) {
</Center> </Center>
{/* Bar Chart */} {/* Bar Chart */}
<Paper bg={colors['white-1']} p="md" mt="lg" withBorder radius="md"> <Paper bg={colors['white-1']} p={{ base: 'sm', md: 'md' }} mt="lg" withBorder radius="md">
<Stack> <Stack gap="xs">
<Title order={4} mb="sm"> <Title order={4} lh={1.2} mb="sm">
Grafik Jumlah Penduduk Miskin Grafik Jumlah Penduduk Miskin
</Title> </Title>
{mounted && chartData.length > 0 ? ( {mounted && chartData.length > 0 ? (
@@ -198,14 +289,14 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) {
value: item.totalPoorPopulation, value: item.totalPoorPopulation,
}))} }))}
dataKey="name" dataKey="name"
series={[ series={[{ name: 'value', color: colors['blue-button'] }]}
{ name: 'value', color: colors['blue-button'] },
]}
withTooltip withTooltip
valueFormatter={(v) => `${v.toLocaleString()} jiwa`} valueFormatter={(v) => `${v.toLocaleString()} jiwa`}
/> />
) : ( ) : (
<Text c="dimmed">Belum ada data untuk ditampilkan dalam grafik</Text> <Text c="dimmed" fz={{ base: 'xs', md: 'sm' }} lh={1.4}>
Belum ada data untuk ditampilkan dalam grafik
</Text>
)} )}
</Stack> </Stack>
</Paper> </Paper>
@@ -217,8 +308,8 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) {
onConfirm={handleDelete} onConfirm={handleDelete}
text="Apakah anda yakin ingin menghapus data ini?" text="Apakah anda yakin ingin menghapus data ini?"
/> />
</Box> </Stack>
); );
} }
export default JumlahPendudukMiskin; export default JumlahPendudukMiskin;

View File

@@ -2,6 +2,7 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { import {
Box,
ScrollArea, ScrollArea,
Stack, Stack,
Tabs, Tabs,
@@ -61,36 +62,76 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
radius="lg" radius="lg"
keepMounted={false} keepMounted={false}
> >
<ScrollArea type="auto" offsetScrollbars> <Box visibleFrom='md' pb={10}>
<TabsList <ScrollArea type="auto" offsetScrollbars>
p="sm" <TabsList
style={{ p="sm"
background: "linear-gradient(135deg, #e7ebf7, #f9faff)", style={{
borderRadius: "1rem", background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)", borderRadius: "1rem",
display: "flex", boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
flexWrap: "nowrap", display: "flex",
gap: "0.5rem", flexWrap: "nowrap",
paddingInline: "0.5rem", 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%"
> >
{tabs.map((tab, i) => (
<TabsTab <TabsList
key={i} p="xs" // lebih kecil
value={tab.value} style={{
leftSection={tab.icon} background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
style={{ borderRadius: "1rem",
fontWeight: 600, display: "flex",
fontSize: "0.9rem", flexWrap: "nowrap",
transition: "all 0.2s ease", gap: "0.5rem",
flexShrink: 0, width: "max-content", // ⬅️ kunci
}} maxWidth: "100%", // ⬅️ penting
> }}
{tab.label} >
</TabsTab> {tabs.map((tab, i) => (
))} <TabsTab
</TabsList> key={i}
</ScrollArea> 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) => ( {tabs.map((tab, i) => (
<TabsPanel <TabsPanel

View File

@@ -1,6 +1,28 @@
'use client'
import { usePathname } from "next/navigation";
import LayoutTabs from "./_lib/layoutTabs"; import LayoutTabs from "./_lib/layoutTabs";
import { Box } from "@mantine/core";
export default function Layout({ children }: { children: React.ReactNode }) { export default function Layout({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
// Contoh path:
// - /darmasaba/desa/berita/semua → panjang 5 → list
// - /darmasaba/desa/berita/Pemerintahan → panjang 5 → list
// - /darmasaba/desa/berita/Pemerintahan/123 → panjang 6 → detail
const segments = pathname.split('/').filter(Boolean);
const isDetailPage = segments.length >= 5;
if (isDetailPage) {
// Tampilkan tanpa tab menu
return (
<Box>
{children}
</Box>
);
}
return ( return (
<LayoutTabs> <LayoutTabs>
{children} {children}

View File

@@ -93,7 +93,7 @@ function EditGrafikBerdasarkanPendidikan() {
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 0, md: 'lg' }} py="xs">
<Group mb="md"> <Group mb="md">
<Button <Button
variant="subtle" variant="subtle"

View File

@@ -51,7 +51,7 @@ function CreateGrafikBerdasarkanPendidikan() {
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 0, md: 'lg' }} py="xs">
<Group mb="md"> <Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md"> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />

View File

@@ -7,6 +7,7 @@ import {
Button, Button,
Center, Center,
Flex, Flex,
Group,
Pagination, Pagination,
Paper, Paper,
Skeleton, Skeleton,
@@ -20,7 +21,7 @@ import {
Text, Text,
Title Title
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -51,6 +52,7 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
const [modalHapus, setModalHapus] = useState(false); const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null); const [selectedId, setSelectedId] = useState<string | null>(null);
const router = useRouter(); const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const handleDelete = async () => { const handleDelete = async () => {
if (selectedId) { if (selectedId) {
@@ -64,8 +66,8 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
const { data, page, totalPages, loading, load } = stategrafik.findMany; const { data, page, totalPages, loading, load } = stategrafik.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search); load(page, 10, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
useEffect(() => { useEffect(() => {
if (stategrafik.findMany.data) { if (stategrafik.findMany.data) {
@@ -103,18 +105,20 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={{ base: 'sm', md: 'lg' }}>
<Skeleton height={500} radius="md" /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
); );
} }
return ( return (
<Box py={10}> <Stack py={{ base: 'sm', md: 'lg' }} gap='md'>
{/* Table Data */} {/* Section: List Table */}
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p={{ base: 'sm', md: 'lg' }} shadow="md" radius="md">
<Flex justify="space-between" align="center" mb="md"> <Flex visibleFrom='md' justify="space-between" align="center" mb={{ base: 'sm', md: 'md' }}>
<Title order={4}>List Pengangguran Berdasarkan Pendidikan</Title> <Title order={4} lh={1.2}>
List Pengangguran Berdasarkan Pendidikan
</Title>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
@@ -129,17 +133,43 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
</Button> </Button>
</Flex> </Flex>
<Box style={{ overflowX: 'auto' }}> <Group hiddenFrom='md' align="center" mb={{ base: 'sm', md: 'md' }}>
<Table highlightOnHover> <Title order={4} lh={1.2}>
List Pengangguran Berdasarkan Pendidikan
</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push(
'/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/create',
)
}
>
Tambah Baru
</Button>
</Group>
{/* Desktop Table */}
<Box visibleFrom="md">
<Table
highlightOnHover
miw={0}
style={{
tableLayout: 'fixed',
width: '100%',
}}
>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>SD</TableTh> <TableTh style={{ width: '16%' }}>SD</TableTh>
<TableTh>SMP</TableTh> <TableTh style={{ width: '16%' }}>SMP</TableTh>
<TableTh>SMA</TableTh> <TableTh style={{ width: '16%' }}>SMA</TableTh>
<TableTh>D3</TableTh> <TableTh style={{ width: '16%' }}>D3</TableTh>
<TableTh>S1</TableTh> <TableTh style={{ width: '16%' }}>S1</TableTh>
<TableTh>Edit</TableTh> <TableTh style={{ width: '10%' }}>Edit</TableTh>
<TableTh>Delete</TableTh> <TableTh style={{ width: '10%' }}>Delete</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
@@ -147,7 +177,7 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
<TableTr> <TableTr>
<TableTd colSpan={7}> <TableTd colSpan={7}>
<Center py={20}> <Center py={20}>
<Text color="dimmed"> <Text c="dimmed" fz="sm" lh={1.4}>
Belum ada data grafik responden Belum ada data grafik responden
</Text> </Text>
</Center> </Center>
@@ -156,11 +186,31 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
) : ( ) : (
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd>{item.SD}</TableTd> <TableTd>
<TableTd>{item.SMP}</TableTd> <Text fz="md" fw={500} lh={1.5}>
<TableTd>{item.SMA}</TableTd> {item.SD}
<TableTd>{item.D3}</TableTd> </Text>
<TableTd>{item.S1}</TableTd> </TableTd>
<TableTd>
<Text fz="md" fw={500} lh={1.5}>
{item.SMP}
</Text>
</TableTd>
<TableTd>
<Text fz="md" fw={500} lh={1.5}>
{item.SMA}
</Text>
</TableTd>
<TableTd>
<Text fz="md" fw={500} lh={1.5}>
{item.D3}
</Text>
</TableTd>
<TableTd>
<Text fz="md" fw={500} lh={1.5}>
{item.S1}
</Text>
</TableTd>
<TableTd> <TableTd>
<Button <Button
color="green" color="green"
@@ -193,6 +243,92 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
{/* Mobile Cards */}
<Box hiddenFrom="md">
{filteredData.length === 0 ? (
<Center py="sm">
<Text c="dimmed" fz="sm" lh={1.4}>
Belum ada data grafik responden
</Text>
</Center>
) : (
<Stack gap="sm">
{filteredData.map((item) => (
<Paper key={item.id} withBorder p="sm" radius="md">
<Stack gap={"xs"}>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
SD
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.SD}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
SMP
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.SMP}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
SMA
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.SMA}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
D3
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.D3}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
S1
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.S1}
</Text>
</Box>
<Flex gap="xs" mt="xs">
<Button
size="compact-sm"
color="green"
variant="light"
onClick={() =>
router.push(
`/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/${item.id}`,
)
}
>
<IconEdit size={16} />
</Button>
<Button
size="compact-sm"
color="red"
variant="light"
disabled={stategrafik.delete.loading}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={16} />
</Button>
</Flex>
</Stack>
</Paper>
))}
</Stack>
)}
</Box>
</Paper> </Paper>
{/* Pagination */} {/* Pagination */}
@@ -211,10 +347,10 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
/> />
</Center> </Center>
{/* Donut Chart */} {/* Section: Donut Chart */}
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md" mt="md"> <Paper withBorder bg={colors['white-1']} p={{ base: 'sm', md: 'lg' }} shadow="md" radius="md">
<Stack> <Stack gap="md">
<Title order={3} pb={10}> <Title order={4} lh={1.2}>
Grafik Pengangguran Berdasarkan Pendidikan Grafik Pengangguran Berdasarkan Pendidikan
</Title> </Title>
<Center> <Center>
@@ -228,7 +364,7 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
thickness={40} thickness={40}
/> />
) : ( ) : (
<Text color="dimmed"> <Text c="dimmed" fz={{ base: 'xs', md: 'sm' }} lh={1.4}>
Belum ada data untuk ditampilkan dalam grafik Belum ada data untuk ditampilkan dalam grafik
</Text> </Text>
)} )}
@@ -243,8 +379,8 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
onConfirm={handleDelete} onConfirm={handleDelete}
text="Apakah anda yakin ingin menghapus grafik pengangguran berdasarkan pendidikan ini?" text="Apakah anda yakin ingin menghapus grafik pengangguran berdasarkan pendidikan ini?"
/> />
</Box> </Stack>
); );
} }
export default GrafikBerdasarkanPendidikan; export default GrafikBerdasarkanPendidikan;

View File

@@ -106,7 +106,7 @@ function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() {
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 0, md: 'lg' }} py="xs">
<Group mb="md"> <Group mb="md">
<Button <Button
variant="subtle" variant="subtle"

View File

@@ -49,7 +49,7 @@ function CreateGrafikBerdasarkanUsiaKerjaYangMenganggur() {
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 0, md: 'lg' }} py="xs">
{/* Header */} {/* Header */}
<Group mb="md"> <Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md"> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">

View File

@@ -1,11 +1,12 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
'use client' 'use client';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { import {
Box, Box,
Button, Button,
Center, Center,
Flex, Flex,
Group,
Pagination, Pagination,
Paper, Paper,
Skeleton, Skeleton,
@@ -19,7 +20,7 @@ import {
Text, Text,
Title, Title,
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -51,6 +52,7 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri
const [modalHapus, setModalHapus] = useState(false); const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null); const [selectedId, setSelectedId] = useState<string | null>(null);
const router = useRouter(); const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const handleDelete = async () => { const handleDelete = async () => {
if (selectedId) { if (selectedId) {
@@ -64,8 +66,8 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri
const { data, page, totalPages, loading, load } = stategrafik.findMany; const { data, page, totalPages, loading, load } = stategrafik.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search); load(page, 10, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
useEffect(() => { useEffect(() => {
if (stategrafik.findMany.data) { if (stategrafik.findMany.data) {
@@ -87,19 +89,21 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={{ base: 'sm', md: 'md' }}>
<Skeleton height={500} radius="md" /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
); );
} }
return ( return (
<Box py={10}> <Box py={{ base: 'sm', md: 'md' }}>
{/* Table */} {/* Table - Desktop */}
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md" mb="lg">
<Stack> <Stack gap="md">
<Flex justify="space-between" align="center" mb="md"> <Flex justify="space-between" align="center" visibleFrom='md'>
<Title order={4}>List Pengangguran Berdasarkan Usia Kerja</Title> <Title order={4} lh={1.2}>
List Pengangguran Berdasarkan Usia Kerja
</Title>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
@@ -112,26 +116,58 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri
</Button> </Button>
</Flex> </Flex>
<Box style={{ overflowX: 'auto' }}> <Group align="center" hiddenFrom='md'>
<Table highlightOnHover> <Title order={4} lh={1.2}>
List Pengangguran Berdasarkan Usia Kerja
</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create')
}
>
Tambah Baru
</Button>
</Group>
{/* Desktop Table */}
<Box visibleFrom="md">
<Table
highlightOnHover
miw={0}
style={{
tableLayout: 'fixed',
width: '100%',
}}
>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Usia 18-25</TableTh> <TableTh style={{ width: '20%' }}>Usia 18-25</TableTh>
<TableTh>Usia 26-35</TableTh> <TableTh style={{ width: '20%' }}>Usia 26-35</TableTh>
<TableTh>Usia 36-45</TableTh> <TableTh style={{ width: '20%' }}>Usia 36-45</TableTh>
<TableTh>Usia 46+</TableTh> <TableTh style={{ width: '20%' }}>Usia 46+</TableTh>
<TableTh>Edit</TableTh> <TableTh style={{ width: '10%' }}>Edit</TableTh>
<TableTh>Delete</TableTh> <TableTh style={{ width: '10%' }}>Delete</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.length > 0 ? ( {filteredData.length > 0 ? (
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd>{item.usia18_25}</TableTd> <TableTd fz="md" fw={500} lh={1.5}>
<TableTd>{item.usia26_35}</TableTd> {item.usia18_25}
<TableTd>{item.usia36_45}</TableTd> </TableTd>
<TableTd>{item.usia46_keatas}</TableTd> <TableTd fz="md" fw={500} lh={1.5}>
{item.usia26_35}
</TableTd>
<TableTd fz="md" fw={500} lh={1.5}>
{item.usia36_45}
</TableTd>
<TableTd fz="md" fw={500} lh={1.5}>
{item.usia46_keatas}
</TableTd>
<TableTd> <TableTd>
<Button <Button
color="green" color="green"
@@ -160,7 +196,9 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri
<TableTr> <TableTr>
<TableTd colSpan={6}> <TableTd colSpan={6}>
<Center py={20}> <Center py={20}>
<Text color="dimmed">Belum ada data grafik responden</Text> <Text c="dimmed" fz="sm" lh={1.4}>
Belum ada data grafik responden
</Text>
</Center> </Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -168,6 +206,80 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
{/* Mobile Card View */}
<Box hiddenFrom="md">
{filteredData.length > 0 ? (
<Stack gap="xs">
{filteredData.map((item) => (
<Paper key={item.id} withBorder p="sm" radius="sm">
<Stack gap={"xs"}>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Usia 18-25
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.usia18_25}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Usia 26-35
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.usia26_35}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Usia 36-45
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.usia36_45}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Usia 46+
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.usia46_keatas}
</Text>
</Box>
<Flex gap="xs" mt="xs">
<Button
size="xs"
color="green"
onClick={() =>
router.push(`/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/${item.id}`)
}
>
<IconEdit size={16} />
</Button>
<Button
size="xs"
color="red"
disabled={stategrafik.delete.loading}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={16} />
</Button>
</Flex>
</Stack>
</Paper>
))}
</Stack>
) : (
<Center py={20}>
<Text c="dimmed" fz="sm" lh={1.4}>
Belum ada data grafik responden
</Text>
</Center>
)}
</Box>
</Stack> </Stack>
</Paper> </Paper>
@@ -189,8 +301,8 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri
{/* Donut Chart */} {/* Donut Chart */}
<Paper bg={colors['white-1']} p="md" mt="lg" withBorder radius="md"> <Paper bg={colors['white-1']} p="md" mt="lg" withBorder radius="md">
<Stack> <Stack gap="md">
<Title order={3} pb={10}> <Title order={4} lh={1.2}>
Grafik Pengangguran Berdasarkan Usia Kerja Grafik Pengangguran Berdasarkan Usia Kerja
</Title> </Title>
{donutData.length > 0 ? ( {donutData.length > 0 ? (
@@ -205,7 +317,9 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri
/> />
</Center> </Center>
) : ( ) : (
<Text c="dimmed">Belum ada data untuk ditampilkan dalam grafik</Text> <Text c="dimmed" fz={{ base: 'xs', md: 'sm' }} lh={1.4}>
Belum ada data untuk ditampilkan dalam grafik
</Text>
)} )}
</Stack> </Stack>
</Paper> </Paper>
@@ -221,4 +335,4 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri
); );
} }
export default GrafikBerdasarkanUsiaKerjaYangMenganggur; export default GrafikBerdasarkanUsiaKerjaYangMenganggur;

View File

@@ -176,7 +176,7 @@ function EditDetailDataPengangguran() {
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 0, md: 'xs' }} py="xs">
<Group mb="md"> <Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md"> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />

View File

@@ -40,7 +40,7 @@ function DetailJumlahPengangguran() {
const data = stateDetail.findUnique.data; const data = stateDetail.findUnique.data;
return ( return (
<Box py={10}> <Box px={{ base: 0, md: 'xs' }} py="xs">
{/* Tombol Kembali */} {/* Tombol Kembali */}
<Button <Button
variant="subtle" variant="subtle"
@@ -54,7 +54,7 @@ function DetailJumlahPengangguran() {
{/* Paper Detail */} {/* Paper Detail */}
<Paper <Paper
withBorder withBorder
w={{ base: "100%", md: "60%" }} w={{ base: "100%", md: "70%" }}
bg={colors['white-1']} bg={colors['white-1']}
p="lg" p="lg"
radius="md" radius="md"

View File

@@ -96,7 +96,7 @@ function CreateJumlahPengangguran() {
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 0, md: 'xs' }} py="xs">
{/* Header */} {/* Header */}
<Group mb="md"> <Group mb="md">
<Button <Button

View File

@@ -7,7 +7,7 @@ import {
Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Table, TableTbody, TableTd, TableTh, TableThead, TableTr,
Text, Title Text, Title
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -20,7 +20,7 @@ function DetailDataPengangguran() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
return ( return (
<Box> <Stack>
<HeaderSearch <HeaderSearch
title='Detail Data Pengangguran' title='Detail Data Pengangguran'
placeholder='Cari bulan atau tahun...' placeholder='Cari bulan atau tahun...'
@@ -29,7 +29,7 @@ function DetailDataPengangguran() {
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
/> />
<ListDetailDataPengangguran search={search} /> <ListDetailDataPengangguran search={search} />
</Box> </Stack>
); );
} }
@@ -38,6 +38,7 @@ function ListDetailDataPengangguran({ search }: { search: string }) {
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran); const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran);
const router = useRouter(); const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { const {
data, data,
@@ -49,8 +50,8 @@ function ListDetailDataPengangguran({ search }: { search: string }) {
useShallowEffect(() => { useShallowEffect(() => {
setMounted(true); setMounted(true);
load(page, 10, search); load(page, 10, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
useEffect(() => { useEffect(() => {
if (data) { if (data) {
@@ -68,23 +69,25 @@ function ListDetailDataPengangguran({ search }: { search: string }) {
} }
}, [data]); }, [data]);
const filteredData = data || [] const filteredData = data || [];
// Loading state // Loading state
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py="md"> <Stack py={{ base: 'md', md: 'lg' }} gap="lg">
<Skeleton h={500} radius="md" /> <Skeleton h={500} radius="md" />
</Stack> </Stack>
); );
} }
return ( return (
<Stack py="md" gap="lg"> <Stack py={{ base: 'md', md: 'lg' }} gap="lg">
{/* Table Section */} {/* Table / Card Section */}
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p={{ base: 'sm', md: 'lg' }} shadow="md" radius="md">
<Group justify="space-between" mb="md"> <Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
<Title order={4}>Daftar Detail Data Pengangguran</Title> <Title order={4} lh={1.2}>
Daftar Detail Data Pengangguran
</Title>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
@@ -95,23 +98,45 @@ function ListDetailDataPengangguran({ search }: { search: string }) {
</Button> </Button>
</Group> </Group>
<Box style={{ overflowX: 'auto' }}> {/* Desktop Table */}
<Table highlightOnHover striped withTableBorder withRowBorders> <Box visibleFrom="md">
<Table
highlightOnHover
miw={0}
withTableBorder
withRowBorders
style={{
tableLayout: 'fixed',
width: '100%',
}}
>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh style={{ width: '25%' }}>Bulan</TableTh> <TableTh style={{ width: '30%' }}>Bulan</TableTh>
<TableTh style={{ width: '20%' }}>Terdidik</TableTh> <TableTh style={{ width: '25%' }}>Terdidik</TableTh>
<TableTh style={{ width: '20%' }}>Tidak Terdidik</TableTh> <TableTh style={{ width: '25%' }}>Tidak Terdidik</TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh> <TableTh style={{ width: '20%' }}>Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.length > 0 ? ( {filteredData.length > 0 ? (
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd>{item.month} {item.year}</TableTd> <TableTd>
<TableTd>{item.educatedUnemployment}</TableTd> <Text fz="md" fw={500} lh={1.5}>
<TableTd>{item.uneducatedUnemployment}</TableTd> {item.month} {item.year}
</Text>
</TableTd>
<TableTd>
<Text fz="md" fw={500} lh={1.5}>
{item.educatedUnemployment}
</Text>
</TableTd>
<TableTd>
<Text fz="md" fw={500} lh={1.5}>
{item.uneducatedUnemployment}
</Text>
</TableTd>
<TableTd> <TableTd>
<Button <Button
variant="light" variant="light"
@@ -119,7 +144,9 @@ function ListDetailDataPengangguran({ search }: { search: string }) {
onClick={() => router.push(`/admin/ekonomi/jumlah-pengangguran/${item.id}`)} onClick={() => router.push(`/admin/ekonomi/jumlah-pengangguran/${item.id}`)}
> >
<IconDeviceImac size={20} /> <IconDeviceImac size={20} />
<Text ml={5}>Detail</Text> <Text ml={5} fz="sm" fw={500} lh={1.4}>
Detail
</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -128,7 +155,9 @@ function ListDetailDataPengangguran({ search }: { search: string }) {
<TableTr> <TableTr>
<TableTd colSpan={4}> <TableTd colSpan={4}>
<Center py={20}> <Center py={20}>
<Text c="dimmed">Tidak ada data yang cocok</Text> <Text c="dimmed" fz="sm" fw={500} lh={1.4}>
Tidak ada data yang cocok
</Text>
</Center> </Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -136,25 +165,85 @@ function ListDetailDataPengangguran({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
{/* Mobile Card View */}
<Box hiddenFrom="md">
<Stack gap="xs">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} withBorder p="sm" radius="sm">
<Stack gap={"xs"}>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Bulan
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.month} {item.year}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Terdidik
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.educatedUnemployment}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Tidak Terdidik
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.uneducatedUnemployment}
</Text>
</Box>
<Button
variant="light"
color="blue"
fullWidth
onClick={() => router.push(`/admin/ekonomi/jumlah-pengangguran/${item.id}`)}
mt="xs"
>
<IconDeviceImac size={18} />
<Text ml={5} fz="sm" fw={500} lh={1.4}>
Detail
</Text>
</Button>
</Stack>
</Paper>
))
) : (
<Center py={20}>
<Text c="dimmed" fz="sm" fw={500} lh={1.4}>
Tidak ada data yang cocok
</Text>
</Center>
)}
</Stack>
</Box>
</Paper> </Paper>
<Center>
<Pagination {/* Pagination */}
value={page} {totalPages > 1 && (
onChange={(newPage) => { <Center>
load(newPage, 10); <Pagination
window.scrollTo({ top: 0, behavior: 'smooth' }); value={page}
}} onChange={(newPage) => {
total={totalPages} load(newPage, 10);
mt="md" window.scrollTo({ top: 0, behavior: 'smooth' });
mb="md" }}
color="blue" total={totalPages}
radius="md" mt="md"
/> mb="md"
</Center> color="blue"
radius="md"
/>
</Center>
)}
{/* Chart Section */} {/* Chart Section */}
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p={{ base: 'sm', md: 'lg' }} shadow="md" radius="md">
<Title order={4} mb="md"> <Title order={4} lh={1.2} mb={{ base: 'sm', md: 'md' }}>
Data Pengangguran Terdidik & Tidak Terdidik Data Pengangguran Terdidik & Tidak Terdidik
</Title> </Title>
{mounted && chartData.length > 0 ? ( {mounted && chartData.length > 0 ? (
@@ -170,11 +259,13 @@ function ListDetailDataPengangguran({ search }: { search: string }) {
/> />
</Box> </Box>
) : ( ) : (
<Text c="dimmed">Belum ada data untuk ditampilkan dalam grafik</Text> <Text c="dimmed" fz={{ base: 'sm', md: 'md' }} fw={500} lh={1.5}>
Belum ada data untuk ditampilkan dalam grafik
</Text>
)} )}
</Paper> </Paper>
</Stack> </Stack>
); );
} }
export default DetailDataPengangguran; export default DetailDataPengangguran;

View File

@@ -125,7 +125,7 @@ function EditLowonganKerja() {
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 0, md: 'xs' }} py="xs">
<Group mb="md"> <Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md"> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />

View File

@@ -42,7 +42,7 @@ function DetailLowonganKerjaLokal() {
const data = lowonganState.findUnique.data; const data = lowonganState.findUnique.data;
return ( return (
<Box py={10}> <Box px={{ base: 0, md: 'xs' }} py="xs">
<Button <Button
variant="subtle" variant="subtle"
onClick={() => router.back()} onClick={() => router.back()}
@@ -99,12 +99,16 @@ function DetailLowonganKerjaLokal() {
<Box> <Box>
<Text fz="lg" fw="bold">Deskripsi</Text> <Text fz="lg" fw="bold">Deskripsi</Text>
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }} /> <Box pl={8}>
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }} />
</Box>
</Box> </Box>
<Box> <Box>
<Text fz="lg" fw="bold">Kualifikasi</Text> <Text fz="lg" fw="bold">Kualifikasi</Text>
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.kualifikasi || '-' }} /> <Box pl={8}>
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.kualifikasi || '-' }} />
</Box>
</Box> </Box>
<Group gap="sm" mt="sm"> <Group gap="sm" mt="sm">

View File

@@ -54,7 +54,7 @@ function CreateLowonganKerja() {
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 0, md: 'xs' }} py="xs">
<Group mb="md"> <Group mb="md">
<Button <Button
variant="subtle" variant="subtle"

View File

@@ -18,7 +18,7 @@ import {
Text, Text,
Title Title
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -46,70 +46,87 @@ function LowonganKerjaLokal() {
function ListLowonganKerjaLokal({ search }: { search: string }) { function ListLowonganKerjaLokal({ search }: { search: string }) {
const stateLowongan = useProxy(lowonganKerjaState); const stateLowongan = useProxy(lowonganKerjaState);
const router = useRouter(); const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { data, page, totalPages, loading, load } = stateLowongan.findMany; const { data, page, totalPages, loading, load } = stateLowongan.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search); load(page, 10, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
const filteredData = data || []; const filteredData = data || [];
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py="md">
<Skeleton height={600} radius="md" /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
); );
} }
return ( return (
<Box py={10}> <Box py="md">
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md"> <Group justify="space-between" mb="lg">
<Title order={4}>Daftar Lowongan Kerja Lokal</Title> <Title order={4}>Daftar Lowongan Kerja Lokal</Title>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
variant="light" variant="light"
onClick={() => onClick={() =>
router.push('/admin/ekonomi/lowongan-kerja-lokal/create') router.push('/admin/ekonomi/lowongan-kerja-lokal/create')
} }
> >
Tambah Baru Tambah Baru
</Button> </Button>
</Group> </Group>
<Box style={{ overflowX: 'auto' }}> {/* Desktop Table */}
<Table highlightOnHover> <Box visibleFrom="md">
<Table
highlightOnHover
miw={0}
style={{
tableLayout: 'fixed',
width: '100%',
}}
>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh style={{ width: '25%' }}>Pekerjaan</TableTh> <TableTh style={{ width: '25%' }}>
<TableTh style={{ width: '25%' }}>Nama Perusahaan</TableTh> <Text fz="sm" fw={600} lh={1.2} c="black">Pekerjaan</Text>
<TableTh style={{ width: '20%' }}>Lokasi</TableTh> </TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh> <TableTh style={{ width: '25%' }}>
<Text fz="sm" fw={600} lh={1.2} c="black">Nama Perusahaan</Text>
</TableTh>
<TableTh style={{ width: '20%' }}>
<Text fz="sm" fw={600} lh={1.2} c="black">Lokasi</Text>
</TableTh>
<TableTh style={{ width: '15%' }}>
<Text fz="sm" fw={600} lh={1.2} c="black">Aksi</Text>
</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.length > 0 ? ( {filteredData.length > 0 ? (
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd style={{ width: '25%' }}> <TableTd>
<Text fw={500} truncate="end" lineClamp={1}> <Text fz="md" fw={500} lh={1.5} truncate="end" lineClamp={1}>
{item.posisi} {item.posisi}
</Text> </Text>
</TableTd> </TableTd>
<TableTd style={{ width: '25%' }}> <TableTd>
<Text truncate fz="sm" c="dimmed"> <Text fz="sm" fw={500} lh={1.5} truncate="end">
{item.namaPerusahaan} {item.namaPerusahaan}
</Text> </Text>
</TableTd> </TableTd>
<TableTd style={{ width: '20%' }}> <TableTd>
<Text truncate fz="sm" c="dimmed"> <Text fz="sm" fw={500} lh={1.5} truncate="end">
{item.lokasi} {item.lokasi}
</Text> </Text>
</TableTd> </TableTd>
<TableTd style={{ width: '15%' }}> <TableTd>
<Button <Button
variant="light" variant="light"
color="blue" color="blue"
@@ -118,9 +135,11 @@ function ListLowonganKerjaLokal({ search }: { search: string }) {
`/admin/ekonomi/lowongan-kerja-lokal/${item.id}` `/admin/ekonomi/lowongan-kerja-lokal/${item.id}`
) )
} }
fullWidth
radius="sm"
> >
<IconDeviceImac size={20} /> <IconDeviceImac size={20} />
<Text ml={5}>Detail</Text> <Text ml="xs">Detail</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -128,8 +147,8 @@ function ListLowonganKerjaLokal({ search }: { search: string }) {
) : ( ) : (
<TableTr> <TableTr>
<TableTd colSpan={4}> <TableTd colSpan={4}>
<Center py={20}> <Center py="xl">
<Text color="dimmed"> <Text fz="sm" c="dimmed" lh={1.4}>
Tidak ada data lowongan kerja yang cocok Tidak ada data lowongan kerja yang cocok
</Text> </Text>
</Center> </Center>
@@ -139,6 +158,57 @@ function ListLowonganKerjaLokal({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
{/* Mobile Card List */}
<Stack gap="sm" hiddenFrom="md">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} withBorder p="md" radius="md">
<Stack gap={"xs"}>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Pekerjaan</Text>
<Text fz="sm" fw={500} lh={1.5}>
{item.posisi}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Nama Perusahaan</Text>
<Text fz="sm" fw={500} lh={1.5}>
{item.namaPerusahaan}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Lokasi</Text>
<Text fz="sm" fw={500} lh={1.5}>
{item.lokasi}
</Text>
</Box>
<Button
variant="light"
color="blue"
onClick={() =>
router.push(
`/admin/ekonomi/lowongan-kerja-lokal/${item.id}`
)
}
fullWidth
radius="sm"
mt="xs"
>
<IconDeviceImac size={20} />
<Text ml="xs">Detail</Text>
</Button>
</Stack>
</Paper>
))
) : (
<Center py="xl">
<Text fz="sm" c="dimmed" lh={1.4}>
Tidak ada data lowongan kerja yang cocok
</Text>
</Center>
)}
</Stack>
</Paper> </Paper>
<Center> <Center>
@@ -159,4 +229,4 @@ function ListLowonganKerjaLokal({ search }: { search: string }) {
); );
} }
export default LowonganKerjaLokal; export default LowonganKerjaLokal;

View File

@@ -2,6 +2,7 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { import {
Box,
ScrollArea, ScrollArea,
Stack, Stack,
Tabs, Tabs,
@@ -68,36 +69,76 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
keepMounted={false} keepMounted={false}
> >
{/* ✅ Scroll horizontal wrapper */} {/* ✅ Scroll horizontal wrapper */}
<ScrollArea type="auto" offsetScrollbars> <Box visibleFrom='md' pb={10}>
<TabsList <ScrollArea type="auto" offsetScrollbars>
p="sm" <TabsList
style={{ p="sm"
background: "linear-gradient(135deg, #e7ebf7, #f9faff)", style={{
borderRadius: "1rem", background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)", borderRadius: "1rem",
display: "flex", boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
flexWrap: "nowrap", display: "flex",
gap: "0.5rem", flexWrap: "nowrap",
paddingInline: "0.5rem", gap: "0.5rem",
}} paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
> }}
{tabs.map((tab, i) => ( >
<TabsTab {tabs.map((tab, i) => (
key={i} <TabsTab
value={tab.value} key={i}
leftSection={tab.icon} value={tab.value}
style={{ leftSection={tab.icon}
fontWeight: 600, style={{
fontSize: "0.9rem", fontWeight: 600,
transition: "all 0.2s ease", fontSize: "0.9rem",
flexShrink: 0, transition: "all 0.2s ease",
}} flexShrink: 0, // ✅ jangan mengecil aneh-aneh
> }}
{tab.label} >
</TabsTab> {tab.label}
))} </TabsTab>
</TabsList> ))}
</ScrollArea> </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) => ( {tabs.map((tab, i) => (
<TabsPanel <TabsPanel

View File

@@ -95,7 +95,7 @@ function EditKategoriProduk() {
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 0, md: 'xs' }} py="xs">
{/* Header dengan tombol back */} {/* Header dengan tombol back */}
<Group mb="md"> <Group mb="md">
<Button <Button

View File

@@ -51,7 +51,7 @@ function CreateKategoriProduk() {
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 0, md: 'xs' }} py="xs">
{/* Header dengan tombol kembali */} {/* Header dengan tombol kembali */}
<Group mb="md"> <Group mb="md">
<Button <Button

View File

@@ -1,7 +1,24 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import {
import { useShallowEffect } from '@mantine/hooks'; 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 { IconEdit, IconPlus, IconSearch, IconX } from '@tabler/icons-react'; import { IconEdit, IconPlus, IconSearch, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -10,81 +27,99 @@ import HeaderSearch from '../../../_com/header';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import pasarDesaState from '../../../_state/ekonomi/pasar-desa/pasar-desa'; import pasarDesaState from '../../../_state/ekonomi/pasar-desa/pasar-desa';
function KategoriProduk() { function KategoriProduk() {
const [search2, setSearch2] = useState("") const [search, setSearch] = useState('');
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Kategori Produk' title='Kategori Produk'
placeholder='Cari nama kategori produk...' placeholder='Cari nama kategori produk...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search2} value={search}
onChange={(e) => setSearch2(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
/> />
<ListKategoriProduk search2={search2} /> <ListKategoriProduk search={search} />
</Box> </Box>
); );
} }
function ListKategoriProduk({ search2 }: { search2: string }) { function ListKategoriProduk({ search }: { search: string }) {
const statePasar = useProxy(pasarDesaState.kategoriProduk) const statePasar = useProxy(pasarDesaState.kategoriProduk);
const [modalHapus, setModalHapus] = useState(false) const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null);
const router = useRouter() const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { const { data, page, totalPages, loading, load } = statePasar.findMany;
data,
page,
totalPages,
loading,
load,
} = statePasar.findMany
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search2) load(page, 10, debouncedSearch);
}, [page, search2]) }, [page, debouncedSearch]);
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
statePasar.delete.byId(selectedId) statePasar.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
} }
} };
const filteredData = data || [] const filteredData = data || [];
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py="md">
<Skeleton height={500} radius="md" /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) );
} }
return ( return (
<Box py={10}> <Box py="md">
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md"> <Group justify="space-between" mb="md">
<Title order={4}>Daftar Kategori Produk</Title> <Title order={4} lh={1.2}>
<Button Daftar Kategori Produk
leftSection={<IconPlus size={18} />} </Title>
color="blue" <Button
variant="light" leftSection={<IconPlus size={18} />}
onClick={() => router.push('/admin/ekonomi/pasar-desa/kategori-produk/create')} color="blue"
> variant="light"
Tambah Baru onClick={() =>
</Button> router.push('/admin/ekonomi/pasar-desa/kategori-produk/create')
}
>
Tambah Baru
</Button>
</Group> </Group>
<Box style={{ overflowX: 'auto' }}> {/* Desktop Table */}
<Table highlightOnHover> <Box visibleFrom="md">
<Table
highlightOnHover
miw={0}
style={{
tableLayout: 'fixed',
width: '100%',
}}
>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh style={{ width: '60%' }}>Nama Kategori</TableTh> <TableTh style={{ width: '60%' }}>
<TableTh style={{ width: '20%' }}>Edit</TableTh> <Text fz="sm" fw={600} lh={1.4}>
<TableTh style={{ width: '20%' }}>Delete</TableTh> Nama Kategori
</Text>
</TableTh>
<TableTh style={{ width: '20%' }}>
<Text fz="sm" fw={600} lh={1.4} ta="center">
Edit
</Text>
</TableTh>
<TableTh style={{ width: '20%' }}>
<Text fz="sm" fw={600} lh={1.4} ta="center">
Delete
</Text>
</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
@@ -92,38 +127,48 @@ function ListKategoriProduk({ search2 }: { search2: string }) {
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Text fw={500} truncate="end" lineClamp={1}> <Text fz="md" fw={500} lh={1.5} truncate="end">
{item.nama} {item.nama}
</Text> </Text>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Center>
<Button <Button
color="green" color="green"
variant="light" variant="light"
onClick={() => router.push(`/admin/ekonomi/pasar-desa/kategori-produk/${item.id}`)} onClick={() =>
router.push(
`/admin/ekonomi/pasar-desa/kategori-produk/${item.id}`
)
}
> >
<IconEdit size={18} /> <IconEdit size={18} />
</Button> </Button>
</Center>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Center>
<Button <Button
color="red" color="red"
variant="light" variant="light"
onClick={() => { onClick={() => {
setSelectedId(item.id) setSelectedId(item.id);
setModalHapus(true) setModalHapus(true);
}} }}
> >
<IconX size={18} /> <IconX size={18} />
</Button> </Button>
</TableTd> </Center>
</TableTd>
</TableTr> </TableTr>
)) ))
) : ( ) : (
<TableTr> <TableTr>
<TableTd colSpan={3}> <TableTd colSpan={3}>
<Center py={20}> <Center py="xl">
<Text color="dimmed">Tidak ada data kategori produk yang cocok</Text> <Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data kategori produk yang cocok
</Text>
</Center> </Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -131,14 +176,69 @@ function ListKategoriProduk({ search2 }: { search2: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
{/* Mobile Card */}
<Box hiddenFrom="md">
{filteredData.length > 0 ? (
<Stack gap="sm">
{filteredData.map((item) => (
<Paper
key={item.id}
withBorder
p="md"
radius="md"
bg={colors['white-1']}
>
<Box mb="xs">
<Text fz="sm" fw={600} lh={1.4}>
Nama Kategori
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.nama}
</Text>
</Box>
<Group justify="flex-end" mt="md">
<Button
color="green"
variant="light"
onClick={() =>
router.push(
`/admin/ekonomi/pasar-desa/kategori-produk/${item.id}`
)
}
>
<IconEdit size={18} />
</Button>
<Button
color="red"
variant="light"
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconX size={18} />
</Button>
</Group>
</Paper>
))}
</Stack>
) : (
<Center py="xl">
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data kategori produk yang cocok
</Text>
</Center>
)}
</Box>
</Paper> </Paper>
<Center> <Center>
<Pagination <Pagination
value={page} value={page}
onChange={(newPage) => { onChange={(newPage) => {
load(newPage, 10) load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' }) window.scrollTo({ top: 0, behavior: 'smooth' });
}} }}
total={totalPages} total={totalPages}
mt="md" mt="md"
@@ -156,7 +256,7 @@ function ListKategoriProduk({ search2 }: { search2: string }) {
text='Apakah anda yakin ingin menghapus kategori produk ini?' text='Apakah anda yakin ingin menghapus kategori produk ini?'
/> />
</Box> </Box>
) );
} }
export default KategoriProduk; export default KategoriProduk;

View File

@@ -1,9 +1,29 @@
'use client' 'use client'
import { usePathname } from "next/navigation";
import LayoutTabs from "./_lib/layoutTabs" import LayoutTabs from "./_lib/layoutTabs"
import { Box } from "@mantine/core";
export default function Layout({children} : {children: React.ReactNode}) { export default function Layout({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
// Contoh path:
// - /darmasaba/desa/berita/semua → panjang 5 → list
// - /darmasaba/desa/berita/Pemerintahan → panjang 5 → list
// - /darmasaba/desa/berita/Pemerintahan/123 → panjang 6 → detail
const segments = pathname.split('/').filter(Boolean);
const isDetailPage = segments.length >= 5;
if (isDetailPage) {
// Tampilkan tanpa tab menu
return (
<Box>
{children}
</Box>
);
}
return ( return (
<LayoutTabs> <LayoutTabs>
{children} {children}

View File

@@ -157,7 +157,7 @@ function EditPasarDesa() {
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 0, md: 'xs' }} py="xs">
<Group mb="md"> <Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md"> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />

View File

@@ -40,7 +40,7 @@ function DetailPasarDesa() {
const data = statePasar.pasarDesa.findUnique.data; const data = statePasar.pasarDesa.findUnique.data;
return ( return (
<Box py={10}> <Box px={{ base: 0, md: 'xs' }} py="xs">
<Button <Button
variant="subtle" variant="subtle"
onClick={() => router.back()} onClick={() => router.back()}
@@ -52,7 +52,7 @@ function DetailPasarDesa() {
<Paper <Paper
withBorder withBorder
w={{ base: "100%", md: "60%" }} w={{ base: "100%", md: "70%" }}
bg={colors['white-1']} bg={colors['white-1']}
p="lg" p="lg"
radius="md" radius="md"

View File

@@ -80,7 +80,7 @@ export default function CreatePasarDesa() {
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 0, md: 'xs' }} py="xs">
{/* Header dengan tombol kembali */} {/* Header dengan tombol kembali */}
<Group mb="md"> <Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md"> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">

View File

@@ -18,7 +18,7 @@ import {
Text, Text,
Title Title
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -45,28 +45,29 @@ function PasarDesa() {
function ListPasarDesa({ search }: { search: string }) { function ListPasarDesa({ search }: { search: string }) {
const statePasar = useProxy(pasarDesaState.pasarDesa); const statePasar = useProxy(pasarDesaState.pasarDesa);
const router = useRouter(); const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { data, page, totalPages, loading, load } = statePasar.findMany; const { data, page, totalPages, loading, load } = statePasar.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search); load(page, 10, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
const filteredData = data || []; const filteredData = data || [];
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py="lg">
<Skeleton height={600} radius="md" /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
); );
} }
return ( return (
<Box py={10}> <Box py="lg">
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md"> <Group justify="space-between" mb="md">
<Title order={4}>Daftar Produk Pasar Desa</Title> <Title order={4} lh={1.2}>Daftar Produk Pasar Desa</Title>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
@@ -79,15 +80,23 @@ function ListPasarDesa({ search }: { search: string }) {
</Button> </Button>
</Group> </Group>
<Box style={{ overflowX: 'auto' }}> {/* Desktop Table */}
<Table highlightOnHover> <Box visibleFrom="md">
<Table
highlightOnHover
miw={0}
style={{
tableLayout: 'fixed',
width: '100%',
}}
>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh style={{ width: '25%' }}>Nama Produk</TableTh> <TableTh style={{ width: '25%' }}><Text fz="sm" fw={600} lh={1.4}>Nama Produk</Text></TableTh>
<TableTh style={{ width: '20%' }}>Harga Produk</TableTh> <TableTh style={{ width: '20%' }}><Text fz="sm" fw={600} lh={1.4}>Harga Produk</Text></TableTh>
<TableTh style={{ width: '15%' }}>Rating</TableTh> <TableTh style={{ width: '15%' }}><Text fz="sm" fw={600} lh={1.4}>Rating</Text></TableTh>
<TableTh style={{ width: '25%' }}>Alamat Usaha</TableTh> <TableTh style={{ width: '25%' }}><Text fz="sm" fw={600} lh={1.4}>Alamat Usaha</Text></TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh> <TableTh style={{ width: '15%' }}><Text fz="sm" fw={600} lh={1.4}>Aksi</Text></TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
@@ -95,18 +104,18 @@ function ListPasarDesa({ search }: { search: string }) {
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Text fw={500} truncate="end" lineClamp={1}> <Text fz="md" fw={500} lh={1.5} truncate="end" lineClamp={1}>
{item.nama} {item.nama}
</Text> </Text>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Text>Rp.{item.harga}</Text> <Text fz="md" lh={1.5}>Rp.{item.harga}</Text>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Text>{item.rating || '-'}</Text> <Text fz="md" lh={1.5}>{item.rating || '-'}</Text>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Text truncate fz="sm" c="dimmed"> <Text fz="sm" lh={1.5} c="dimmed">
{item.alamatUsaha || '-'} {item.alamatUsaha || '-'}
</Text> </Text>
</TableTd> </TableTd>
@@ -121,7 +130,7 @@ function ListPasarDesa({ search }: { search: string }) {
} }
> >
<IconDeviceImac size={20} /> <IconDeviceImac size={20} />
<Text ml={5}>Detail</Text> <Text ml={5} fz="sm" fw={500} lh={1.4}>Detail</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -129,8 +138,8 @@ function ListPasarDesa({ search }: { search: string }) {
) : ( ) : (
<TableTr> <TableTr>
<TableTd colSpan={5}> <TableTd colSpan={5}>
<Center py={20}> <Center py={32}>
<Text color="dimmed"> <Text c="dimmed" fz="sm" lh={1.5}>
Tidak ada produk pasar desa yang cocok Tidak ada produk pasar desa yang cocok
</Text> </Text>
</Center> </Center>
@@ -140,6 +149,57 @@ function ListPasarDesa({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
{/* Mobile Cards */}
<Stack hiddenFrom="md" gap="sm">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} withBorder p="md" radius="md">
<Stack gap={'xs'}>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Nama Produk</Text>
<Text fz="sm" fw={500} lh={1.4}>{item.nama}</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Harga Produk</Text>
<Text fz="sm" fw={500} lh={1.4}>Rp.{item.harga}</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Rating</Text>
<Text fz="sm" fw={500} lh={1.4}>{item.rating || '-'}</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Alamat Usaha</Text>
<Text fz="sm" fw={500} lh={1.4} c="dimmed">
{item.alamatUsaha || '-'}
</Text>
</Box>
<Box>
<Button
fullWidth
variant="light"
color="blue"
onClick={() =>
router.push(
`/admin/ekonomi/pasar-desa/produk-pasar-desa/${item.id}`
)
}
>
<IconDeviceImac size={20} />
<Text ml={5} fz="sm" fw={500} lh={1.4}>Detail</Text>
</Button>
</Box>
</Stack>
</Paper>
))
) : (
<Center py={32}>
<Text c="dimmed" fz="sm" lh={1.5}>
Tidak ada produk pasar desa yang cocok
</Text>
</Center>
)}
</Stack>
</Paper> </Paper>
<Center> <Center>
@@ -160,4 +220,4 @@ function ListPasarDesa({ search }: { search: string }) {
); );
} }
export default PasarDesa; export default PasarDesa;

View File

@@ -142,7 +142,7 @@ function EditProgramKemiskinan() {
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 0, md: 'lg' }} py="xs">
{/* Header */} {/* Header */}
<Group mb="md"> <Group mb="md">
<Button <Button

View File

@@ -50,7 +50,7 @@ function DetailProgramKemiskinan() {
const data = programState.findUnique.data; const data = programState.findUnique.data;
return ( return (
<Box py={10}> <Box px={{ base: 0, md: 'lg' }} py="xs">
{/* Tombol Kembali */} {/* Tombol Kembali */}
<Button <Button
variant="subtle" variant="subtle"
@@ -64,7 +64,7 @@ function DetailProgramKemiskinan() {
{/* Card utama */} {/* Card utama */}
<Paper <Paper
withBorder withBorder
w={{ base: "100%", md: "60%" }} w={{ base: "100%", md: "70%" }}
bg={colors['white-1']} bg={colors['white-1']}
p="lg" p="lg"
radius="md" radius="md"

View File

@@ -71,7 +71,7 @@ function CreateProgramKemiskinan() {
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 0, md: 'lg' }} py="xs">
{/* Header dengan tombol back */} {/* Header dengan tombol back */}
<Group mb="md"> <Group mb="md">
<Button <Button

View File

@@ -1,18 +1,43 @@
'use client' 'use client'
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import {
import { useShallowEffect } from '@mantine/hooks'; 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 { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { CartesianGrid, Legend, Line, LineChart, Tooltip as RechartTooltip, XAxis, YAxis } from 'recharts'; import {
CartesianGrid,
Legend,
Line,
LineChart,
Tooltip as RechartTooltip,
XAxis,
YAxis,
} from 'recharts';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header'; import HeaderSearch from '../../_com/header';
import programKemiskinanState from '../../_state/ekonomi/program-kemiskinan'; import programKemiskinanState from '../../_state/ekonomi/program-kemiskinan';
function ProgramKemiskinan() { function ProgramKemiskinan() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState('');
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
@@ -32,21 +57,22 @@ function ListProgramKemiskinan({ search }: { search: string }) {
const router = useRouter(); const router = useRouter();
const [lineChart, setLineChart] = useState<any[]>([]); const [lineChart, setLineChart] = useState<any[]>([]);
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { data, page, totalPages, loading, load } = programState.findMany; const { data, page, totalPages, loading, load } = programState.findMany;
useShallowEffect(() => { useShallowEffect(() => {
setMounted(true); setMounted(true);
load(page, 10, search); load(page, 10, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
useEffect(() => { useEffect(() => {
if (data) { if (data) {
const chartData = data const chartData = data
.filter(item => item.statistik) .filter((item) => item.statistik)
.map(item => ({ .map((item) => ({
tahun: item.statistik?.tahun, tahun: item.statistik?.tahun,
jumlah: Number(item.statistik?.jumlah) jumlah: Number(item.statistik?.jumlah),
})) }))
.sort((a, b) => (a.tahun || 0) - (b.tahun || 0)); .sort((a, b) => (a.tahun || 0) - (b.tahun || 0));
@@ -58,49 +84,90 @@ function ListProgramKemiskinan({ search }: { search: string }) {
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={{ base: 'md', md: 'lg' }}>
<Skeleton height={600} radius="md" /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
); );
} }
return ( return (
<Box py={10}> <Box py={{ base: 'md', md: 'lg' }}>
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md"> {/* Daftar Program Kemiskinan */}
<Group justify="space-between" mb="md"> <Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
<Title order={4}>Daftar Program Kemiskinan</Title> <Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
<Button leftSection={<IconPlus size={18} />} color="blue" variant="light" onClick={() => router.push('/admin/ekonomi/program-kemiskinan/create')}> <Title order={4} lh={1.2}>
Daftar Program Kemiskinan
</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/ekonomi/program-kemiskinan/create')}
>
Tambah Baru Tambah Baru
</Button> </Button>
</Group> </Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover> {/* Desktop Table */}
<Box visibleFrom="md">
<Table
highlightOnHover
miw={0}
style={{
tableLayout: 'fixed',
width: '100%',
}}
>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh style={{ width: '30%' }}>Judul Program</TableTh> <TableTh style={{ width: '30%' }}>
<TableTh style={{ width: '40%' }}>Deskripsi Singkat</TableTh> <Text fz="sm" fw={600} lh={1.4}>
<TableTh style={{ width: '20%' }}>Jumlah Masyarakat Miskin</TableTh> Judul Program
<TableTh style={{ width: '10%' }}>Aksi</TableTh> </Text>
</TableTh>
<TableTh style={{ width: '40%' }}>
<Text fz="sm" fw={600} lh={1.4}>
Deskripsi Singkat
</Text>
</TableTh>
<TableTh style={{ width: '20%' }}>
<Text fz="sm" fw={600} lh={1.4}>
Jumlah Masyarakat Miskin
</Text>
</TableTh>
<TableTh style={{ width: '10%' }}>
<Text fz="sm" fw={600} lh={1.4}>
Aksi
</Text>
</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.length > 0 ? ( {filteredData.length > 0 ? (
filteredData.map(item => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Text fw={500} truncate lineClamp={1}>{item.nama}</Text> <Text fw={500} fz="md" lh={1.45} truncate lineClamp={1}>
{item.nama}
</Text>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Text fz="sm" truncate lineClamp={2} dangerouslySetInnerHTML={{ __html: item.deskripsi }} /> <Text fz="sm" lh={1.45} truncate lineClamp={2} c="dark.9" dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd>
<TableTd>
<Text fz="md" lh={1.45} fw={500}>
{item.statistik?.jumlah || '-'}
</Text>
</TableTd> </TableTd>
<TableTd>{item.statistik?.jumlah || '-'}</TableTd>
<TableTd> <TableTd>
<Button <Button
variant="light" variant="light"
color="blue" color="blue"
onClick={() => router.push(`/admin/ekonomi/program-kemiskinan/${item.id}`)} onClick={() => router.push(`/admin/ekonomi/program-kemiskinan/${item.id}`)}
fz="sm"
lh={1.4}
> >
<IconDeviceImac size={20} /> <IconDeviceImac size={18} />
<Text ml={5}>Detail</Text> <Text ml={5}>Detail</Text>
</Button> </Button>
</TableTd> </TableTd>
@@ -110,7 +177,9 @@ function ListProgramKemiskinan({ search }: { search: string }) {
<TableTr> <TableTr>
<TableTd colSpan={4}> <TableTd colSpan={4}>
<Center py={20}> <Center py={20}>
<Text color="dimmed">Tidak ada data program kemiskinan yang cocok</Text> <Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data program kemiskinan yang cocok
</Text>
</Center> </Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -118,6 +187,61 @@ function ListProgramKemiskinan({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
{/* Mobile Cards */}
<Box hiddenFrom="md">
<Stack gap="md">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} withBorder p="md" radius="sm">
<Stack gap={"xs"}>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Judul Program
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.nama}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Deskripsi Singkat
</Text>
<Text fz="sm" fw={500} lh={1.4} c="dark.9" dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Jumlah Masyarakat Miskin
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.statistik?.jumlah || '-'}
</Text>
</Box>
<Box>
<Button
fullWidth
variant="light"
color="blue"
onClick={() => router.push(`/admin/ekonomi/program-kemiskinan/${item.id}`)}
fz="sm"
lh={1.4}
>
<IconDeviceImac size={18} />
<Text ml={5}>Detail</Text>
</Button>
</Box>
</Stack>
</Paper>
))
) : (
<Center py={20}>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data program kemiskinan yang cocok
</Text>
</Center>
)}
</Stack>
</Box>
</Paper> </Paper>
{/* Pagination */} {/* Pagination */}
@@ -137,25 +261,45 @@ function ListProgramKemiskinan({ search }: { search: string }) {
</Center> </Center>
{/* Chart */} {/* Chart */}
<Box py={10}> <Box pt={{ base: 'md', md: 'lg' }}>
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
<Title pb={10} order={3}>Grafik Berdasarkan Responden</Title> <Title order={4} lh={1.2} pb={{ base: 'sm', md: 'md' }}>
Grafik Berdasarkan Responden
</Title>
{mounted && lineChart.length > 0 ? ( {mounted && lineChart.length > 0 ? (
<Box style={{ width: '100%', overflowX: 'auto' }}> <Box>
<LineChart width={820} height={300} data={lineChart}> <Box
<CartesianGrid strokeDasharray="3 3" /> component="div"
<XAxis dataKey="tahun" /> miw={{ base: 320, md: 820 }}
<YAxis /> mx="auto"
<RechartTooltip style={{ overflowX: 'auto' }}
formatter={(value: any, name: string) => [`${value} orang`, name]} >
labelFormatter={(label: any) => `Tahun: ${label}`} <LineChart
/> width={Math.max(320, lineChart.length * 60)}
<Legend /> height={300}
<Line type="monotone" dataKey="jumlah" name="Jumlah per Tahun" stroke={colors['blue-button']} /> data={lineChart}
</LineChart> >
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="tahun" />
<YAxis />
<RechartTooltip
formatter={(value: any) => [`${value} orang`, 'Jumlah']}
labelFormatter={(label: any) => `Tahun: ${label}`}
/>
<Legend />
<Line
type="monotone"
dataKey="jumlah"
name="Jumlah per Tahun"
stroke={colors['blue-button']}
/>
</LineChart>
</Box>
</Box> </Box>
) : ( ) : (
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text> <Text c="dimmed" fz={{ base: 'xs', md: 'sm' }} lh={1.4}>
Belum ada data untuk ditampilkan dalam grafik
</Text>
)} )}
</Paper> </Paper>
</Box> </Box>
@@ -163,4 +307,4 @@ function ListProgramKemiskinan({ search }: { search: string }) {
); );
} }
export default ProgramKemiskinan; export default ProgramKemiskinan;

View File

@@ -101,7 +101,7 @@ function EditSektorUnggulanDesa() {
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 0, md: 'lg' }} py="xs">
<Group mb="md"> <Group mb="md">
<Button <Button
variant="subtle" variant="subtle"

View File

@@ -48,7 +48,7 @@ function DetailSektorUnggulanDesa() {
const data = stateGrafik.findUnique.data; const data = stateGrafik.findUnique.data;
return ( return (
<Box py={10}> <Box px={{ base: 0, md: 'lg' }} py="xs">
{/* Tombol kembali */} {/* Tombol kembali */}
<Button <Button
variant="subtle" variant="subtle"
@@ -61,7 +61,7 @@ function DetailSektorUnggulanDesa() {
<Paper <Paper
withBorder withBorder
w={{ base: '100%', md: '60%' }} w={{ base: '100%', md: '70%' }}
bg={colors['white-1']} bg={colors['white-1']}
p="lg" p="lg"
radius="md" radius="md"
@@ -81,7 +81,9 @@ function DetailSektorUnggulanDesa() {
<Box> <Box>
<Text fz="lg" fw="bold">Deskripsi</Text> <Text fz="lg" fw="bold">Deskripsi</Text>
<Text ta={"justify"} fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.description || '-' }} style={{ wordBreak: "break-word", whiteSpace: "normal" }} /> <Box pl={8}>
<Text ta={"justify"} fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.description || '-' }} style={{ wordBreak: "break-word", whiteSpace: "normal" }} />
</Box>
</Box> </Box>
<Box> <Box>

View File

@@ -57,7 +57,7 @@ function CreateSektorUnggulanDesa() {
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 0, md: 'lg' }} py="xs">
{/* Header dengan back button */} {/* Header dengan back button */}
<Group mb="md"> <Group mb="md">
<Button <Button

View File

@@ -18,7 +18,7 @@ import {
Text, Text,
Title Title
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -58,6 +58,8 @@ function ListSektorUnggulanDesa({ search }: { search: string }) {
load, load,
} = state.findMany; } = state.findMany;
const [debouncedSearch] = useDebouncedValue(search, 1000);
useEffect(() => { useEffect(() => {
if (state.findMany.data) { if (state.findMany.data) {
setChartData( setChartData(
@@ -72,14 +74,14 @@ function ListSektorUnggulanDesa({ search }: { search: string }) {
}, [state.findMany.data]); }, [state.findMany.data]);
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search) load(page, 10, debouncedSearch)
}, [page, search]) }, [page, debouncedSearch])
const filteredData = data || [] const filteredData = data || [];
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py="md">
<Skeleton height={600} radius="md" /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
); );
@@ -87,69 +89,131 @@ function ListSektorUnggulanDesa({ search }: { search: string }) {
return ( return (
<Stack gap="md" py="md"> <Stack gap="md" py="md">
{/* List Table */} {/* List Section */}
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
<Group justify="space-between" mb="md"> <Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
<Title order={4}>List Sektor Unggulan Desa</Title> <Title order={4} lh={1.2}>
<Button leftSection={<IconPlus size={18} />} color="blue" variant="light" onClick={() => router.push('/admin/ekonomi/sektor-unggulan-desa/create')}> List Sektor Unggulan Desa
</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/ekonomi/sektor-unggulan-desa/create')}
>
Tambah Baru Tambah Baru
</Button> </Button>
</Group> </Group>
{loading ? (
<Skeleton height={300} radius="md" /> {/* Desktop Table */}
) : ( <Box visibleFrom="md">
<Box style={{ overflowX: 'auto' }}> <Table
<Table highlightOnHover> highlightOnHover
<TableThead> miw={0}
<TableTr> style={{
<TableTh style={{ width: '30%' }}>Nama Sektor</TableTh> tableLayout: 'fixed',
<TableTh style={{ width: '40%' }}>Deskripsi</TableTh> width: '100%',
<TableTh style={{ width: '15%' }}>Detail</TableTh> }}
</TableTr> >
</TableThead> <TableThead>
<TableTbody> <TableTr>
{filteredData.length > 0 ? ( <TableTh style={{ width: '35%' }}>Nama Sektor</TableTh>
filteredData.map((item) => ( <TableTh style={{ width: '45%' }}>Deskripsi</TableTh>
<TableTr key={item.id}> <TableTh style={{ width: '20%' }}>Detail</TableTh>
<TableTd> </TableTr>
<Box w={200}> </TableThead>
<Text fw={500} truncate="end" lineClamp={1}> <TableTbody>
{item.name} {filteredData.length > 0 ? (
</Text> filteredData.map((item) => (
</Box> <TableTr key={item.id}>
</TableTd> <TableTd>
<TableTd> <Text fz="md" fw={500} lh={1.45} truncate="end">
<Box w={200}> {item.name}
<Text truncate="end" fz="sm" c="dimmed" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.description || '-' }} /> </Text>
</Box> </TableTd>
</TableTd> <TableTd>
<TableTd> <Text fz="sm" lineClamp={3} fw={500} lh={1.4} c={item.description ? 'inherit' : 'dimmed'} dangerouslySetInnerHTML={{ __html: item.description || '-' }} />
<Button </TableTd>
variant="light" <TableTd>
color="blue" <Button
onClick={() => router.push(`/admin/ekonomi/sektor-unggulan-desa/${item.id}`)} variant="light"
> color="blue"
<IconDeviceImac size={20} /> onClick={() => router.push(`/admin/ekonomi/sektor-unggulan-desa/${item.id}`)}
<Text ml={6}>Detail</Text> radius="md"
</Button> fz="sm"
</TableTd> px="sm"
</TableTr> >
)) <IconDeviceImac size={18} />
) : ( <Text ml={6}>Detail</Text>
<TableTr> </Button>
<TableTd colSpan={3}>
<Center py={20}>
<Text color="dimmed">Tidak ada data sektor unggulan yang cocok</Text>
</Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
)} ))
</TableTbody> ) : (
</Table> <TableTr>
</Box> <TableTd colSpan={3}>
)} <Center py={24}>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data sektor unggulan yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
{/* Mobile Cards */}
<Stack gap="sm" hiddenFrom="md">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} withBorder p="sm" radius="md">
<Stack gap={"xs"}>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Nama Sektor
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.name}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Deskripsi
</Text>
<Box pl={8}>
<Text fz="sm" lineClamp={3} fw={500} lh={1.4} c={item.description ? 'inherit' : 'dimmed'} dangerouslySetInnerHTML={{ __html: item.description || '-' }} />
</Box>
</Box>
<Box>
<Button
variant="light"
color="blue"
onClick={() => router.push(`/admin/ekonomi/sektor-unggulan-desa/${item.id}`)}
fullWidth
mt="xs"
radius="md"
fz="sm"
>
<IconDeviceImac size={18} />
<Text ml={6}>Detail</Text>
</Button>
</Box>
</Stack>
</Paper>
))
) : (
<Center py={24}>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data sektor unggulan yang cocok
</Text>
</Center>
)}
</Stack>
</Paper> </Paper>
{/* Pagination */}
<Center> <Center>
<Pagination <Pagination
value={page} value={page}
@@ -158,22 +222,20 @@ function ListSektorUnggulanDesa({ search }: { search: string }) {
window.scrollTo({ top: 0, behavior: 'smooth' }); window.scrollTo({ top: 0, behavior: 'smooth' });
}} }}
total={totalPages} total={totalPages}
mt="md"
mb="md"
color="blue" color="blue"
radius="md" radius="md"
/> />
</Center> </Center>
{/* Chart */} {/* Chart Section */}
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
<Title order={4} pb="sm"> <Title order={4} lh={1.2} pb={{ base: 'sm', md: 'md' }}>
Grafik Sektor Unggulan Desa Grafik Sektor Unggulan Desa
</Title> </Title>
{loading ? ( {loading ? (
<Skeleton height={350} radius="md" /> <Skeleton height={350} radius="md" />
) : chartData.length > 0 ? ( ) : chartData.length > 0 ? (
<Box style={{ width: '100%', height: 400 }}> <Box style={{ width: '100%', height: 350 }}>
<ResponsiveContainer> <ResponsiveContainer>
<BarChart data={chartData}> <BarChart data={chartData}>
<XAxis dataKey="name" /> <XAxis dataKey="name" />
@@ -186,7 +248,9 @@ function ListSektorUnggulanDesa({ search }: { search: string }) {
</Box> </Box>
) : ( ) : (
<Center py={50}> <Center py={50}>
<Text c="dimmed">Belum ada data untuk ditampilkan dalam grafik</Text> <Text c="dimmed" fz="sm" lh={1.4}>
Belum ada data untuk ditampilkan dalam grafik
</Text>
</Center> </Center>
)} )}
</Paper> </Paper>
@@ -194,4 +258,4 @@ function ListSektorUnggulanDesa({ search }: { search: string }) {
); );
} }
export default SektorUnggulanDesa; export default SektorUnggulanDesa;

View File

@@ -1,131 +0,0 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import {
ScrollArea,
Stack,
Tabs,
TabsList,
TabsPanel,
TabsTab,
Title
} from '@mantine/core';
import {
IconBuildingCommunity,
IconHierarchy,
IconUsers
} 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: "Pegawai",
value: "pegawai",
href: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai",
icon: <IconUsers size={18} stroke={1.8} />
},
{
label: "Posisi Organisasi",
value: "posisiorganisasi",
href: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi",
icon: <IconHierarchy size={18} stroke={1.8} />
},
{
label: "Struktur Organisasi",
value: "strukturorganisasi",
href: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/struktur-organisasi",
icon: <IconBuildingCommunity 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" }}>
Struktur Organisasi & SK Pengurus BUMDesa
</Title>
<Tabs
color={colors["blue-button"]}
variant="pills"
value={activeTab}
onChange={handleTabChange}
radius="lg"
keepMounted={false}
>
{/* ✅ Scroll horizontal biar rapi kalau label panjang */}
<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",
}}
>
{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,
}}
>
{tab.label}
</TabsTab>
))}
</TabsList>
</ScrollArea>
{tabs.map((tab, i) => (
<TabsPanel
key={i}
value={tab.value}
style={{
padding: "1.5rem",
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
borderRadius: "1rem",
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
}}
>
{children}
</TabsPanel>
))}
</Tabs>
</Stack >
);
}
export default LayoutTabs;

View File

@@ -1,12 +0,0 @@
'use client'
import LayoutTabs from "./_lib/layoutTabs"
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<LayoutTabs>
{children}
</LayoutTabs>
)
}

View File

@@ -1,185 +0,0 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Badge, Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, ThemeIcon, Title } from '@mantine/core';
import { IconCheck, IconDeviceImacCog, IconPlus, IconSearch, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import stateStrukturBumDes from '../../../_state/ekonomi/struktur-organisasi/struktur-organisasi';
function PegawaiBumDes() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Pegawai BUMDesa'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListPegawaiBumdes search={search} />
</Box>
);
}
function ListPegawaiBumdes({ search }: { search: string }) {
const stateOrganisasi = useProxy(stateStrukturBumDes.pegawai);
const router = useRouter();
const {
data,
page,
totalPages,
loading,
load,
} = stateOrganisasi.findMany;
useEffect(() => {
load(page, 10, search);
}, [page, search]);
const filteredData = data || []
// Handle loading state
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={300} />
</Stack>
);
}
if (data.length === 0) {
return (
<Box py={10}>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Pegawai BUMDesa</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/create')}
>
Tambah Baru
</Button>
</Group>
<Center py="xl">
<Text c="dimmed">Tidak ada data pegawai yang ditemukan</Text>
</Center>
</Paper>
</Box>
);
}
return (
<Box py={10}>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Pegawai BUMDesa</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/create')}
>
Tambah Baru
</Button>
</Group>
<Box style={{ overflowX: "auto" }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '25%' }}>Nama Lengkap</TableTh>
<TableTh style={{ width: '20%' }}>Posisi</TableTh>
<TableTh style={{ width: '10%' }}>Status</TableTh>
<TableTh style={{ width: '10%' }}>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{(() => {
console.log('Rendering table with items:', stateOrganisasi.findMany.data);
return null;
})()}
{([...filteredData]
.sort((a, b) => {
if (a.isActive === b.isActive) {
return a.namaLengkap.localeCompare(b.namaLengkap); // kalau status sama, urut nama
}
return Number(b.isActive) - Number(a.isActive); // aktif duluan
}) // Aktif di atas
).map((item) => (
<TableTr key={item.id}>
<TableTd>
<Box w={150}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.namaLengkap}
</Text>
</Box>
</TableTd>
<TableTd>
<Box w={150}>
<Badge variant="light" color="blue">
{item.posisi?.nama || 'Belum diatur'}
</Badge>
</Box>
</TableTd>
<TableTd>
<Group gap="xs" wrap="nowrap">
<Box visibleFrom="sm">
<Badge color={item.isActive ? "green" : "red"}>
{item.isActive ? "Aktif" : "Tidak Aktif"}
</Badge>
</Box>
<Box hiddenFrom="sm">
{item.isActive ? (
<ThemeIcon color="green" variant="light" size="sm">
<IconCheck size={16} />
</ThemeIcon>
) : (
<ThemeIcon color="red" variant="light" size="sm">
<IconX size={16} />
</ThemeIcon>
)}
</Box>
</Group>
</TableTd>
<TableTd>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() => router.push(`/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/${item.id}`)}
>
Detail
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
<Center mt="lg">
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
}}
total={totalPages}
withEdges
withControls
radius="md"
/>
</Center>
</Paper>
</Box>
);
}
export default PegawaiBumDes;

View File

@@ -1,169 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import stateStrukturBumDes from '../../../_state/ekonomi/struktur-organisasi/struktur-organisasi';
function PosisiOrganisasiBumDes() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Posisi Organisasi BUMDes'
placeholder='Cari posisi organisasi...'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListPosisiOrganisasiBumDes search={search} />
</Box>
);
}
function ListPosisiOrganisasiBumDes({ search }: { search: string }) {
const stateOrganisasi = useProxy(stateStrukturBumDes.posisiOrganisasi)
const router = useRouter();
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const {
data,
page,
totalPages,
loading,
load,
} = stateOrganisasi.findMany;
useShallowEffect(() => {
load(page, 10, search);
}, [page, search]);
const handleHapus = async () => {
if (selectedId) {
await stateOrganisasi.delete.byId(selectedId);
setModalHapus(false)
setSelectedId(null)
}
}
const filteredData = data || []
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={600} radius="md" />
</Stack>
);
}
return (
<Box py={10}>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Posisi Organisasi BumDes</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/create')}
>
Tambah Baru
</Button>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '20%' }}>Nama Posisi</TableTh>
<TableTh style={{ width: '20%' }}>Deskripsi</TableTh>
<TableTh style={{ width: '20%' }}>Hierarki</TableTh>
<TableTh style={{ width: '20%' }}>Edit</TableTh>
<TableTh style={{ width: '20%' }}>Hapus</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd style={{ width: '20%' }}>
<Text fw={500} truncate="end" lineClamp={1}>{item.nama}</Text>
</TableTd>
<TableTd style={{ width: '20%' }}>
<Box w={200}>
<Text lineClamp={1} fz="sm" c="dimmed" dangerouslySetInnerHTML={{ __html: item.deskripsi || '-' }} />
</Box>
</TableTd>
<TableTd style={{ width: '20%' }}>
<Text>{item.hierarki || '-'}</Text>
</TableTd>
<TableTd style={{ width: '20%' }}>
<Button
variant="light"
color="green"
size="sm"
onClick={() => router.push(`/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/${item.id}`)}
>
<IconEdit size={18} />
</Button>
</TableTd>
<TableTd style={{ width: '20%' }}>
<Button
variant="light"
color="red"
size="sm"
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={18} />
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text color="dimmed">Tidak ada data posisi organisasi yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10, search);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
{/* Modal Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus posisi organisasi BumDes ini?"
/>
</Box>
);
}
export default PosisiOrganisasiBumDes;

View File

@@ -40,7 +40,7 @@ function DetailAjukanIdeInofativDesa() {
const data = state.findUnique.data; const data = state.findUnique.data;
return ( return (
<Box py={10}> <Box px={{ base: 0, md: 'lg' }} py="xs">
{/* Tombol Kembali */} {/* Tombol Kembali */}
<Button <Button
variant="subtle" variant="subtle"
@@ -54,7 +54,7 @@ function DetailAjukanIdeInofativDesa() {
{/* Card Utama */} {/* Card Utama */}
<Paper <Paper
withBorder withBorder
w={{ base: "100%", md: "80%", lg: "60%" }} w={{ base: "100%", md: "70%" }}
bg={colors['white-1']} bg={colors['white-1']}
p="lg" p="lg"
radius="md" radius="md"

View File

@@ -17,7 +17,7 @@ import {
Text, Text,
Title Title
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -44,6 +44,7 @@ function AjukanIdeInovatif() {
function ListAjukanIdeInovatif({ search }: { search: string }) { function ListAjukanIdeInovatif({ search }: { search: string }) {
const state = useProxy(ajukanIdeInovatifState) const state = useProxy(ajukanIdeInovatifState)
const router = useRouter() const router = useRouter()
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const { const {
data, data,
@@ -54,8 +55,8 @@ function ListAjukanIdeInovatif({ search }: { search: string }) {
} = state.findMany; } = state.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search) load(page, 10, debouncedSearch)
}, [page, search]) }, [page, debouncedSearch])
const filteredData = data || [] const filteredData = data || []

View File

@@ -111,7 +111,7 @@ function EditDigitalSmartVillage() {
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 0, md: 'lg' }} py="xs">
{/* Header */} {/* Header */}
<Group mb="md"> <Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md"> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">

View File

@@ -49,7 +49,7 @@ function DetailDesaDigital() {
const data = stateDesaDigital.findUnique.data; const data = stateDesaDigital.findUnique.data;
return ( return (
<Box py={10}> <Box px={{ base: 0, md: 'lg' }} py="xs">
{/* Tombol Kembali */} {/* Tombol Kembali */}
<Button <Button
variant="subtle" variant="subtle"
@@ -63,7 +63,7 @@ function DetailDesaDigital() {
{/* Card Utama */} {/* Card Utama */}
<Paper <Paper
withBorder withBorder
w={{ base: "100%", md: "60%" }} w={{ base: "100%", md: "70%" }}
bg={colors['white-1']} bg={colors['white-1']}
p="lg" p="lg"
radius="md" radius="md"
@@ -77,12 +77,12 @@ function DetailDesaDigital() {
{/* Sub Card Detail */} {/* Sub Card Detail */}
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs"> <Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm"> <Stack gap="sm">
<Box> <Box pl={5}>
<Text fz="lg" fw="bold">Judul</Text> <Text fz="lg" fw="bold">Judul</Text>
<Text fz="md" c="dimmed">{data?.name || '-'}</Text> <Text fz="md" c="dimmed">{data?.name || '-'}</Text>
</Box> </Box>
<Box> <Box pl={5}>
<Text fz="lg" fw="bold">Deskripsi</Text> <Text fz="lg" fw="bold">Deskripsi</Text>
<Text <Text
fz="md" fz="md"

View File

@@ -73,7 +73,7 @@ export default function CreateDesaDigital() {
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 0, md: 'lg' }} py="xs">
{/* Header dengan tombol kembali */} {/* Header dengan tombol kembali */}
<Group mb="md" align="center"> <Group mb="md" align="center">
<Button <Button

View File

@@ -18,7 +18,7 @@ import {
Text, Text,
Title Title
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -45,28 +45,31 @@ function DesaDigitalSmartVillage() {
function ListDesaDigitalSmartVillage({ search }: { search: string }) { function ListDesaDigitalSmartVillage({ search }: { search: string }) {
const state = useProxy(desaDigitalState); const state = useProxy(desaDigitalState);
const router = useRouter(); const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { data, page, totalPages, loading, load } = state.findMany; const { data, page, totalPages, loading, load } = state.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search); load(page, 10, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
const filteredData = data || []; const filteredData = data || [];
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py="lg">
<Skeleton height={600} radius="md" /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
); );
} }
return ( return (
<Box py={10}> <Box py="lg">
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md"> <Group justify="space-between" mb="md">
<Title order={4}>List Desa Digital Smart Village</Title> <Title order={4} lh={1.2}>
List Desa Digital Smart Village
</Title>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
@@ -78,15 +81,34 @@ function ListDesaDigitalSmartVillage({ search }: { search: string }) {
Tambah Baru Tambah Baru
</Button> </Button>
</Group> </Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover> {/* Desktop Table */}
<Box visibleFrom="sm">
<Table
highlightOnHover
miw={0}
style={{
tableLayout: 'fixed',
width: '100%',
}}
>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh style={{ width: '30%' }}>Nama Inovasi</TableTh> <TableTh style={{ width: '30%' }}>
<TableTh style={{ width: '50%' }}> <Text fz="sm" fw={600} lh={1.4}>
Deskripsi Singkat Inovasi Nama Inovasi
</Text>
</TableTh>
<TableTh style={{ width: '50%' }}>
<Text fz="sm" fw={600} lh={1.4}>
Deskripsi Singkat Inovasi
</Text>
</TableTh>
<TableTh style={{ width: '20%' }}>
<Text fz="sm" fw={600} lh={1.4}>
Aksi
</Text>
</TableTh> </TableTh>
<TableTh style={{ width: '20%' }}>Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
@@ -94,21 +116,18 @@ function ListDesaDigitalSmartVillage({ search }: { search: string }) {
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Box w={200}> <Text fz="md" fw={500} lh={1.5} truncate="end">
<Text fw={500} truncate="end" lineClamp={1}> {item.name}
{item.name} </Text>
</Text>
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={200}> <Text
<Text fz="sm"
fz="sm" c="dimmed"
c="dimmed" lh={1.5}
lineClamp={1} lineClamp={1}
dangerouslySetInnerHTML={{ __html: item.deskripsi }} dangerouslySetInnerHTML={{ __html: item.deskripsi }}
/> />
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button <Button
@@ -121,7 +140,9 @@ function ListDesaDigitalSmartVillage({ search }: { search: string }) {
} }
> >
<IconDeviceImac size={20} /> <IconDeviceImac size={20} />
<Text ml={5}>Detail</Text> <Text ml={5} fz="sm" fw={500} lh={1.4}>
Detail
</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -129,8 +150,8 @@ function ListDesaDigitalSmartVillage({ search }: { search: string }) {
) : ( ) : (
<TableTr> <TableTr>
<TableTd colSpan={3}> <TableTd colSpan={3}>
<Center py={20}> <Center py="xl">
<Text color="dimmed"> <Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data inovasi digital yang cocok Tidak ada data inovasi digital yang cocok
</Text> </Text>
</Center> </Center>
@@ -140,6 +161,64 @@ function ListDesaDigitalSmartVillage({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
{/* Mobile Cards */}
<Box hiddenFrom="sm">
<Stack gap="md">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} withBorder p="md" radius="md">
<Stack gap={4}>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Nama Inovasi
</Text>
<Text fz="sm" fw={500} lh={1.5}>
{item.name}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Deskripsi Singkat Inovasi
</Text>
<Box pl={5}>
<Text
fz="sm"
fw={500}
lh={1.5}
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
/>
</Box>
</Box>
<Box>
<Button
variant="light"
color="blue"
fullWidth
onClick={() =>
router.push(
`/admin/inovasi/desa-digital-smart-village/${item.id}`
)
}
>
<IconDeviceImac size={20} />
<Text ml={5} fz="sm" fw={500} lh={1.4}>
Detail
</Text>
</Button>
</Box>
</Stack>
</Paper>
))
) : (
<Center py="xl">
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data inovasi digital yang cocok
</Text>
</Center>
)}
</Stack>
</Box>
</Paper> </Paper>
<Center> <Center>
<Pagination <Pagination

View File

@@ -123,7 +123,7 @@ function EditInfoTeknologiTepatGuna() {
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 0, md: 'lg' }} py="xs">
{/* Tombol back + title */} {/* Tombol back + title */}
<Group mb="md"> <Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md"> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">

View File

@@ -40,7 +40,7 @@ function DetailInfoTeknologiTepatGuna() {
const data = stateInfoTekno.findUnique.data const data = stateInfoTekno.findUnique.data
return ( return (
<Box py={10}> <Box px={{ base: 0, md: 'lg' }} py="xs">
{/* Tombol Kembali */} {/* Tombol Kembali */}
<Button <Button
variant="subtle" variant="subtle"
@@ -54,7 +54,7 @@ function DetailInfoTeknologiTepatGuna() {
{/* Card Utama */} {/* Card Utama */}
<Paper <Paper
withBorder withBorder
w={{ base: "100%", md: "50%" }} w={{ base: "100%", md: "70%" }}
bg={colors['white-1']} bg={colors['white-1']}
p="lg" p="lg"
radius="md" radius="md"

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