Merge pull request 'Fix All Search Admin' (#48) from nico/5-jan-26 into staggingweb

Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/48
This commit is contained in:
2026-01-05 17:12:29 +08:00
39 changed files with 872 additions and 694 deletions

View File

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

View File

@@ -194,7 +194,7 @@ const posisiOrganisasi = proxy({
try {
this.loading = true;
const res = await ApiFetch.api.ekonomi['struktur-organisasi']['posisi-organisasi']['create'].post(this.form);
const res = await ApiFetch.api.ekonomi["struktur-organisasi"]["posisi-organisasi"]["create"].post(this.form);
if (res.status === 200) {
toast.success("Berhasil menambahkan posisi organisasi");
posisiOrganisasi.findMany.load();

View File

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

View File

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

View File

@@ -220,11 +220,34 @@ const roleState = proxy({
isActive: true;
};
}>[],
page: 1,
totalPages: 1,
loading: false,
async load() {
const res = await ApiFetch.api.role["findMany"].get();
if (res.status === 200) {
roleState.findMany.data = res.data?.data ?? [];
search: "",
load: async (page = 1, limit = 10, search = "") => {
roleState.findMany.loading = true; // ✅ Akses langsung via nama path
roleState.findMany.page = page;
roleState.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.role["findMany"].get({ query });
if (res.status === 200 && res.data?.success) {
roleState.findMany.data = res.data.data ?? [];
roleState.findMany.totalPages = res.data.totalPages ?? 1;
} else {
roleState.findMany.data = [];
roleState.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch role paginated:", err);
roleState.findMany.data = [];
roleState.findMany.totalPages = 1;
} finally {
roleState.findMany.loading = false;
}
},
},

View File

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

View File

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

View File

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

View File

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

View File

@@ -166,7 +166,7 @@ function ListAPBDesa({ search }: { search: string }) {
<TableTd>
<Button
variant="light"
color="green"
color="blue"
onClick={() =>
router.push(
`/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/${item.id}`
@@ -243,7 +243,7 @@ function ListAPBDesa({ search }: { search: string }) {
</Box>
<Button
variant="light"
color="green"
color="blue"
fullWidth
onClick={() =>
router.push(

View File

@@ -128,10 +128,18 @@ function ListBelanja({ search }: { search: string }) {
>
<TableThead>
<TableTr>
<TableTh style={{ width: '35%' }}>Nama</TableTh>
<TableTh style={{ width: '25%' }}>Nilai</TableTh>
<TableTh style={{ width: '20%' }}>Persentase</TableTh>
<TableTh style={{ width: '20%' }}>Aksi</TableTh>
<TableTh style={{ width: '40%' }}>
<Text fz="sm" fw={600} lh={1.4}>Nama</Text>
</TableTh>
<TableTh style={{ width: '25%' }}>
<Text fz="sm" fw={600} lh={1.4}>Nilai</Text>
</TableTh>
<TableTh style={{ width: '15%' }}>
<Text fz="sm" fw={600} lh={1.4}>Edit</Text>
</TableTh>
<TableTh style={{ width: '20%' }}>
<Text fz="sm" fw={600} lh={1.4}>Delete</Text>
</TableTh>
</TableTr>
</TableThead>
<TableTbody>

View File

@@ -120,12 +120,20 @@ function ListPembiayaan({ search }: { search: string }) {
width: '100%',
}}
>
<TableThead>
<TableTr>
<TableTh style={{ width: '35%' }}>Nama</TableTh>
<TableTh style={{ width: '25%' }}>Nilai</TableTh>
<TableTh style={{ width: '20%' }}>Persentase</TableTh>
<TableTh style={{ width: '20%' }}>Aksi</TableTh>
<TableThead>
<TableTr>
<TableTh style={{ width: '40%' }}>
<Text fz="sm" fw={600} lh={1.4}>Nama</Text>
</TableTh>
<TableTh style={{ width: '25%' }}>
<Text fz="sm" fw={600} lh={1.4}>Nilai</Text>
</TableTh>
<TableTh style={{ width: '15%' }}>
<Text fz="sm" fw={600} lh={1.4}>Edit</Text>
</TableTh>
<TableTh style={{ width: '20%' }}>
<Text fz="sm" fw={600} lh={1.4}>Delete</Text>
</TableTh>
</TableTr>
</TableThead>
<TableTbody>

View File

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

View File

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

View File

@@ -21,7 +21,7 @@ import {
Text,
Title
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
@@ -59,6 +59,8 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
const [chartData, setChartData] = useState<DemografiPekerjaan[]>([]);
const [mounted, setMounted] = useState(false);
const [modalHapus, setModalHapus] = useState(false);
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const [selectedId, setSelectedId] = useState<string | null>(null);
const {
@@ -79,8 +81,8 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
useShallowEffect(() => {
setMounted(true);
load(page, 10, search);
}, [page, search]);
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
useEffect(() => {
if (data) {

View File

@@ -28,27 +28,27 @@ import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import pasarDesaState from '../../../_state/ekonomi/pasar-desa/pasar-desa';
function KategoriProduk() {
const [search2, setSearch2] = useState('');
const [search, setSearch] = useState('');
return (
<Box>
<HeaderSearch
title='Kategori Produk'
placeholder='Cari nama kategori produk...'
searchIcon={<IconSearch size={20} />}
value={search2}
onChange={(e) => setSearch2(e.currentTarget.value)}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListKategoriProduk search2={search2} />
<ListKategoriProduk search={search} />
</Box>
);
}
function ListKategoriProduk({ search2 }: { search2: string }) {
function ListKategoriProduk({ search }: { search: string }) {
const statePasar = useProxy(pasarDesaState.kategoriProduk);
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search2, 1000);
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { data, page, totalPages, loading, load } = statePasar.findMany;

View File

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

View File

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

View File

@@ -38,7 +38,7 @@ function ListTarifLayanan({ search }: { search: string }) {
const router = useRouter();
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const [debouncedSearch] = useDebouncedValue(search, 10000);
const [debouncedSearch] = useDebouncedValue(search, 1000);
const {
data,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -29,7 +29,7 @@ function Page() {
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
<Stack gap="md">
{/* Header */}
<Group align="center">
<Group justify='space-between' align="center">
<Title order={3} c={colors['blue-button']}>
Pratinjau Program Unggulan
</Title>

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

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

View File

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