Fix All Search Admin

This commit is contained in:
2026-01-05 17:11:30 +08:00
parent f436aa2ef0
commit daaed8089b
39 changed files with 872 additions and 694 deletions

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

@@ -166,7 +166,7 @@ function ListAPBDesa({ search }: { search: string }) {
<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}`
@@ -243,7 +243,7 @@ function ListAPBDesa({ search }: { search: string }) {
</Box> </Box>
<Button <Button
variant="light" variant="light"
color="green" color="blue"
fullWidth fullWidth
onClick={() => onClick={() =>
router.push( router.push(

View File

@@ -128,10 +128,18 @@ function ListBelanja({ search }: { search: string }) {
> >
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh style={{ width: '35%' }}>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: '20%' }}>Persentase</TableTh> </TableTh>
<TableTh style={{ width: '20%' }}>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>

View File

@@ -120,12 +120,20 @@ function ListPembiayaan({ search }: { search: string }) {
width: '100%', width: '100%',
}} }}
> >
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh style={{ width: '35%' }}>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: '20%' }}>Persentase</TableTh> </TableTh>
<TableTh style={{ width: '20%' }}>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>

View File

@@ -160,7 +160,6 @@ function ListPendapatan({ search }: { search: string }) {
px="xs" px="xs"
> >
<IconEdit size={16} /> <IconEdit size={16} />
<Text ml={5}>Edit</Text>
</Button> </Button>
</TableTd> </TableTd>
<TableTd> <TableTd>
@@ -176,7 +175,6 @@ function ListPendapatan({ search }: { search: string }) {
px="xs" px="xs"
> >
<IconTrash size={16} /> <IconTrash size={16} />
<Text ml={5}>Hapus</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>

View File

@@ -153,9 +153,14 @@ function ListPosisiOrganisasiBumDes({ search }: { search: string }) {
</Text> </Text>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Text fz="sm" fw={500} lh={1.45} c="dimmed" lineClamp={2}> <Text
{item.deskripsi || '-'} fz="sm"
</Text> fw={500}
lh={1.45}
c="dimmed"
lineClamp={2}
dangerouslySetInnerHTML={{ __html: item.deskripsi || '-' }}
/>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Text fz="md" fw={500} lh={1.45}>{item.hierarki || '-'}</Text> <Text fz="md" fw={500} lh={1.45}>{item.hierarki || '-'}</Text>
@@ -223,9 +228,14 @@ function ListPosisiOrganisasiBumDes({ search }: { search: string }) {
<Text fz="sm" fw={600} lh={1.4}> <Text fz="sm" fw={600} lh={1.4}>
Deskripsi Deskripsi
</Text> </Text>
<Text fz="sm" fw={500} lh={1.4}> <Text
{item.deskripsi || '-'} fz="sm"
</Text> fw={500}
lh={1.45}
c="dimmed"
lineClamp={2}
dangerouslySetInnerHTML={{ __html: item.deskripsi || '-' }}
/>
</Box> </Box>
<Box> <Box>
<Text fz="sm" fw={600} lh={1.4}> <Text fz="sm" fw={600} lh={1.4}>

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) {

View File

@@ -28,27 +28,27 @@ 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(search2, 1000); const [debouncedSearch] = useDebouncedValue(search, 1000);
const { data, page, totalPages, loading, load } = statePasar.findMany; const { data, page, totalPages, loading, load } = statePasar.findMany;

View File

@@ -142,9 +142,7 @@ function ListKolaborasiInovasi({ search }: { search: string }) {
</Text> </Text>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Text fz="sm" fw={500} lh={1.45} truncate="end" lineClamp={1}> <Text dangerouslySetInnerHTML={{ __html: item.slug }} fz="sm" fw={500} lh={1.45} truncate="end" lineClamp={1} />
{item.slug}
</Text>
</TableTd> </TableTd>
<TableTd ta="center"> <TableTd ta="center">
<Button <Button
@@ -214,9 +212,7 @@ function ListKolaborasiInovasi({ search }: { search: string }) {
<Text fz="sm" fw={600} lh={1.4}> <Text fz="sm" fw={600} lh={1.4}>
Deskripsi Singkat Deskripsi Singkat
</Text> </Text>
<Text fz="sm" fw={500} lh={1.4}> <Text dangerouslySetInnerHTML={{ __html: item.slug }} fz="sm" fw={500} lh={1.45} truncate="end" lineClamp={1} />
{item.slug}
</Text>
</Box> </Box>
<Box> <Box>
<Button <Button

View File

@@ -141,9 +141,8 @@ function ListJenisLayanan({ search }: { search: string }) {
lh={1.5} lh={1.5}
c={theme.black} c={theme.black}
lineClamp={2} lineClamp={2}
> dangerouslySetInnerHTML={{ __html: item.deskripsi || '-' }}
{item.deskripsi || '-'} />
</Text>
</TableTd> </TableTd>
<TableTd style={{ width: '15%' }} ta="center"> <TableTd style={{ width: '15%' }} ta="center">
<Button <Button
@@ -197,9 +196,13 @@ function ListJenisLayanan({ search }: { search: string }) {
<Text fz="sm" fw={600} lh={1.4} c={theme.black}> <Text fz="sm" fw={600} lh={1.4} c={theme.black}>
Deskripsi Deskripsi
</Text> </Text>
<Text fz="sm" fw={500} lh={1.5} c={theme.black}> <Text
{item.deskripsi || '-'} fz="sm"
</Text> fw={500}
lh={1.5}
c={theme.black}
dangerouslySetInnerHTML={{ __html: item.deskripsi || '-' }}
/>
</Box> </Box>
<Group justify="flex-end" mt="xs"> <Group justify="flex-end" mt="xs">
<Button <Button

View File

@@ -38,7 +38,7 @@ function ListTarifLayanan({ 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, 10000); const [debouncedSearch] = useDebouncedValue(search, 1000);
const { const {
data, data,

View File

@@ -96,15 +96,15 @@ function ListKontakDarurat({ search }: { search: string }) {
{filteredData.length > 0 ? ( {filteredData.length > 0 ? (
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd w={335}>
<Text fw={500} fz="md" lh={1.45} truncate="end" lineClamp={1}> <Text fw={500} fz="md" lh={1.45} truncate="end" lineClamp={1}>
{item.name} {item.name}
</Text> </Text>
</TableTd> </TableTd>
<TableTd> <TableTd w={335}>
<Text fz="sm" lh={1.45} c="dimmed" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} /> <Text fz="sm" lh={1.45} c="dimmed" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd> </TableTd>
<TableTd> <TableTd w={335}>
<Button <Button
variant="light" variant="light"
color="blue" color="blue"

View File

@@ -110,10 +110,10 @@ function ListProgramKesehatan({ search }: { search: string }) {
{item.name} {item.name}
</Text> </Text>
</TableTd> </TableTd>
<TableTd w={200}> <TableTd>
<Text fz="sm" lh={1.5} truncate="end" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsiSingkat }} /> <Text fz="sm" lh={1.5} truncate="end" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsiSingkat }} />
</TableTd> </TableTd>
<TableTd w={200}> <TableTd>
<Text fz="sm" lh={1.5} truncate="end" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} /> <Text fz="sm" lh={1.5} truncate="end" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd> </TableTd>
<TableTd> <TableTd>

View File

@@ -2,7 +2,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { import {
Box, Box,
Button, Button,
@@ -47,16 +47,14 @@ interface ListRespondenProps {
function ListResponden({ search }: ListRespondenProps) { function ListResponden({ search }: ListRespondenProps) {
const state = useProxy(indeksKepuasanState.responden); const state = useProxy(indeksKepuasanState.responden);
const router = useRouter(); const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const { data, page, totalPages, loading, load } = state.findMany; const { data, page, totalPages, loading, load } = state.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10,); load(page, 10, debouncedSearch);
}, [page]); }, [page, debouncedSearch]);
const filteredData = (data || []).filter((item) => { const filteredData = data || [];
const keyword = search.toLowerCase();
return item.name.toLowerCase().includes(keyword);
});
if (loading || !data) { if (loading || !data) {
return ( return (

View File

@@ -84,7 +84,9 @@ function DetailProgramInovasi() {
<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.description || '-' }}></Text> <Box pl={5}>
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.description || '-' }}></Text>
</Box>
</Box> </Box>
<Box> <Box>

View File

@@ -34,8 +34,8 @@ function ListProgramInovasi({ search }: { search: string }) {
const { data, page, totalPages, loading, load } = stateProgramInovasi.findMany; const { data, page, totalPages, loading, load } = stateProgramInovasi.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, debouncedSearch); load(page, 10, debouncedSearch);
}, [page, debouncedSearch]); }, [page, debouncedSearch]);
const filteredData = data || []; const filteredData = data || [];
@@ -144,9 +144,7 @@ function ListProgramInovasi({ search }: { search: string }) {
{/* Description */} {/* Description */}
<Box> <Box>
<Text fz="sm" fw={600} lh={1.4}>Deskripsi</Text> <Text fz="sm" fw={600} lh={1.4}>Deskripsi</Text>
<Text fz="sm" c="gray.7" lineClamp={2}> <Text dangerouslySetInnerHTML={{ __html: item.description || '-' }} fz="sm" c="gray.7" lineClamp={2} />
{item.description || '-'}
</Text>
</Box> </Box>
{/* Link */} {/* Link */}

View File

@@ -1,7 +1,7 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; 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 { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconDatabase, IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; import { IconDatabase, 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';
@@ -35,6 +35,8 @@ function ListDataPendidikan({ 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); // 500ms delay
const { data, page, totalPages, loading, load } = dataPendidikan.findMany;
const handleDelete = () => { const handleDelete = () => {
if (selectedId) { if (selectedId) {
@@ -47,8 +49,8 @@ function ListDataPendidikan({ search }: { search: string }) {
useShallowEffect(() => { useShallowEffect(() => {
setMounted(true); setMounted(true);
stateDPM.findMany.load(); load(page, 10, debouncedSearch);
}, []); }, [page, debouncedSearch]);
useEffect(() => { useEffect(() => {
setMounted(true); setMounted(true);
@@ -68,9 +70,9 @@ function ListDataPendidikan({ search }: { search: string }) {
return item.name.toLowerCase().includes(keyword) || item.jumlah.toString().includes(keyword); return item.name.toLowerCase().includes(keyword) || item.jumlah.toString().includes(keyword);
}); });
if (!stateDPM.findMany.data) { if (loading || !data) {
return ( return (
<Stack gap="sm"> <Stack py={20}>
<Skeleton height={500} radius="md" /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
); );
@@ -137,6 +139,17 @@ function ListDataPendidikan({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
)} )}
<Center mt="md">
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
color="blue"
/>
</Center>
</Paper> </Paper>
<Paper withBorder p="md" bg={colors['white-1']} shadow="sm" radius="md"> <Paper withBorder p="md" bg={colors['white-1']} shadow="sm" radius="md">

View File

@@ -29,7 +29,7 @@ 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 */} {/* Header */}
<Group align="center"> <Group justify='space-between' align="center">
<Title order={3} c={colors['blue-button']}> <Title order={3} c={colors['blue-button']}>
Pratinjau Program Unggulan Pratinjau Program Unggulan
</Title> </Title>

View File

@@ -1,11 +1,23 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' /* eslint-disable @typescript-eslint/no-unused-vars */
'use client';
import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan'; import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Select, Stack, TextInput, Title } from '@mantine/core'; import {
import { IconArrowBack, IconDeviceFloppy } from '@tabler/icons-react'; Box,
Button,
Group,
Loader,
Paper,
Select,
Stack,
Text,
TextInput,
Title,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { ChangeEvent, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
@@ -20,9 +32,13 @@ interface FormResponden {
function EditResponden() { function EditResponden() {
const router = useRouter(); const router = useRouter();
const params = useParams() as { id: string }; const params = useParams() as { id: string };
const state = useProxy(indeksKepuasanState.responden);
const id = params.id; const id = params.id;
// ✅ proxy asli untuk mutasi
const state = indeksKepuasanState.responden;
// ✅ snapshot untuk re-render (read-only)
const snapshot = useProxy(indeksKepuasanState.responden);
const [formData, setFormData] = useState<FormResponden>({ const [formData, setFormData] = useState<FormResponden>({
name: '', name: '',
tanggal: '', tanggal: '',
@@ -31,59 +47,107 @@ function EditResponden() {
kelompokUmurId: '', kelompokUmurId: '',
}); });
// Helper untuk membuat data Select dari state const [originalData, setOriginalData] = useState<FormResponden>({
const mapSelectData = (data: any[] | null | undefined) => name: '',
(data || []) tanggal: '',
.filter(Boolean) jenisKelaminId: '',
.map((v: any) => ({ ratingId: '',
value: v.id || '', kelompokUmurId: '',
label: typeof v.name === 'string' ? v.name : 'Tanpa Nama', });
}));
useEffect(() => { const [isSubmitting, setIsSubmitting] = useState(false);
// Load opsi dropdown
// 🔹 Load data pilihan select
const loadSelectOptions = useCallback(() => {
indeksKepuasanState.jenisKelaminResponden.findMany.load(); indeksKepuasanState.jenisKelaminResponden.findMany.load();
indeksKepuasanState.pilihanRatingResponden.findMany.load(); indeksKepuasanState.pilihanRatingResponden.findMany.load();
indeksKepuasanState.kelompokUmurResponden.findMany.load(); indeksKepuasanState.kelompokUmurResponden.findMany.load();
}, []);
const loadResponden = async () => { // 🔹 Load data responden by ID
if (!id) return; const loadResponden = useCallback(async () => {
if (!id) return;
try {
const data = await state.update.load(id);
if (!data) return;
try { const newForm = {
const data = await state.update.load(id); name: data.name || '',
if (data) { tanggal: data.tanggal || '',
state.update.id = id; jenisKelaminId: data.jenisKelaminId || '',
ratingId: data.ratingId || '',
kelompokUmurId: data.kelompokUmurId || '',
};
// **formData lokal tetap controlled**, tidak overwrite tiap render setFormData(newForm);
setFormData({ setOriginalData(newForm);
name: data.name || '', } catch (error) {
tanggal: data.tanggal || '', console.error('Error loading responden:', error);
jenisKelaminId: data.jenisKelaminId || '', toast.error('Gagal memuat data responden');
ratingId: data.ratingId || '', }
kelompokUmurId: data.kelompokUmurId || '', }, [id]);
});
}
} catch (error) {
console.error("Error loading responden:", error);
toast.error("Gagal memuat data responden");
}
};
useEffect(() => {
loadSelectOptions();
loadResponden(); loadResponden();
}, [id, state.update]); }, [loadSelectOptions, loadResponden]);
const handleChange = (field: keyof FormResponden) => (e: ChangeEvent<HTMLInputElement> | string | null) => {
const value = typeof e === 'string' || e === null ? e || '' : e.currentTarget.value;
setFormData((prev) => ({ ...prev, [field]: value }));
};
// 🔹 Submit data
const handleSubmit = async () => { const handleSubmit = async () => {
state.update.id = id; try {
state.update.form = { ...formData }; // hanya update global state saat submit setIsSubmitting(true);
await state.update.submit(); state.update.id = id;
router.push('/admin/ppid/ikm-desa-darmasaba/responden'); state.update.form = { ...formData }; // mutasi proxy asli ✅
await state.update.submit();
toast.success('Responden berhasil diperbarui!');
router.push('/admin/ppid/indeks-kepuasan-masyarakat/responden');
} catch (error) {
console.error('Error updating responden:', error);
toast.error('Gagal memperbarui responden');
} finally {
setIsSubmitting(false);
}
}; };
// 🔹 Reset form ke data awal
const handleResetForm = () => {
setFormData({ ...originalData });
toast.info('Form dikembalikan ke data awal');
};
// 🔹 Reusable Select component
const ControlledSelect = ({
label,
value,
onChange,
options,
error,
placeholder = 'Pilih',
loading = false,
}: {
label: string;
value: string;
onChange: (val: string) => void;
options: { value: string; label: string }[];
error?: string;
placeholder?: string;
loading?: boolean;
}) => (
<Select
label={<Text fw="bold" fz="sm" mb={4}>{label}</Text>}
value={value}
onChange={(val) => onChange(val || '')}
data={options}
placeholder={placeholder}
disabled={loading}
clearable
searchable
required
radius="md"
error={error}
/>
);
return ( return (
<Box px={{ base: 0, md: 'lg' }} py="xs"> <Box px={{ base: 0, md: 'lg' }} py="xs">
<Group mb="md"> <Group mb="md">
@@ -108,72 +172,72 @@ function EditResponden() {
label="Nama Responden" label="Nama Responden"
placeholder="Masukkan nama responden" placeholder="Masukkan nama responden"
value={formData.name} value={formData.name}
onChange={handleChange('name')} onChange={(e) => setFormData({ ...formData, name: e.currentTarget.value })}
radius="md" radius="md"
required required
/> />
<TextInput <TextInput
label="Tanggal" label="Tanggal"
type="date" type="date"
value={formData.tanggal ? new Date(formData.tanggal).toISOString().split('T')[0] : ''} value={formData.tanggal ? new Date(formData.tanggal).toISOString().split('T')[0] : ''}
onChange={handleChange('tanggal')} onChange={(e) => setFormData({ ...formData, tanggal: e.currentTarget.value })}
radius="md" radius="md"
required required
/> />
<Select <ControlledSelect
label="Jenis Kelamin" label="Jenis Kelamin"
placeholder="Pilih jenis kelamin"
value={formData.jenisKelaminId} value={formData.jenisKelaminId}
onChange={handleChange('jenisKelaminId')} onChange={(val) => setFormData({ ...formData, jenisKelaminId: val })}
data={mapSelectData(indeksKepuasanState.jenisKelaminResponden.findMany.data)} options={(indeksKepuasanState.jenisKelaminResponden.findMany.data || [])
disabled={indeksKepuasanState.jenisKelaminResponden.findMany.loading} .map((v) => ({ value: v.id || '', label: v.name || 'Tanpa Nama' }))}
clearable loading={indeksKepuasanState.jenisKelaminResponden.findMany.loading}
searchable error={!formData.jenisKelaminId ? 'Pilih jenis kelamin' : undefined}
required
radius="md"
error={!formData.jenisKelaminId ? "Pilih jenis kelamin" : undefined}
/> />
<Select <ControlledSelect
label="Rating" label="Rating"
placeholder="Pilih rating"
value={formData.ratingId} value={formData.ratingId}
onChange={handleChange('ratingId')} onChange={(val) => setFormData({ ...formData, ratingId: val })}
data={mapSelectData(indeksKepuasanState.pilihanRatingResponden.findMany.data)} options={(indeksKepuasanState.pilihanRatingResponden.findMany.data || [])
disabled={indeksKepuasanState.pilihanRatingResponden.findMany.loading} .map((v) => ({ value: v.id || '', label: v.name || 'Tanpa Nama' }))}
clearable loading={indeksKepuasanState.pilihanRatingResponden.findMany.loading}
searchable error={!formData.ratingId ? 'Pilih rating' : undefined}
required
radius="md"
error={!formData.ratingId ? "Pilih rating" : undefined}
/> />
<Select <ControlledSelect
label="Kelompok Umur" label="Kelompok Umur"
placeholder="Pilih kelompok umur"
value={formData.kelompokUmurId} value={formData.kelompokUmurId}
onChange={handleChange('kelompokUmurId')} onChange={(val) => setFormData({ ...formData, kelompokUmurId: val })}
data={mapSelectData(indeksKepuasanState.kelompokUmurResponden.findMany.data)} options={(indeksKepuasanState.kelompokUmurResponden.findMany.data || [])
disabled={indeksKepuasanState.kelompokUmurResponden.findMany.loading} .map((v) => ({ value: v.id || '', label: v.name || 'Tanpa Nama' }))}
clearable loading={indeksKepuasanState.kelompokUmurResponden.findMany.loading}
searchable error={!formData.kelompokUmurId ? 'Pilih kelompok umur' : undefined}
required
radius="md"
error={!formData.kelompokUmurId ? "Pilih kelompok umur" : undefined}
/> />
<Group justify="flex-end" mt="md"> <Group justify="right">
<Button variant="light" color="red" onClick={() => router.back()}> <Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal Batal
</Button> </Button>
<Button <Button
leftSection={<IconDeviceFloppy size={20} />}
onClick={handleSubmit} onClick={handleSubmit}
loading={state.update.loading} radius="md"
color={colors['blue-button']} size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -26,7 +26,7 @@ export default function DetailResponden() {
stateDetail.delete.byId(selectedId) stateDetail.delete.byId(selectedId)
setModalHapus(false) setModalHapus(false)
setSelectedId(null) setSelectedId(null)
router.push("/admin/ppid/ikm-desa-darmasaba/responden") router.push("/admin/ppid/indeks-kepuasan-masyarakat/responden")
} }
} }
@@ -108,7 +108,7 @@ export default function DetailResponden() {
variant="light" variant="light"
onClick={() => { onClick={() => {
if (stateDetail.findUnique.data) { if (stateDetail.findUnique.data) {
router.push(`/admin/ppid/ikm-desa-darmasaba/responden/${stateDetail.findUnique.data.id}/edit`); router.push(`/admin/ppid/indeks-kepuasan-masyarakat/responden/${stateDetail.findUnique.data.id}/edit`);
} }
}} }}
disabled={!stateDetail.findUnique.data} disabled={!stateDetail.findUnique.data}

View File

@@ -1,21 +1,20 @@
'use client' 'use client'
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import React from 'react';
import { useRouter } from 'next/navigation';
import grafikBerdasarkanJenisKelamin from '@/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanJenisKelamin';
import { useProxy } from 'valtio/utils';
import { useState } from 'react';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Title, TextInput, Select, Text } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan'; import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan';
import colors from '@/con/colors';
import { Box, Button, Group, Loader, Paper, Select, Stack, TextInput, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
function RespondenCreate() { function RespondenCreate() {
const router = useRouter(); const router = useRouter();
const stategrafikBerdasarkanResponden = useProxy(indeksKepuasanState.responden) const stategrafikBerdasarkanResponden = useProxy(indeksKepuasanState.responden)
const [donutData, setDonutData] = useState<any[]>([]); const [donutData, setDonutData] = useState<any[]>([]);
const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => { const resetForm = () => {
stategrafikBerdasarkanResponden.create.form = { stategrafikBerdasarkanResponden.create.form = {
@@ -35,6 +34,7 @@ function RespondenCreate() {
}) })
const handleSubmit = async () => { const handleSubmit = async () => {
setIsSubmitting(true);
try { try {
const id = await stategrafikBerdasarkanResponden.create.create(); const id = await stategrafikBerdasarkanResponden.create.create();
if (typeof id !== 'undefined') { if (typeof id !== 'undefined') {
@@ -45,13 +45,15 @@ function RespondenCreate() {
} }
} }
resetForm(); resetForm();
router.push("/admin/ppid/ikm-desa-darmasaba/responden"); router.push("/admin/ppid/indeks-kepuasan-masyarakat/responden");
} catch (error) { } catch (error) {
console.error('Error submitting form:', error); console.error('Error submitting form:', error);
} finally {
setIsSubmitting(false);
} }
} }
return ( return (
<Box px={{ base: 0, md: 'lg' }} py="xs"> <Box>
<Box mb={10}> <Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}> <Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack size={20} /> <IconArrowBack size={20} />
@@ -132,13 +134,32 @@ function RespondenCreate() {
} }
disabled={indeksKepuasanState.kelompokUmurResponden.findMany.loading} disabled={indeksKepuasanState.kelompokUmurResponden.findMany.loading}
/> />
<Button <Group justify="right">
mt={10} {/* Tombol Batal */}
bg={colors['blue-button']} <Button
onClick={handleSubmit} variant="outline"
> color="gray"
Submit radius="md"
</Button> size="md"
onClick={resetForm}
>
Reset
</Button>
{/* Tombol Simpan */}
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>

View File

@@ -1,4 +1,8 @@
'use client'; 'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { import {
Box, Box,
Button, Button,
@@ -16,10 +20,7 @@ import {
Text, Text,
Title, Title,
} from '@mantine/core'; } from '@mantine/core';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import indeksKepuasanState from '../../../_state/landing-page/indeks-kepuasan'; import indeksKepuasanState from '../../../_state/landing-page/indeks-kepuasan';
@@ -28,7 +29,7 @@ function Responden() {
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
title="Responden" title="Data Responden"
placeholder="Cari nama responden..." placeholder="Cari nama responden..."
searchIcon={<IconSearch size={18} />} searchIcon={<IconSearch size={18} />}
value={search} value={search}
@@ -46,193 +47,182 @@ interface ListRespondenProps {
function ListResponden({ search }: ListRespondenProps) { function ListResponden({ search }: ListRespondenProps) {
const state = useProxy(indeksKepuasanState.responden); const state = useProxy(indeksKepuasanState.responden);
const router = useRouter(); const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const { data, page, totalPages, loading, load } = state.findMany; const { data, page, totalPages, loading, load } = state.findMany;
const filteredData = (data || []).filter((item) => { useShallowEffect(() => {
const keyword = search.toLowerCase(); load(page, 10, debouncedSearch);
return item.name.toLowerCase().includes(keyword); }, [page, debouncedSearch]);
});
const filteredData = data || [];
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py="xl"> <Stack py={{ base: 'md', md: 'lg' }}>
<Skeleton height={750} radius="lg" /> <Skeleton height={650} radius="lg" />
</Stack> </Stack>
); );
} }
if (data.length === 0) { if (data.length === 0) {
return ( return (
<Paper withBorder bg="white" p={{ base: 'md', sm: 'lg' }} radius="md" shadow="sm"> <Box py={{ base: 'md', md: 'lg' }}>
<Stack gap="md"> <Paper p={{ base: 'md', md: 'lg' }} radius="lg" shadow="md" withBorder>
<Title order={4} lh={1.2}> <Stack align="center" gap="sm">
Data Responden <Title order={2} lh={1.2}>
</Title> Data Responden
<Box visibleFrom="md"> </Title>
<Table striped withRowBorders> <Text c="dimmed" ta="center" fz={{ base: 'sm', md: 'md' }} lh={1.5}>
<TableThead> Belum ada data responden yang tersedia
<TableTr> </Text>
<TableTh ta="center">No</TableTh> </Stack>
<TableTh>Nama</TableTh> </Paper>
<TableTh>Tanggal</TableTh> </Box>
<TableTh>Jenis Kelamin</TableTh>
<TableTh ta="center">Aksi</TableTh>
</TableTr>
</TableThead>
</Table>
</Box>
<Text c="dimmed" ta="center" py="md" fz={{ base: 'sm', md: 'md' }} lh={1.4}>
Belum ada data responden yang tersedia
</Text>
</Stack>
</Paper>
); );
} }
return ( return (
<Paper withBorder bg="white" p={{ base: 'md', sm: 'lg' }} radius="md" shadow="sm"> <Box>
<Stack gap="md"> <Stack gap={'lg'}>
<Title order={4} lh={1.2}>
Data Responden
</Title>
{/* Desktop Table */} {/* Desktop Table */}
<Box visibleFrom="md"> <Box visibleFrom="md">
<Table striped withRowBorders> <Paper p="lg" radius="lg" shadow="md" withBorder>
<TableThead> <Title order={4} size="lg" mb="md" lh={1.2}>
<TableTr> Daftar Responden
<TableTh w="5%" ta="center"> </Title>
No <Table
</TableTh> striped
<TableTh w="25%">Nama</TableTh> highlightOnHover
<TableTh w="25%">Tanggal</TableTh> withRowBorders
<TableTh w="20%">Jenis Kelamin</TableTh> verticalSpacing="sm"
<TableTh w="15%" ta="center"> >
Aksi <TableThead>
</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length === 0 ? (
<TableTr> <TableTr>
<TableTd colSpan={5}> <TableTh fz="sm" fw={600} w={60}>No</TableTh>
<Text c="dimmed" ta="center" py="md" fz="sm" lh={1.4}> <TableTh fz="sm" fw={600}>Nama</TableTh>
Tidak ada data yang cocok dengan pencarian <TableTh fz="sm" fw={600}>Tanggal</TableTh>
</Text> <TableTh fz="sm" fw={600}>Jenis Kelamin</TableTh>
</TableTd> <TableTh fz="sm" fw={600} w={120}>Aksi</TableTh>
</TableTr> </TableTr>
) : ( </TableThead>
filteredData.map((item, index) => ( <TableTbody>
<TableTr key={item.id}> {filteredData.length === 0 ? (
<TableTd ta="center">{index + 1}</TableTd> <TableTr>
<TableTd>{item.name}</TableTd> <TableTd colSpan={5}>
<TableTd> <Text ta="center" c="dimmed" fz="sm" lh={1.5}>
{item.tanggal ? new Date(item.tanggal).toLocaleDateString('id-ID') : '-'} Tidak ditemukan data dengan kata kunci pencarian
</TableTd> </Text>
<TableTd>{item.jenisKelamin.name}</TableTd>
<TableTd ta="center">
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() =>
router.push(`/admin/ppid/ikm-desa-darmasaba/responden/${item.id}`)
}
>
Detail
</Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
)) ) : (
)} filteredData.map((item, index) => (
</TableTbody> <TableTr key={item.id}>
</Table> <TableTd fz="md" lh={1.5}>{index + 1}</TableTd>
<TableTd fz="md" lh={1.5}>{item.name}</TableTd>
<TableTd fz="md" lh={1.5}>
{item.tanggal
? new Date(item.tanggal).toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric',
})
: '-'}
</TableTd>
<TableTd fz="md" lh={1.5}>{item.jenisKelamin.name}</TableTd>
<TableTd>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImac size={16} />}
onClick={() =>
router.push(
`/admin/ppid/indeks-kepuasan-masyarakat/responden/${item.id}`
)
}
>
Detail
</Button>
</TableTd>
</TableTr>
))
)}
</TableTbody>
</Table>
</Paper>
</Box> </Box>
{/* Mobile Card View */} {/* Mobile Cards */}
<Box hiddenFrom="md"> <Box hiddenFrom="md">
{filteredData.length === 0 ? ( <Stack gap="sm">
<Text c="dimmed" ta="center" py="md" fz="sm" lh={1.4}> <Title order={4} size="md" lh={1.2} px="md">
Tidak ada data yang cocok dengan pencarian Daftar Responden
</Text> </Title>
) : ( {filteredData.length === 0 ? (
<Stack gap="sm"> <Paper p="md" radius="lg" shadow="sm" mx="md">
{filteredData.map((item, index) => ( <Text ta="center" c="dimmed" fz="sm" lh={1.5}>
<Paper key={item.id} withBorder p="sm" radius="md"> Tidak ditemukan data dengan kata kunci pencarian
</Text>
</Paper>
) : (
filteredData.map((item) => (
<Paper key={item.id} p="md" radius="lg" shadow="sm" mx="md">
<Stack gap={'xs'}> <Stack gap={'xs'}>
<Box> <Text fz="sm" c="dimmed" lh={1.4}>Nama</Text>
<Text fz="sm" fw={600} lh={1.4}> <Text fz="md" lh={1.5}>{item.name}</Text>
No
</Text> <Text fz="sm" c="dimmed" lh={1.4}>Tanggal</Text>
<Text fz="sm" fw={500} lh={1.4}> <Text fz="md" lh={1.5}>
{index + 1} {item.tanggal
</Text> ? new Date(item.tanggal).toLocaleDateString('id-ID', {
</Box> day: '2-digit',
<Box> month: 'long',
<Text fz="sm" fw={600} lh={1.4}> year: 'numeric',
Nama })
</Text> : '-'}
<Text fz="sm" fw={500} lh={1.4}> </Text>
{item.name}
</Text> <Text fz="sm" c="dimmed" lh={1.4}>Jenis Kelamin</Text>
</Box> <Text fz="md" lh={1.5}>{item.jenisKelamin.name}</Text>
<Box>
<Text fz="sm" fw={600} lh={1.4}> <Button
Tanggal size="xs"
</Text> radius="md"
<Text fz="sm" fw={500} lh={1.4}> variant="light"
{item.tanggal color="blue"
? new Date(item.tanggal).toLocaleDateString('id-ID') leftSection={<IconDeviceImac size={16} />}
: '-'} onClick={() =>
</Text> router.push(
</Box> `/admin/ppid/indeks-kepuasan-masyarakat/responden/${item.id}`
<Box> )
<Text fz="sm" fw={600} lh={1.4}> }
Jenis Kelamin mt="xs"
</Text> >
<Text fz="sm" fw={500} lh={1.4}> Detail
{item.jenisKelamin.name} </Button>
</Text>
</Box>
<Box ta="center">
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() =>
router.push(`/admin/ppid/ikm-desa-darmasaba/responden/${item.id}`)
}
>
Detail
</Button>
</Box>
</Stack> </Stack>
</Paper> </Paper>
))} ))
</Stack> )}
)} </Stack>
</Box> </Box>
{filteredData.length > 0 && ( <Center>
<Center> <Pagination
<Pagination value={page}
value={page} total={totalPages}
total={totalPages} onChange={(newPage) => {
onChange={(newPage) => { load(newPage, 10);
load(newPage, 10); window.scrollTo({ top: 0, behavior: 'smooth' });
window.scrollTo({ top: 0, behavior: 'smooth' }); }}
}} size="md"
size="md" radius="md"
radius="md" mt={{ base: 'md', md: 'lg' }}
/> />
</Center> </Center>
)}
</Stack> </Stack>
</Paper> </Box>
); );
} }

View File

@@ -1,155 +0,0 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import { IconBrush, IconForms, IconUser } from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
function LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter();
const pathname = usePathname();
const tabs = [
{
label: "User",
value: "user",
href: "/admin/user&role/user",
icon: <IconUser size={18} stroke={1.8} />,
},
{
label: "Role",
value: "role",
href: "/admin/user&role/role",
icon: <IconForms size={18} stroke={1.8} />,
},
{
label: "Menu Access",
value: "menu-access",
href: "/admin/user&role/menu-access",
icon: <IconBrush size={18} stroke={1.8} />,
}
];
const currentTab = tabs.find(tab => tab.href === pathname);
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
const handleTabChange = (value: string | null) => {
const tab = tabs.find(t => t.value === value);
if (tab) {
router.push(tab.href);
}
setActiveTab(value);
};
useEffect(() => {
const match = tabs.find(tab => tab.href === pathname);
if (match) {
setActiveTab(match.value);
}
}, [pathname]);
return (
<Stack gap="lg">
<Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
User & Role
</Title>
<Tabs
color={colors['blue-button']}
variant="pills"
value={activeTab}
onChange={handleTabChange}
radius="lg"
keepMounted={false}
>
<Box visibleFrom='md' pb={10}>
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
}}
>
{tabs.map((tab, i) => (
<TabsTab
key={i}
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
}}
>
{tab.label}
</TabsTab>
))}
</TabsList>
</ScrollArea>
</Box>
<Box hiddenFrom='md' pb={10}>
<ScrollArea
type="auto"
offsetScrollbars={false}
w="100%"
>
<TabsList
p="xs" // lebih kecil
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
width: "max-content", // ⬅️ kunci
maxWidth: "100%", // ⬅️ penting
}}
>
{tabs.map((tab, i) => (
<TabsTab
key={i}
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
paddingInline: "0.75rem", // ⬅️ lebih ramping
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
}}
>
{tab.label}
</TabsTab>
))}
</TabsList>
</ScrollArea>
</Box>
{tabs.map((tab, i) => (
<TabsPanel
key={i}
value={tab.value}
style={{
padding: "1.5rem",
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
borderRadius: "1rem",
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
}}
>
{children}
</TabsPanel>
))}
</Tabs>
</Stack>
);
}
export default LayoutTabs;

View File

@@ -1,10 +1,11 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { import {
Box, Box,
Button, Button,
Center,
Group, Group,
Pagination,
Paper, Paper,
Skeleton, Skeleton,
Stack, Stack,
@@ -17,9 +18,10 @@ import {
Text, Text,
Title Title
} from '@mantine/core'; } from '@mantine/core';
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 { useState } from 'react';
import { useProxy } from 'valtio/utils'; 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';
@@ -46,10 +48,12 @@ function ListRole({ 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); // 500ms delay
const { data, page, totalPages, loading, load } = listDataState.findMany;
useEffect(() => { useShallowEffect(() => {
listDataState.findMany.load(); load(page, 10, debouncedSearch);
}, []); }, [page, debouncedSearch]);
const handleDelete = () => { const handleDelete = () => {
if (selectedId) { if (selectedId) {
@@ -65,13 +69,14 @@ function ListRole({ search }: { search: string }) {
return item.name.toLowerCase().includes(keyword); return item.name.toLowerCase().includes(keyword);
}); });
if (!listDataState.findMany.data) { if (loading || !data) {
return ( return (
<Stack py={{ base: 'sm', md: 'md' }}> <Stack py={20}>
<Skeleton h={500} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
); );
} }
return ( return (
<Box py={{ base: 'sm', md: 'md' }}> <Box py={{ base: 'sm', md: 'md' }}>
@@ -199,6 +204,18 @@ function ListRole({ search }: { search: string }) {
</Box> </Box>
</Paper> </Paper>
<Center mt="md">
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
color="blue"
/>
</Center>
{/* Modal Konfirmasi Hapus */} {/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}

View File

@@ -14,23 +14,23 @@ export const devBar = [
name: "Desa Anti Korupsi", name: "Desa Anti Korupsi",
path: "/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi" path: "/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi"
}, },
// {
// id: "Landing_Page_3",
// name: "Indeks Kepuasan Masyarakat",
// path: "/admin/landing-page/indeks-kepuasan-masyarakat/grafik-kepuasan-masyarakat"
// },
{ {
id: "Landing_Page_3", id: "Landing_Page_3",
name: "Indeks Kepuasan Masyarakat",
path: "/admin/landing-page/indeks-kepuasan-masyarakat/grafik-kepuasan-masyarakat"
},
{
id: "Landing_Page_4",
name: "SDGs", name: "SDGs",
path: "/admin/landing-page/SDGs" path: "/admin/landing-page/SDGs"
}, },
{ {
id: "Landing_Page_5", id: "Landing_Page_4",
name: "APBDes", name: "APBDes",
path: "/admin/landing-page/apbdes" path: "/admin/landing-page/apbdes"
}, },
{ {
id: "Landing_Page_6", id: "Landing_Page_5",
name: "Prestasi Desa", name: "Prestasi Desa",
path: "/admin/landing-page/prestasi-desa/list-prestasi-desa" path: "/admin/landing-page/prestasi-desa/list-prestasi-desa"
} }
@@ -354,7 +354,7 @@ export const devBar = [
{ {
id: "Pendidikan_3", id: "Pendidikan_3",
name: "Program Pendidikan Anak", name: "Program Pendidikan Anak",
path: "/admin/pendidikan/program-pendidikan-anak/program-unggulan" path: "/admin/pendidikan/program-pendidikan-anak/tujuan-program"
}, },
{ {
id: "Pendidikan_4", id: "Pendidikan_4",
@@ -418,23 +418,23 @@ export const navBar = [
name: "Desa Anti Korupsi", name: "Desa Anti Korupsi",
path: "/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi" path: "/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi"
}, },
// {
// id: "Landing_Page_3",
// name: "Indeks Kepuasan Masyarakat",
// path: "/admin/landing-page/indeks-kepuasan-masyarakat/grafik-kepuasan-masyarakat"
// },
{ {
id: "Landing_Page_3", id: "Landing_Page_3",
name: "Indeks Kepuasan Masyarakat",
path: "/admin/landing-page/indeks-kepuasan-masyarakat/grafik-kepuasan-masyarakat"
},
{
id: "Landing_Page_4",
name: "SDGs", name: "SDGs",
path: "/admin/landing-page/SDGs" path: "/admin/landing-page/SDGs"
}, },
{ {
id: "Landing_Page_5", id: "Landing_Page_4",
name: "APBDes", name: "APBDes",
path: "/admin/landing-page/apbdes" path: "/admin/landing-page/apbdes"
}, },
{ {
id: "Landing_Page_6", id: "Landing_Page_5",
name: "Prestasi Desa", name: "Prestasi Desa",
path: "/admin/landing-page/prestasi-desa/list-prestasi-desa" path: "/admin/landing-page/prestasi-desa/list-prestasi-desa"
} }
@@ -758,7 +758,7 @@ export const navBar = [
{ {
id: "Pendidikan_3", id: "Pendidikan_3",
name: "Program Pendidikan Anak", name: "Program Pendidikan Anak",
path: "/admin/pendidikan/program-pendidikan-anak/program-unggulan" path: "/admin/pendidikan/program-pendidikan-anak/tujuan-program"
}, },
{ {
id: "Pendidikan_4", id: "Pendidikan_4",
@@ -822,23 +822,23 @@ export const role1 = [
name: "Desa Anti Korupsi", name: "Desa Anti Korupsi",
path: "/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi" path: "/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi"
}, },
// {
// id: "Landing_Page_3",
// name: "Indeks Kepuasan Masyarakat",
// path: "/admin/landing-page/indeks-kepuasan-masyarakat/grafik-kepuasan-masyarakat"
// },
{ {
id: "Landing_Page_3", id: "Landing_Page_3",
name: "Indeks Kepuasan Masyarakat",
path: "/admin/landing-page/indeks-kepuasan-masyarakat/grafik-kepuasan-masyarakat"
},
{
id: "Landing_Page_4",
name: "SDGs", name: "SDGs",
path: "/admin/landing-page/SDGs" path: "/admin/landing-page/SDGs"
}, },
{ {
id: "Landing_Page_5", id: "Landing_Page_4",
name: "APBDes", name: "APBDes",
path: "/admin/landing-page/apbdes" path: "/admin/landing-page/apbdes"
}, },
{ {
id: "Landing_Page_6", id: "Landing_Page_5",
name: "Prestasi Desa", name: "Prestasi Desa",
path: "/admin/landing-page/prestasi-desa/list-prestasi-desa" path: "/admin/landing-page/prestasi-desa/list-prestasi-desa"
} }
@@ -1170,7 +1170,7 @@ export const role3 = [
{ {
id: "Pendidikan_3", id: "Pendidikan_3",
name: "Program Pendidikan Anak", name: "Program Pendidikan Anak",
path: "/admin/pendidikan/program-pendidikan-anak/program-unggulan" path: "/admin/pendidikan/program-pendidikan-anak/tujuan-program"
}, },
{ {
id: "Pendidikan_4", id: "Pendidikan_4",

View File

@@ -7,20 +7,20 @@ async function kategoriProdukFindMany(context: Context) {
// Ambil parameter dari query // Ambil parameter dari query
const page = Number(context.query.page) || 1; const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10; const limit = Number(context.query.limit) || 10;
const search2 = (context.query.search as string) || ''; const search = (context.query.search as string) || '';
const skip = (page - 1) * limit; const skip = (page - 1) * limit;
// Buat where clause // Buat where clause
const where: any = { isActive: true }; const where: any = { isActive: true };
// Tambahkan pencarian (jika ada) // Tambahkan pencarian (jika ada)
if (search2) { if (search) {
where.OR = [ where.OR = [
{ nama: { contains: search2, mode: 'insensitive' } }, { nama: { contains: search, mode: 'insensitive' } },
{KategoriToPasar : { {KategoriToPasar : {
some: { some: {
kategori: { kategori: {
nama: { contains: search2, mode: 'insensitive' } nama: { contains: search, mode: 'insensitive' }
} }
} }
}} }}

View File

@@ -5,19 +5,19 @@ import { Context } from "elysia";
async function kategoriProdukFindManyAll(context: Context) { async function kategoriProdukFindManyAll(context: Context) {
// Ambil query search (opsional) // Ambil query search (opsional)
const search2 = (context.query.search as string) || ""; const search = (context.query.search as string) || "";
// Buat where clause // Buat where clause
const where: any = { isActive: true }; const where: any = { isActive: true };
if (search2) { if (search) {
where.OR = [ where.OR = [
{ nama: { contains: search2, mode: "insensitive" } }, { nama: { contains: search, mode: "insensitive" } },
{ {
KategoriToPasar: { KategoriToPasar: {
some: { some: {
kategori: { kategori: {
nama: { contains: search2, mode: "insensitive" }, nama: { contains: search, mode: "insensitive" },
}, },
}, },
}, },

View File

@@ -1,48 +1,72 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { Context } from "elysia"; import { Context } from "elysia";
// Di findMany.ts
export default async function pegawaiFindMany(context: Context) { export default async function pegawaiFindMany(context: Context) {
const page = Number(context.query.page) || 1; const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10; const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || "";
const skip = (page - 1) * limit; const skip = (page - 1) * limit;
const isActiveParam = context.query.isActive;
// where clause dinamis
const where: any = {};
if (isActiveParam !== undefined) {
where.isActive = isActiveParam === "true";
}
if (search) {
where.OR = [
{ namaLengkap: { contains: search, mode: "insensitive" } },
{ alamat: { contains: search, mode: "insensitive" } },
{ posisi: { nama: { contains: search, mode: "insensitive" } } },
];
}
try { try {
const [data, total] = await Promise.all([ // Ambil semua data terlebih dahulu (tanpa pagination)
const [allData, total] = await Promise.all([
prisma.pegawaiBumDes.findMany({ prisma.pegawaiBumDes.findMany({
where: { isActive: true }, where,
include: { include: {
posisi: true, posisi: true,
image: true, image: true,
}, },
skip,
take: limit,
orderBy: { createdAt: 'desc' },
}), }),
prisma.pegawaiBumDes.count({ prisma.pegawaiBumDes.count({ where }),
where: { isActive: true }
})
]); ]);
// Sort manual berdasarkan hierarki posisi
const sortedData = allData.sort((a, b) => {
// Sort berdasarkan hierarki terlebih dahulu
if (a.posisi.hierarki !== b.posisi.hierarki) {
return a.posisi.hierarki - b.posisi.hierarki;
}
// Jika hierarki sama, sort berdasarkan nama posisi
return a.posisi.nama.localeCompare(b.posisi.nama);
});
// Lakukan pagination manual setelah sorting
const paginatedData = sortedData.slice(skip, skip + limit);
const totalPages = Math.ceil(total / limit); const totalPages = Math.ceil(total / limit);
return { return {
success: true, success: true,
message: "Success fetch pegawai with pagination", message: "Success fetch pegawai with hierarchy order",
data, data: paginatedData,
page, page,
totalPages, totalPages,
total, total,
}; };
} catch (e) { } catch (error) {
console.error("Find many paginated error:", e); console.error("Find many pegawai error:", error);
return { return {
success: false, success: false,
message: "Failed fetch pegawai with pagination", message: "Failed fetch pegawai",
data: [], data: [],
page: 1, page: 1,
totalPages: 1, totalPages: 1,
total: 0, total: 0,
}; };
} }
} }

View File

@@ -0,0 +1,48 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function pegawaiFindManyAll(context: Context) {
const search = (context.query.search as string) || "";
const isActiveParam = context.query.isActive;
// Buat where clause dinamis
const where: any = {};
if (isActiveParam !== undefined) {
where.isActive = isActiveParam === "true";
}
if (search) {
where.OR = [
{ namaLengkap: { contains: search, mode: "insensitive" } },
{ alamat: { contains: search, mode: "insensitive" } },
];
}
try {
const data = await prisma.pegawaiBumDes.findMany({
where,
include: {
posisi: true,
image: true,
},
orderBy: { posisi: { hierarki: "asc" } },
});
return {
success: true,
message: "Success fetch all pegawai (non-paginated)",
total: data.length,
data,
};
} catch (error) {
console.error("Find many all error:", error);
return {
success: false,
message: "Failed fetch all pegawai",
total: 0,
data: [],
};
}
}

View File

@@ -1,8 +1,49 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function dataPendidikanFindMany() { export default async function dataPendidikanFindMany(context: Context) {
const res = await prisma.dataPendidikan.findMany(); const page = Number(context.query.page) || 1;
return { const limit = Number(context.query.limit) || 10;
data: res, const search = (context.query.search as string) || '';
}; const skip = (page - 1) * limit;
}
const where: any = { isActive: true };
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ name: { contains: search, mode: 'insensitive' } },
{ jumlah: { contains: search, mode: 'insensitive' } },
];
}
try {
const [data, total] = await Promise.all([
prisma.dataPendidikan.findMany({
where,
skip,
take: limit,
orderBy: { name: "asc" }, // opsional, kalau mau urut berdasarkan waktu
}),
prisma.dataPendidikan.count({
where: { isActive: true },
}),
]);
return {
success: true,
message: "Success fetch program inovasi with pagination",
data,
page,
totalPages: Math.ceil(total / limit),
total,
};
} catch (e) {
console.error("Find many paginated error:", e);
return {
success: false,
message: "Failed fetch program inovasi with pagination",
};
}
}

View File

@@ -1,11 +1,49 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function roleFindMany() { export default async function roleFindMany(context: Context) {
const data = await prisma.role.findMany(); const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || '';
const skip = (page - 1) * limit;
return { const where: any = { isActive: true };
success: true,
message: "Success get all role", // Tambahkan pencarian (jika ada)
data, if (search) {
}; where.OR = [
{ name: { contains: search, mode: 'insensitive' } },
{ description: { contains: search, mode: 'insensitive' } },
];
}
try {
const [data, total] = await Promise.all([
prisma.role.findMany({
where,
skip,
take: limit,
orderBy: { name: "asc" }, // opsional, kalau mau urut berdasarkan waktu
}),
prisma.role.count({
where: { isActive: true },
}),
]);
return {
success: true,
message: "Success fetch role with pagination",
data,
page,
totalPages: Math.ceil(total / limit),
total,
};
} catch (e) {
console.error("Find many paginated error:", e);
return {
success: false,
message: "Failed fetch role with pagination",
};
}
} }