diff --git a/src/app/admin/(dashboard)/landing-page/SDGs/[id]/edit/page.tsx b/src/app/admin/(dashboard)/landing-page/SDGs/[id]/edit/page.tsx index c1adc0ac..582a044d 100644 --- a/src/app/admin/(dashboard)/landing-page/SDGs/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/SDGs/[id]/edit/page.tsx @@ -123,7 +123,7 @@ export default function EditKolaborasiInovasi() { }; return ( - + - + + {/* Desktop Table */} + + + + + Daftar APBDes + + + - - - - - APBDes - Tahun - Dokumen - Aksi - - - - {filteredData.length > 0 ? ( - filteredData.map((item) => ( - - - - APBDes {item.tahun} - - - - {item.tahun || '-'} - - - {item.file?.link ? ( - - ) : ( - - Tidak ada dokumen + +
+ + + + APBDes + + + Tahun + + + Dokumen + + + Aksi + + + + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + APBDes {item.tahun} - )} - - - + + + + {item.tahun || '-'} + + + + {item.file?.link ? ( + + ) : ( + + Tidak ada dokumen + + )} + + - + + + )) + ) : ( + + +
+ + Tidak ada data APBDes yang cocok + +
- )) - ) : ( - - -
- Tidak ada data APBDes yang cocok -
-
-
- )} -
-
-
-
+ )} + + +
+ +
-
+ {/* Mobile Cards */} + + + + + Daftar APBDes + + + + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + APBDes {item.tahun} + + + + Tahun + + + {item.tahun || '-'} + + + + + + Dokumen + + {item.file?.link ? ( + + ) : ( + + Tidak ada + + )} + + + + + + )) + ) : ( + +
+ + Tidak ada data APBDes yang cocok + +
+
+ )} +
+
+
+ +
{ diff --git a/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/_lib/layouTabs.tsx b/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/_lib/layouTabs.tsx index d327fb3b..fd7a715f 100644 --- a/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/_lib/layouTabs.tsx +++ b/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/_lib/layouTabs.tsx @@ -69,7 +69,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) { keepMounted={false} > {/* ✅ Scroll horizontal wrapper */} - + - + + {tabs.map((tab, i) => ( @@ -74,6 +79,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) { fontWeight: 600, fontSize: "0.9rem", transition: "all 0.2s ease", + flexShrink: 0, // ✅ jangan mengecil aneh-aneh }} > {tab.label} diff --git a/src/app/admin/(dashboard)/landing-page/prestasi-desa/kategori-prestasi-desa/[id]/page.tsx b/src/app/admin/(dashboard)/landing-page/prestasi-desa/kategori-prestasi-desa/[id]/page.tsx index 0cb9cbbe..b0128513 100644 --- a/src/app/admin/(dashboard)/landing-page/prestasi-desa/kategori-prestasi-desa/[id]/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/prestasi-desa/kategori-prestasi-desa/[id]/page.tsx @@ -78,7 +78,7 @@ function EditKategoriPrestasi() { }; return ( - + + + List Kategori Prestasi + - - + - Nama Kategori - Edit - Delete + Nama Kategori + Edit + Delete {filteredData.length === 0 ? ( - - + + {search ? 'Tidak ada hasil yang cocok' : 'Belum ada data kategori'} @@ -95,68 +100,130 @@ function ListKategoriPrestasi({ search }: { search: string }) { filteredData.map((item) => ( - - {item.name} - + + {item.name} + - - + + - - + + )) )}
+ + {totalPages > 1 && ( +
+ load(newPage)} + total={totalPages} + withEdges + size="sm" + styles={{ + control: { + '&[data-active]': { + background: `${colors['blue-button']} !important`, + }, + }, + }} + /> +
+ )}
- {totalPages > 1 && ( -
- load(newPage)} - total={totalPages} - withEdges - size="sm" - styles={{ - control: { - '&[data-active]': { - background: `${colors['blue-button']} !important`, - }, - }, - }} - /> -
- )} + {/* MOBILE: Card */} + + + {filteredData.length === 0 ? ( + + + {search ? 'Tidak ada hasil yang cocok' : 'Belum ada data kategori'} + + + ) : ( + filteredData.map((item) => ( + + + {item.name} + + + + + + + )) + )} + + {totalPages > 1 && ( +
+ load(newPage)} + total={totalPages} + withEdges + size="xs" + styles={{ + control: { + '&[data-active]': { + background: `${colors['blue-button']} !important`, + }, + }, + }} + /> +
+ )} +
+
+ + {/* Modal Konfirmasi Hapus */} + setModalHapus(false)} + onConfirm={handleHapus} + text='Apakah anda yakin ingin menghapus kategori prestasi ini?' + /> - {/* Modal Konfirmasi Hapus */} - setModalHapus(false)} - onConfirm={handleHapus} - text='Apakah anda yakin ingin menghapus kategori prestasi ini?' - /> -
+
); } -export default KategoriPrestasiDesa +export default KategoriPrestasiDesa \ No newline at end of file diff --git a/src/app/admin/(dashboard)/landing-page/prestasi-desa/list-prestasi-desa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/landing-page/prestasi-desa/list-prestasi-desa/[id]/edit/page.tsx index 085d347a..402ede7e 100644 --- a/src/app/admin/(dashboard)/landing-page/prestasi-desa/list-prestasi-desa/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/prestasi-desa/list-prestasi-desa/[id]/edit/page.tsx @@ -128,7 +128,7 @@ export default function EditPrestasiDesa() { }; return ( - + - - + + {/* Desktop Table */} + +
- Nama Prestasi - Deskripsi - Kategori - Aksi + Nama Prestasi + Deskripsi + Kategori + Aksi {filteredData.length > 0 ? ( filteredData.map((item) => ( - - - {item.name} - + + + {item.name} + - - + + - - - {item.kategori?.name || 'Tidak ada kategori'} - + + + {item.kategori?.name || 'Tidak ada kategori'} + - +
+ + {/* Mobile Cards */} + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + {item.name} + + + + Kategori: {item.kategori?.name || 'Tidak ada kategori'} + + + + + + + )) + ) : ( +
+ + Tidak ada data prestasi + +
+ )} +
+ {totalPages > 1 && ( -
+
)} @@ -132,4 +178,4 @@ function ListPrestasi({ search }: { search: string }) { ) } -export default ListPrestasiDesa; +export default ListPrestasiDesa; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/landing-page/profil/_lib/layoutTabs.tsx b/src/app/admin/(dashboard)/landing-page/profil/_lib/layoutTabs.tsx index 7210d917..43419da7 100644 --- a/src/app/admin/(dashboard)/landing-page/profil/_lib/layoutTabs.tsx +++ b/src/app/admin/(dashboard)/landing-page/profil/_lib/layoutTabs.tsx @@ -75,7 +75,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) { keepMounted={false} > {/* ✅ Scroll horizontal wrapper */} - + - + { try { const res = await fetch('/api/auth/me', { - credentials: 'include' // ✅ ADD credentials + credentials: 'include' }); const data = await res.json(); if (data.user) { - // ✅ Check if user is NOT active → redirect to waiting room if (!data.user.isActive) { authStore.setUser(null); router.replace('/waiting-room'); return; } - // ✅ Fetch menuIds const menuRes = await fetch(`/api/admin/user-menu-access?userId=${data.user.id}`, { - credentials: 'include' // ✅ ADD credentials + credentials: 'include' }); const menuData = await menuRes.json(); @@ -67,7 +65,6 @@ export default function Layout({ children }: { children: React.ReactNode }) { ? [...menuData.menuIds] : null; - // ✅ Set user dengan menuIds yang fresh authStore.setUser({ id: data.user.id, name: data.user.name, @@ -76,7 +73,6 @@ export default function Layout({ children }: { children: React.ReactNode }) { isActive: data.user.isActive }); - // ✅ IMPROVED: Redirect ONLY if di root /admin const currentPath = window.location.pathname; if (currentPath === '/admin') { @@ -84,7 +80,6 @@ export default function Layout({ children }: { children: React.ReactNode }) { console.log('🔄 Redirecting from /admin to:', expectedPath); router.replace(expectedPath); } - // ✅ Jangan redirect jika user sudah di path yang valid } else { authStore.setUser(null); @@ -100,17 +95,17 @@ export default function Layout({ children }: { children: React.ReactNode }) { }; fetchUser(); - }, [router]); // ✅ Only depend on router + }, [router]); const getRedirectPath = (roleId: number): string => { switch (roleId) { - case 0: // DEVELOPER - case 1: // SUPERADMIN - case 2: // ADMIN_DESA + case 0: + case 1: + case 2: return '/admin/landing-page/profil/program-inovasi'; - case 3: // ADMIN_KESEHATAN + case 3: return '/admin/kesehatan/posyandu'; - case 4: // ADMIN_PENDIDIKAN + case 4: return '/admin/pendidikan/info-sekolah/jenjang-pendidikan'; default: return '/admin'; @@ -139,7 +134,7 @@ export default function Layout({ children }: { children: React.ReactNode }) { const response = await fetch('/api/auth/logout', { method: 'POST', - credentials: 'include' // ✅ ADD credentials + credentials: 'include' }); const result = await response.json(); @@ -163,6 +158,12 @@ export default function Layout({ children }: { children: React.ReactNode }) { } }; + // ✅ Handler untuk menutup mobile menu saat navigasi + const handleNavClick = (path: string) => { + router.push(path); + close(); // Tutup mobile menu + }; + return ( - {/* ... rest of your JSX (Header, Navbar, Main) sama seperti sebelumnya ... */} - {/* ... Navbar content sama seperti sebelumnya ... */} {currentNav.map((v, k) => { const isParentActive = segments.includes(_.lowerCase(v.name)); return ( - {v.name}} style={{ borderRadius: rem(10), marginBottom: rem(4), transition: "background 150ms ease" }} styles={{ root: { '&:hover': { backgroundColor: 'rgba(25, 113, 194, 0.05)' } } }} variant="light" active={isParentActive}> + {v.name}} + style={{ borderRadius: rem(10), marginBottom: rem(4), transition: "background 150ms ease" }} + styles={{ root: { '&:hover': { backgroundColor: 'rgba(25, 113, 194, 0.05)' } } }} + variant="light" + active={isParentActive} + > {v.children.map((child, key) => { const isChildActive = segments.includes(_.lowerCase(child.name)); return ( - {child.name}} styles={{ root: { borderRadius: rem(8), marginBottom: rem(2), transition: 'background 150ms ease', padding: '6px 12px', '&:hover': { backgroundColor: isChildActive ? 'rgba(25, 113, 194, 0.15)' : 'rgba(25, 113, 194, 0.05)' }, ...(isChildActive && { backgroundColor: 'rgba(25, 113, 194, 0.1)' }) } }} active={isChildActive} component={Link} /> + { + e.preventDefault(); + handleNavClick(child.path); + }} + href={child.path} + c={isChildActive ? colors["blue-button"] : "gray"} + label={{child.name}} + styles={{ + root: { + borderRadius: rem(8), + marginBottom: rem(2), + transition: 'background 150ms ease', + padding: '6px 12px', + '&:hover': { + backgroundColor: isChildActive ? 'rgba(25, 113, 194, 0.15)' : 'rgba(25, 113, 194, 0.05)' + }, + ...(isChildActive && { backgroundColor: 'rgba(25, 113, 194, 0.1)' }) + } + }} + active={isChildActive} + component={Link} + /> ); })} diff --git a/src/app/api/[[...slugs]]/_lib/ekonomi/jumlah-penduduk-miskin/findMany.ts b/src/app/api/[[...slugs]]/_lib/ekonomi/jumlah-penduduk-miskin/findMany.ts index 4cc14207..7bcfc08c 100644 --- a/src/app/api/[[...slugs]]/_lib/ekonomi/jumlah-penduduk-miskin/findMany.ts +++ b/src/app/api/[[...slugs]]/_lib/ekonomi/jumlah-penduduk-miskin/findMany.ts @@ -28,7 +28,7 @@ export default async function grafikJumlahPendudukMiskinFindMany( where, skip, take: limit, - orderBy: { createdAt: "desc" }, + orderBy: { year: "asc" }, }), prisma.grafikJumlahPendudukMiskin.count({ where, diff --git a/src/app/darmasaba/(pages)/ekonomi/demografi-pekerjaan/page.tsx b/src/app/darmasaba/(pages)/ekonomi/demografi-pekerjaan/page.tsx index 6f88b299..e665b354 100644 --- a/src/app/darmasaba/(pages)/ekonomi/demografi-pekerjaan/page.tsx +++ b/src/app/darmasaba/(pages)/ekonomi/demografi-pekerjaan/page.tsx @@ -1,6 +1,6 @@ 'use client' import colors from '@/con/colors'; -import { Stack, Box, Paper, Text, ColorSwatch, Flex, Skeleton } from '@mantine/core'; +import { Stack, Box, Paper, Text, ColorSwatch, Flex, Skeleton, Title } from '@mantine/core'; import React from 'react'; import BackButton from '../../desa/layanan/_com/BackButto'; import { BarChart } from '@mantine/charts'; @@ -32,23 +32,47 @@ function Page() { - - + + Demografi Pekerjaan + + + Desa Darmasaba memiliki komposisi penduduk yang beragam dalam sektor pekerjaan - Desa Darmasaba memiliki komposisi penduduk yang beragam dalam sektor pekerjaan - - Statistik Demografi Pekerjaan Di Desa Darmasaba + + + Statistik Demografi Pekerjaan Di Desa Darmasaba + ({ id: item.id, Pekerjaan: item.pekerjaan, @@ -62,28 +86,45 @@ function Page() { ]} tickLine="y" xAxisProps={{ - angle: -45, // Rotate labels by -45 degrees - textAnchor: 'end', // Anchor text to the end for better alignment - height: 100, // Increase height for rotated labels - interval: 0, // Show all labels + angle: -45, + textAnchor: 'end', + height: 100, + interval: 0, style: { - fontSize: '12px', // Adjust font size if needed + fontSize: '12px', overflow: 'visible', - whiteSpace: 'nowrap' + whiteSpace: 'nowrap', + lineHeight: 1.4, } }} /> - - Laki-Laki + + + Laki-Laki + - - Perempuan + + + Perempuan + @@ -95,4 +136,4 @@ function Page() { ); } -export default Page; +export default Page; \ No newline at end of file diff --git a/src/app/darmasaba/(pages)/ekonomi/jumlah-penduduk-miskin/page.tsx b/src/app/darmasaba/(pages)/ekonomi/jumlah-penduduk-miskin/page.tsx index 9f0e9041..9673b549 100644 --- a/src/app/darmasaba/(pages)/ekonomi/jumlah-penduduk-miskin/page.tsx +++ b/src/app/darmasaba/(pages)/ekonomi/jumlah-penduduk-miskin/page.tsx @@ -2,7 +2,7 @@ import jumlahPendudukMiskin from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-penduduk-miskin'; import colors from '@/con/colors'; import { BarChart } from '@mantine/charts'; -import { Box, Paper, Skeleton, Stack, Text } from '@mantine/core'; +import { Box, Paper, Skeleton, Stack, Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { useEffect, useState } from 'react'; import { useProxy } from 'valtio/utils'; @@ -17,13 +17,10 @@ function Page() { const state = useProxy(jumlahPendudukMiskin) const [chartData, setChartData] = useState([]) - useShallowEffect(() => { state.findMany.load() }, []) - - useEffect(() => { if (state.findMany.data) { setChartData(state.findMany.data.map((item) => ({ @@ -48,20 +45,30 @@ function Page() { - + Jumlah Penduduk Miskin - </Text> + - Jumlah Data Penduduk Miskin - + + Jumlah Data Penduduk Miskin + + {state.findMany.data?.reduce((sum, item) => sum + (Number(item.totalPoorPopulation) || 0), 0).toLocaleString()} Orang - </Text> + - Jumlah Penduduk Miskin Per Tahun + + Jumlah Penduduk Miskin Per Tahun + @@ -64,114 +64,151 @@ function Page() { ) } - if (!stateGrafikNganggur.findMany.data) { - return ( - - - - ) - } return ( - - + + Jumlah Penduduk Usia Kerja Yang Menganggur - </Text> + - Pengangguran Berdasarkan Usia - {mounted && donutGrafikNganggurData.length > 0 ? ( - - + + Pengangguran Berdasarkan Usia + + {mounted && donutGrafikNganggurData.length > 0 ? ( + + + + - ) : } + ) : ( + + )} - 18-25 + + 18-25 + - 26-35 + + 26-35 + - 36-45 + + 36-45 + - 46+ + + 46+ + - Pengangguran Berdasarkan Pendidikan - {mounted2 && donutGrafikNganggurDataPendidikan.length > 0 ? (
- - - -
) : } + + Pengangguran Berdasarkan Pendidikan + + {mounted2 && donutGrafikNganggurDataPendidikan.length > 0 ? ( +
+ + + +
+ ) : ( + + )} - SD + + SD + - SMP + + SMP + - SMA/SMK + + SMA/SMK + - D3 + + D3 + - S1 + + S1 + @@ -183,4 +220,4 @@ function Page() { ); } -export default Page; +export default Page; \ No newline at end of file diff --git a/src/app/darmasaba/(pages)/ekonomi/jumlah-pengangguran/page.tsx b/src/app/darmasaba/(pages)/ekonomi/jumlah-pengangguran/page.tsx index ba8b5920..25dd4632 100644 --- a/src/app/darmasaba/(pages)/ekonomi/jumlah-pengangguran/page.tsx +++ b/src/app/darmasaba/(pages)/ekonomi/jumlah-pengangguran/page.tsx @@ -36,7 +36,6 @@ function Page() { useEffect(() => { setMounted(true); if (state.findMany.data) { - // Set chart data setChartData(state.findMany.data.map((item) => ({ id: item.id, bulan: item.month, @@ -44,7 +43,6 @@ function Page() { takberpendidikan: Number(item.uneducatedUnemployment), }))); - // Calculate yearly totals const currentYearData = state.findMany.data.filter(item => item.year === currentYear); if (currentYearData.length > 0) { const yearlyTotal = { @@ -72,30 +70,37 @@ function Page() { - - + + Jumlah Pengangguran - </Text> + - DATA PENGANGGURAN DESA + + DATA PENGANGGURAN DESA + - + {/* Total Unemployment Card */} - Total Pengangguran - + + Total Pengangguran + + {yearlyData?.total.toLocaleString() || 0} Orang - + Total data tahun {currentYear} @@ -105,11 +110,13 @@ function Page() { - Pengangguran Terdidik - + + Pengangguran Terdidik + + {yearlyData?.educated.toLocaleString() || 0} Orang - + {yearlyData ? <> {((yearlyData.educated / yearlyData.total) * 100).toFixed(1)}% @@ -123,11 +130,13 @@ function Page() { - Pengangguran Tidak Terdidik - + + Pengangguran Tidak Terdidik + + {yearlyData?.uneducated.toLocaleString() || 0} Orang - + {yearlyData ? <> {((yearlyData.uneducated / yearlyData.total) * 100).toFixed(1)}% @@ -142,13 +151,17 @@ function Page() { - Pengangguran Berpendidikan + + Pengangguran Berpendidikan + - Pengangguran Tak Berpendidikan + + Pengangguran Tak Berpendidikan + @@ -156,15 +169,24 @@ function Page() { {!mounted || chartData.length === 0 ? ( - Data Pengangguran Terdidik dan Tidak Terdidik - Belum ada data untuk ditampilkan dalam grafik + + Data Pengangguran Terdidik dan Tidak Terdidik + + + Belum ada data untuk ditampilkan dalam grafik + ) : ( - Data Pengangguran Terdidik dan Tidak Terdidik - + + Data Pengangguran Terdidik dan Tidak Terdidik + + )} - - Detail Data Pengangguran - - - - Bulan - Total - Terdidik - Tidak Terdidik - Perubahan - - - - {state.findMany.data?.map((item, index) => ( - - {item.month} - {item.totalUnemployment} - {item.educatedUnemployment} - {item.uneducatedUnemployment} - {item.percentageChange}% + + Detail Data Pengangguran + + +
+ + + + Bulan + + + Total + + + Terdidik + + + Tidak Terdidik + + + Perubahan + - ))} - -
+ + + {state.findMany.data?.map((item, index) => ( + + + {item.month} + + + {item.totalUnemployment} + + + {item.educatedUnemployment} + + + {item.uneducatedUnemployment} + + + {item.percentageChange}% + + + ))} + + +
@@ -211,4 +256,4 @@ function Page() { ); } -export default Page; +export default Page; \ No newline at end of file diff --git a/src/app/darmasaba/(pages)/ekonomi/lowongan-kerja-lokal/[id]/page.tsx b/src/app/darmasaba/(pages)/ekonomi/lowongan-kerja-lokal/[id]/page.tsx index 53d29443..73aa801a 100644 --- a/src/app/darmasaba/(pages)/ekonomi/lowongan-kerja-lokal/[id]/page.tsx +++ b/src/app/darmasaba/(pages)/ekonomi/lowongan-kerja-lokal/[id]/page.tsx @@ -2,7 +2,7 @@ import lowonganKerjaState from '@/app/admin/(dashboard)/_state/ekonomi/lowongan-kerja'; import colors from '@/con/colors'; -import { Box, Button, Center, Group, Paper, Skeleton, Stack, Text } from '@mantine/core'; +import { Box, Button, Center, Group, Paper, Skeleton, Stack, Text, Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconBrandWhatsapp, IconBriefcase, IconCurrencyDollar, IconMapPin, IconPhone } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -33,18 +33,25 @@ function DetailLowonganKerjaUser() { ); } + const formatRupiah = (value: number) => + new Intl.NumberFormat("id-ID", { + style: "currency", + currency: "IDR", + minimumFractionDigits: 0, + }).format(value); + return ( - + - {/* Judul */} - + {/* Judul Posisi - H1 */} + {data.posisi} - </Text> - <Text c="dimmed" fz="sm"> + + + {/* Tanggal Posting - Caption */} + Diposting: {new Date(data.createdAt).toLocaleDateString('id-ID', { day: '2-digit', month: 'long', @@ -70,44 +83,72 @@ function DetailLowonganKerjaUser() { - {data.namaPerusahaan} + + {data.namaPerusahaan} + - {data.lokasi} + + {data.lokasi} + - {data.notelp} + + {data.notelp} + - {data.gaji || '-'} + + {formatRupiah(Number(data.gaji)) || '-'} + - {data.tipePekerjaan} + + {data.tipePekerjaan} + + {/* Deskripsi Pekerjaan - H2 */} - + Deskripsi Pekerjaan - </Text> + + {/* Kualifikasi - H2 */} - + Kualifikasi - </Text> + - + Lowongan Kerja Lokal - </Text> + } value={search} onChange={(e) => setSearch(e.currentTarget.value)} + fz={{ base: 'sm', md: 'md' }} + lh={1.5} /> @@ -80,30 +82,42 @@ function Page() { - - + + - {v.posisi} - {v.namaPerusahaan} + + {v.posisi} + + + {v.namaPerusahaan} + - - - {v.lokasi} + + + + {v.lokasi} + - - + + - Full Time - {formatCurrency(v.gaji)} + + Full Time + + + {formatCurrency(v.gaji)} + - + ) @@ -123,4 +137,4 @@ function Page() { ); } -export default Page; +export default Page; \ No newline at end of file diff --git a/src/app/darmasaba/(pages)/ekonomi/pasar-desa/[id]/page.tsx b/src/app/darmasaba/(pages)/ekonomi/pasar-desa/[id]/page.tsx index c013d69f..579c8286 100644 --- a/src/app/darmasaba/(pages)/ekonomi/pasar-desa/[id]/page.tsx +++ b/src/app/darmasaba/(pages)/ekonomi/pasar-desa/[id]/page.tsx @@ -1,7 +1,7 @@ 'use client' import colors from '@/con/colors'; -import { Box, Button, Paper, Stack, Text, Image, Skeleton, Group, Badge, Divider } from '@mantine/core'; -import { IconArrowBack, IconMapPin, IconPhone, IconStar } from '@tabler/icons-react'; +import { Box, Button, Paper, Stack, Text, Image, Skeleton, Group, Badge, Divider, Title } from '@mantine/core'; +import { IconArrowBack, IconBrandWhatsapp, IconMapPin, IconPhone, IconStar } from '@tabler/icons-react'; import { useRouter, useParams } from 'next/navigation'; import React from 'react'; import { useProxy } from 'valtio/utils'; @@ -31,14 +31,16 @@ function DetailProdukPasarUser() { {/* Tombol kembali */} - + - Tidak ada gambar + + Tidak ada gambar + )} {/* Detail Produk */} - + {data.nama || 'Produk Tanpa Nama'} - </Text> + - Rp {data.harga?.toLocaleString('id-ID')} + + Rp {data.harga?.toLocaleString('id-ID')} + {data.rating && ( - {data.rating} + + {data.rating} + )} @@ -95,16 +102,20 @@ function DetailProdukPasarUser() { {/* Info Tambahan */} - Kategori + + Kategori + {data.KategoriToPasar && data.KategoriToPasar.length > 0 ? ( data.KategoriToPasar.map((kategori) => ( - + {kategori.kategori.nama} )) ) : ( - Tidak ada kategori + + Tidak ada kategori + )} @@ -112,14 +123,18 @@ function DetailProdukPasarUser() { {data.alamatUsaha && ( - {data.alamatUsaha} + + {data.alamatUsaha} + )} {data.kontak && ( - {data.kontak} + + {data.kontak} + )} @@ -128,8 +143,10 @@ function DetailProdukPasarUser() { {/* Deskripsi */} - Deskripsi Produk - + + Deskripsi Produk + + Tidak ada deskripsi. @@ -144,8 +161,11 @@ function DetailProdukPasarUser() { component="a" href={`https://wa.me/${data.kontak.replace(/[^0-9]/g, '')}`} target="_blank" + leftSection={} > - Hubungi Penjual via WhatsApp + + Hubungi Penjual via WhatsApp + )} @@ -154,4 +174,4 @@ function DetailProdukPasarUser() { ); } -export default DetailProdukPasarUser; +export default DetailProdukPasarUser; \ No newline at end of file diff --git a/src/app/darmasaba/(pages)/ekonomi/pasar-desa/page.tsx b/src/app/darmasaba/(pages)/ekonomi/pasar-desa/page.tsx index d655b676..601edc81 100644 --- a/src/app/darmasaba/(pages)/ekonomi/pasar-desa/page.tsx +++ b/src/app/darmasaba/(pages)/ekonomi/pasar-desa/page.tsx @@ -7,7 +7,7 @@ import { IconBrandWhatsapp, IconMapPinFilled, IconSearch, IconStarFilled } from import { motion } from 'motion/react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; -import { useProxy } from 'valtio/utils'; + import { useProxy } from 'valtio/utils'; import BackButton from '../../desa/layanan/_com/BackButto'; function Page() { @@ -30,8 +30,8 @@ function Page() { const filteredData = selectedCategory ? data?.filter(item => - item.KategoriToPasar?.some(kategori => kategori.kategoriId === selectedCategory) - ) + item.KategoriToPasar?.some(kategori => kategori.kategoriId === selectedCategory) + ) : data; useShallowEffect(() => { @@ -55,7 +55,7 @@ function Page() { - + <Title order={1} c={colors["blue-button"]} fw="bold" lh={1.15}> Pasar Desa @@ -71,7 +71,14 @@ function Page() { - + Pasar Desa Online adalah media promosi untuk membantu warga memasarkan dan memperkenalkan produk mereka. @@ -92,6 +99,9 @@ function Page() { searchable nothingFoundMessage="Tidak ada kategori ditemukan" style={{ width: '100%' }} + fz={{ base: 'sm', md: 'md' }} + lh={{ base: 1.5, md: 1.55 }} + c="black" /> @@ -114,15 +124,29 @@ function Page() { style={{ objectFit: 'cover' }} loading="lazy" /> - + {v.nama} - + Rp {v.harga.toLocaleString('id-ID')} - + - + {v.rating} @@ -130,7 +154,11 @@ function Page() { - + {v.alamatUsaha} diff --git a/src/app/darmasaba/(pages)/ekonomi/program-kemiskinan/page.tsx b/src/app/darmasaba/(pages)/ekonomi/program-kemiskinan/page.tsx index 03d578ba..df12bb8d 100644 --- a/src/app/darmasaba/(pages)/ekonomi/program-kemiskinan/page.tsx +++ b/src/app/darmasaba/(pages)/ekonomi/program-kemiskinan/page.tsx @@ -69,48 +69,47 @@ function Page() { } return ( - + - + Program Kemiskinan setSearch(e.target.value)} leftSection={} - w={{ base: "50%", md: "100%" }} + w="100%" /> Berbagai program bantuan untuk mengurangi kemiskinan dan meningkatkan kesejahteraan masyarakat - + {state.findMany.data.map(v => { return ( - + {v.nama} @@ -139,7 +137,7 @@ function Page() { ) })} -
+
{ @@ -147,16 +145,15 @@ function Page() { window.scrollTo({ top: 0, behavior: 'smooth' }) }} total={totalPages} - my={"md"} + my="md" />
- + Statistik Kemiskinan Masyarakat @@ -166,7 +163,7 @@ function Page() { <Box w="100%" style={{ overflowX: 'auto' }}> <Center> <RechartsLineChart - width={Math.min(800, window.innerWidth - 100)} + width={Math.min(800, typeof window !== 'undefined' ? window.innerWidth - 100 : 800)} height={400} data={statistikData} margin={{ top: 5, right: 30, left: 20, bottom: 5 }} @@ -175,10 +172,12 @@ function Page() { <XAxis dataKey="tahun" label={{ value: 'Tahun', position: 'insideBottomRight', offset: -5 }} + tick={{ fontSize: 12 }} /> <YAxis label={{ value: 'Jumlah', angle: -90, position: 'insideLeft' }} domain={[0, 'auto']} + tick={{ fontSize: 12 }} /> <Tooltip formatter={(value) => [`${value} orang`, 'Jumlah']} @@ -199,9 +198,9 @@ function Page() { ) : ( <Box p="md" ta="center" bg="gray.0" style={{ borderRadius: '8px' }}> <Text - fz={{ base: '12px', md: '14px' }} + fz={{ base: 'xs', md: 'sm' }} c="dimmed" - lh={{ base: '1.4', md: '1.5' }} + lh={{ base: 1.4, md: 1.4 }} > {state.findMany.loading ? 'Memuat data statistik...' diff --git a/src/app/darmasaba/(pages)/ekonomi/sektor-unggulan-desa/page.tsx b/src/app/darmasaba/(pages)/ekonomi/sektor-unggulan-desa/page.tsx index 2ceb23f2..6ae9be12 100644 --- a/src/app/darmasaba/(pages)/ekonomi/sektor-unggulan-desa/page.tsx +++ b/src/app/darmasaba/(pages)/ekonomi/sektor-unggulan-desa/page.tsx @@ -1,6 +1,6 @@ 'use client' import colors from '@/con/colors'; -import { Stack, Box, Text, Paper, Skeleton, Center } from '@mantine/core'; +import { Stack, Box, Text, Paper, Skeleton, Center, Title } from '@mantine/core'; import React from 'react'; import BackButton from '../../desa/layanan/_com/BackButto'; import { BarChart } from '@mantine/charts'; @@ -28,16 +28,15 @@ function Page() { ) } - // Add this check before the return statement if (data.length === 0) { return ( <Stack pos="relative" bg={colors.Bg} py="xl" gap={22}> <Box px={{ base: 'md', md: 100 }}> <BackButton /> - <Text fz={{ base: 'h1', md: '2.5rem' }} c={colors['blue-button']} fw="bold"> + <Title order={1} c={colors['blue-button']} fw="bold"> Sektor Unggulan Desa Darmasaba - </Text> - <Text c="dimmed" mt="md"> + + Data sektor unggulan belum tersedia @@ -53,32 +52,49 @@ function Page() { Ton: item.value, })); - const chartWidth = Math.max(600, chartData.length * 150); // contoh: 150px per bar + const chartWidth = Math.max(600, chartData.length * 150); return ( - + - - + + Sektor Unggulan Desa Darmasaba + + + Desa Darmasaba dikenal sebagai desa dengan potensi unggulan di sektor pertanian dan peternakan - Desa Darmasaba dikenal sebagai desa dengan potensi unggulan di sektor pertanian dan peternakan - - + + {data.map((v, k) => { return ( - - {v.name} - + + + {v.name} + + - ) + ); })} - Statistik Sektor Unggulan Darmasaba + + Statistik Sektor Unggulan Darmasaba +
@@ -111,4 +127,4 @@ function Page() { ); } -export default Page; +export default Page; \ No newline at end of file diff --git a/src/app/darmasaba/(pages)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/[id]/page.tsx b/src/app/darmasaba/(pages)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/[id]/page.tsx new file mode 100644 index 00000000..3a8400e5 --- /dev/null +++ b/src/app/darmasaba/(pages)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/[id]/page.tsx @@ -0,0 +1,174 @@ +'use client'; +import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi'; +import colors from '@/con/colors'; +import { + Box, + Divider, + Group, + Image, + Paper, + Skeleton, + Stack, + Text, + Title, +} from '@mantine/core'; +import { useShallowEffect } from '@mantine/hooks'; +import { IconArrowBack } from '@tabler/icons-react'; +import { useParams, useRouter } from 'next/navigation'; +import { useProxy } from 'valtio/utils'; + +function DetailPegawaiBumdes() { + const statePegawai = useProxy(stateStrukturBumDes.pegawai); + const params = useParams(); + const router = useRouter(); + + useShallowEffect(() => { + stateStrukturBumDes.posisiOrganisasi.findMany.load(); + statePegawai.findUnique.load(params?.id as string); + }, []); + + if (!statePegawai.findUnique.data) { + return ( + + + + ); + } + + const data = statePegawai.findUnique.data; + + return ( + + {/* Back button */} + + router.back()} + style={{ + cursor: 'pointer', + display: 'flex', + alignItems: 'center', + gap: 8, + }} + > + + + Kembali + + + + + + + {/* Foto Profil */} + {data.namaLengkap + + {/* Nama & Jabatan */} + + + {data.namaLengkap || '-'} {data.gelarAkademik || ''} + + + + {data.posisi?.nama || 'Posisi tidak tersedia'} + + + + + + + {/* Informasi Detail */} + + + + + + + + + + ); +} + +/* Komponen Baris Informasi */ +function InfoRow({ + label, + value, + valueColor, + multiline = false, +}: { + label: string; + value?: string | null; + valueColor?: string; + multiline?: boolean; +}) { + return ( + + + {label} + + + + {value || '-'} + + + ); +} + +export default DetailPegawaiBumdes; diff --git a/src/app/darmasaba/(pages)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/page.tsx b/src/app/darmasaba/(pages)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/page.tsx index 5471fb5d..8432fb34 100644 --- a/src/app/darmasaba/(pages)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/page.tsx +++ b/src/app/darmasaba/(pages)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/page.tsx @@ -1,7 +1,6 @@ /* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable @typescript-eslint/no-explicit-any */ 'use client' - import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi' import colors from '@/con/colors' import { @@ -32,12 +31,13 @@ import { IconZoomOut, } from '@tabler/icons-react' import { debounce } from 'lodash' +import { useTransitionRouter } from 'next-view-transitions' import { OrganizationChart } from 'primereact/organizationchart' import { useEffect, useRef, useState } from 'react' import { useProxy } from 'valtio/utils' import BackButton from '../../desa/layanan/_com/BackButto' +import '../../ppid/struktur-ppid/struktur.css' import { useMediaQuery } from '@mantine/hooks' -import { useTransitionRouter } from 'next-view-transitions' export default function Page() { return ( @@ -49,14 +49,16 @@ export default function Page() { paddingBottom: 48, }} > - + + Struktur Organisasi & SK Pengurus BumDes @@ -75,14 +77,18 @@ export default function Page() { } function StrukturOrganisasiBumDes() { - const router = useTransitionRouter() const stateOrganisasi: any = useProxy(stateStrukturBumDes.pegawai) + const router = useTransitionRouter() const chartContainerRef = useRef(null) const [scale, setScale] = useState(1) const [isFullscreen, setFullscreen] = useState(false) const [searchQuery, setSearchQuery] = useState('') + + // debounce pencarian const debouncedSearch = useRef( - debounce((value: string) => setSearchQuery(value), 1000) + debounce((value: string) => { + setSearchQuery(value) + }, 1000) ).current useEffect(() => { @@ -90,8 +96,7 @@ function StrukturOrganisasiBumDes() { }, []) const isLoading = - !stateOrganisasi.findMany.data && - stateOrganisasi.findMany.loading !== false + !stateOrganisasi.findMany.data && stateOrganisasi.findMany.loading !== false if (isLoading) { return ( @@ -149,7 +154,7 @@ function StrukturOrganisasiBumDes() { ) } - // 📊 susun struktur organisasi + // 🧩 buat struktur organisasi const posisiMap = new Map() const aktifPegawai = data.filter((p: any) => p.isActive) @@ -183,7 +188,6 @@ function StrukturOrganisasiBumDes() { name: pegawai?.namaLengkap || 'Belum Ditugaskan', title: node.nama || 'Tanpa Jabatan', image: pegawai?.image?.link || '/img/default.png', - description: node.deskripsi || '', }, children: node.children?.map(toOrgChartFormat) || [], } @@ -208,7 +212,7 @@ function StrukturOrganisasiBumDes() { chartData = filterNodes(chartData) } - // 🔍 fullscreen dan zoom control + // 🎬 fullscreen & zoom control const toggleFullscreen = () => { if (!document.fullscreenElement) { chartContainerRef.current?.requestFullscreen() @@ -225,7 +229,7 @@ function StrukturOrganisasiBumDes() { return ( - {/* 🧭 Kontrol atas */} + {/* 🔍 Controls */} + - {/* 🧩 Chart Container */} -
- + + + } + className="p-organizationchart p-organizationchart-horizontal" + /> + + +
+
+ ) +} + +function NodeCard({ node, router }: any) { + const imageSrc = node?.data?.image || '/img/default.png' + const name = node?.data?.name || 'Tanpa Nama' + const title = node?.data?.title || 'Tanpa Jabatan' + const hasId = Boolean(node?.data?.id) + const isMobile = useMediaQuery("(max-width: 768px)"); + + return ( + + {(styles) => ( + { + if (hasId) { + e.currentTarget.style.transform = 'translateY(-4px)' + e.currentTarget.style.boxShadow = '0 8px 24px rgba(28, 110, 164, 0.25)' + } + }} + onMouseLeave={(e) => { + if (hasId) { + e.currentTarget.style.transform = 'translateY(0)' + e.currentTarget.style.boxShadow = '' + } + }} + > + + {/* Photo */} + + {name} + + + {/* Name */} + + {name} + + + {/* Title/Position */} + + {title} + + + {/* Detail Button */} + {hasId && ( +
-
- ) - } - - function NodeCard({ node, router }: any) { - const imageSrc = node?.data?.image || '/img/default.png' - const name = node?.data?.name || 'Tanpa Nama' - const title = node?.data?.title || 'Tanpa Jabatan' - const hasId = Boolean(node?.data?.id) - const isMobile = useMediaQuery("(max-width: 768px)"); - - return ( - - {(styles) => ( - { - if (hasId) { - e.currentTarget.style.transform = 'translateY(-4px)' - e.currentTarget.style.boxShadow = '0 8px 24px rgba(28, 110, 164, 0.25)' - } - }} - onMouseLeave={(e) => { - if (hasId) { - e.currentTarget.style.transform = 'translateY(0)' - e.currentTarget.style.boxShadow = '' - } - }} - > - - {/* Photo */} - - {name} - - - {/* Name */} - - {name} - - - {/* Title/Position */} - - {title} - - - {/* Detail Button */} - {hasId && ( - - )} - - + Lihat Detail + )} - - ) - } \ No newline at end of file +
+ + )} + + ) +} diff --git a/src/app/darmasaba/(pages)/pendidikan/perpustakaan-digital/layout.tsx b/src/app/darmasaba/(pages)/pendidikan/perpustakaan-digital/layout.tsx index a592e597..8d014d98 100644 --- a/src/app/darmasaba/(pages)/pendidikan/perpustakaan-digital/layout.tsx +++ b/src/app/darmasaba/(pages)/pendidikan/perpustakaan-digital/layout.tsx @@ -1,8 +1,24 @@ 'use client' +import colors from '@/con/colors'; +import { Box } from '@mantine/core'; +import { usePathname } from 'next/navigation'; import React, { Suspense } from 'react'; import LayoutTabs from './_lib/layoutTabs'; function Layout({ children }: { children: React.ReactNode }) { + const pathname = usePathname(); + const segments = pathname.split('/').filter(Boolean); + const isDetailPage = segments.length === 5; // [darmasaba, desa, berita, kategori, id] + + if (isDetailPage) { + // Tampilkan tanpa tab menu + return ( + + {children} + + ); + } + return ( Loading...}> diff --git a/src/app/darmasaba/(pages)/ppid/struktur-ppid/page.tsx b/src/app/darmasaba/(pages)/ppid/struktur-ppid/page.tsx index 89a35181..cb58be3c 100644 --- a/src/app/darmasaba/(pages)/ppid/struktur-ppid/page.tsx +++ b/src/app/darmasaba/(pages)/ppid/struktur-ppid/page.tsx @@ -517,7 +517,7 @@ function NodeCard({ node, router }: any) { fontWeight: 600, }} > - Lihat Detail + Lihat Detail )}
diff --git a/src/app/waiting-room/page.tsx b/src/app/waiting-room/page.tsx index c725dd5b..19cd25ad 100644 --- a/src/app/waiting-room/page.tsx +++ b/src/app/waiting-room/page.tsx @@ -10,10 +10,19 @@ import { Stack, Text, Title, + Progress, + Group, } from '@mantine/core'; import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; -import { authStore } from '@/store/authStore'; // ✅ integrasi authStore +import { authStore } from '@/store/authStore'; + +// ⚙️ Configuration +const CONFIG = { + POLL_INTERVAL: 3000, // 3 detik + MAX_RETRIES: 2, // 2x retry + TIMEOUT_DURATION: 5 * 60 * 1000, // 5 menit (300 detik) +}; async function fetchUser() { const res = await fetch('/api/auth/me', { @@ -26,21 +35,48 @@ async function fetchUser() { return res.json(); } +function formatTime(seconds: number): string { + const mins = Math.floor(seconds / 60); + const secs = seconds % 60; + return `${mins}:${secs.toString().padStart(2, '0')}`; +} + export default function WaitingRoom() { const router = useRouter(); const [user, setUser] = useState(null); const [error, setError] = useState(null); const [isRedirecting, setIsRedirecting] = useState(false); const [retryCount, setRetryCount] = useState(0); - const MAX_RETRIES = 2; + + // ⏱️ Countdown timer + const [timeLeft, setTimeLeft] = useState(CONFIG.TIMEOUT_DURATION / 1000); // dalam detik + const [hasTimedOut, setHasTimedOut] = useState(false); + // ⏱️ Countdown effect + useEffect(() => { + if (isRedirecting || hasTimedOut) return; + const countdownInterval = setInterval(() => { + setTimeLeft((prev) => { + if (prev <= 1) { + setHasTimedOut(true); + setError('Waktu tunggu habis. Silakan hubungi administrator atau coba login ulang nanti.'); + return 0; + } + return prev - 1; + }); + }, 1000); + + return () => clearInterval(countdownInterval); + }, [isRedirecting, hasTimedOut]); + + // 🔄 Polling effect useEffect(() => { let isMounted = true; let interval: ReturnType; const poll = async () => { - if (isRedirecting || !isMounted) return; + if (isRedirecting || !isMounted || hasTimedOut) return; try { const data = await fetchUser(); @@ -59,12 +95,11 @@ export default function WaitingRoom() { }); } - // In the poll function + // ✅ Check if approved if (currentUser?.isActive === true) { setIsRedirecting(true); clearInterval(interval); - // Update authStore with the current user data authStore.setUser({ id: currentUser.id, name: currentUser.name || 'User', @@ -78,7 +113,7 @@ export default function WaitingRoom() { localStorage.removeItem('auth_nomor'); localStorage.removeItem('auth_username'); - // Force a session refresh + // Force session refresh try { const res = await fetch('/api/auth/refresh-session', { method: 'POST', @@ -99,26 +134,26 @@ export default function WaitingRoom() { redirectPath = '/admin/pendidikan/info-sekolah/jenjang-pendidikan'; break; } - window.location.href = redirectPath; // Use window.location to force full page reload + window.location.href = redirectPath; } } catch (error) { console.error('Error refreshing session:', error); - router.refresh(); // Fallback to client-side refresh + router.refresh(); } } } catch (err: any) { if (!isMounted) return; if (err.message.includes('401')) { - if (retryCount < MAX_RETRIES) { + if (retryCount < CONFIG.MAX_RETRIES) { setRetryCount((prev) => prev + 1); setTimeout(() => { - if (isMounted) interval = setInterval(poll, 3000); + if (isMounted) interval = setInterval(poll, CONFIG.POLL_INTERVAL); }, 800); } else { setError('Sesi tidak valid. Silakan login ulang.'); clearInterval(interval); - authStore.setUser(null); // ✅ clear sesi + authStore.setUser(null); } } else { console.error('Error polling:', err); @@ -126,26 +161,53 @@ export default function WaitingRoom() { } }; - interval = setInterval(poll, 3000); + interval = setInterval(poll, CONFIG.POLL_INTERVAL); return () => { isMounted = false; if (interval) clearInterval(interval); }; - }, [router, isRedirecting, retryCount]); + }, [router, isRedirecting, retryCount, hasTimedOut]); - // ✅ UI Error - if (error) { + // 🚨 Handle logout + const handleLogout = async () => { + try { + await fetch('/api/auth/logout', { + method: 'POST', + credentials: 'include' + }); + } catch (err) { + console.error('Logout error:', err); + } finally { + authStore.setUser(null); + localStorage.clear(); + router.push('/login'); + } + }; + + // ❌ UI Error / Timeout + if (error || hasTimedOut) { return ( -
- +
+ - - Sesi Tidak Valid + <Title order={3} c="red" ta="center"> + {hasTimedOut ? '⏱️ Waktu Habis' : '❌ Sesi Tidak Valid'} - {error} - + + {error || 'Waktu tunggu persetujuan telah habis.'} + + + Silakan hubungi Superadmin atau coba login ulang nanti. + + + +
@@ -171,24 +233,56 @@ export default function WaitingRoom() { ); } - // ✅ UI Default (MENUNGGU) — INI YANG KAMU HILANGKAN! + // ⏳ UI Default (MENUNGGU) + const progressValue = ((CONFIG.TIMEOUT_DURATION / 1000 - timeLeft) / (CONFIG.TIMEOUT_DURATION / 1000)) * 100; + return (
- Menunggu Persetujuan + ⏳ Menunggu Persetujuan + Akun Anda sedang dalam proses verifikasi oleh Superadmin. - + + Nomor: {user?.nomor || '...'} + + {/* ⏱️ Countdown Timer */} + + + Sisa waktu: + + {formatTime(timeLeft)} + + + + + + Jangan tutup halaman ini. Anda akan dialihkan otomatis setelah disetujui. + + {/* 🚪 Tombol Keluar */} +