nico/17-des-25 #43

Merged
nicoarya20 merged 1 commits from nico/17-des-25 into staggingweb 2025-12-17 17:39:30 +08:00
44 changed files with 2028 additions and 833 deletions

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";
@@ -136,12 +137,43 @@ const statepermohonanInformasiPublik = proxy({
}; };
}>[] }>[]
| null, | null,
async load() { page: 1,
const res = await ApiFetch.api.ppid.permohonaninformasipublik[ totalPages: 1,
"find-many" total: 0,
].get(); loading: false,
if (res.status === 200) { search: "",
statepermohonanInformasiPublik.findMany.data = res.data?.data ?? []; load: async (page = 1, limit = 10, search = "") => {
// Change to arrow function
statepermohonanInformasiPublik.findMany.loading = true; // Use the full path to access the property
statepermohonanInformasiPublik.findMany.page = page;
statepermohonanInformasiPublik.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.ppid.permohonaninformasipublik[
"find-many"
].get({
query,
});
if (res.status === 200 && res.data?.success) {
statepermohonanInformasiPublik.findMany.data = res.data.data || [];
statepermohonanInformasiPublik.findMany.total = res.data.total || 0;
statepermohonanInformasiPublik.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error("Failed to load permohonan keberatan informasi:", res.data?.message);
statepermohonanInformasiPublik.findMany.data = [];
statepermohonanInformasiPublik.findMany.total = 0;
statepermohonanInformasiPublik.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading permohonan keberatan informasi:", error);
statepermohonanInformasiPublik.findMany.data = [];
statepermohonanInformasiPublik.findMany.total = 0;
statepermohonanInformasiPublik.findMany.totalPages = 1;
} finally {
statepermohonanInformasiPublik.findMany.loading = false;
} }
}, },
}, },

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";
@@ -57,17 +58,48 @@ const permohonanKeberatanInformasi = proxy({
}, },
}, },
findMany: { findMany: {
data: null as data: null as
| null
| Prisma.FormulirPermohonanKeberatanGetPayload<{ | Prisma.FormulirPermohonanKeberatanGetPayload<{
omit: { isActive: true }; omit: { isActive: true };
}>[] }>[],
| null, page: 1,
async load() { totalPages: 1,
const res = await ApiFetch.api.ppid.permohonankeberataninformasipublik[ total: 0,
"find-many" loading: false,
].get(); search: "",
if (res.status === 200) { load: async (page = 1, limit = 10, search = "") => {
permohonanKeberatanInformasi.findMany.data = res.data?.data ?? []; // Change to arrow function
permohonanKeberatanInformasi.findMany.loading = true; // Use the full path to access the property
permohonanKeberatanInformasi.findMany.page = page;
permohonanKeberatanInformasi.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.ppid.permohonankeberataninformasipublik[
"find-many"
].get({
query,
});
if (res.status === 200 && res.data?.success) {
permohonanKeberatanInformasi.findMany.data = res.data.data || [];
permohonanKeberatanInformasi.findMany.total = res.data.total || 0;
permohonanKeberatanInformasi.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error("Failed to load permohonan keberatan informasi:", res.data?.message);
permohonanKeberatanInformasi.findMany.data = [];
permohonanKeberatanInformasi.findMany.total = 0;
permohonanKeberatanInformasi.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading permohonan keberatan informasi:", error);
permohonanKeberatanInformasi.findMany.data = [];
permohonanKeberatanInformasi.findMany.total = 0;
permohonanKeberatanInformasi.findMany.totalPages = 1;
} finally {
permohonanKeberatanInformasi.findMany.loading = false;
} }
}, },
}, },

View File

@@ -4,7 +4,7 @@ import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
@@ -108,7 +108,7 @@ function DetailPerbekelDariMasa() {
radius="md" radius="md"
size="md" size="md"
> >
<IconX size={20} /> <IconTrash size={20} />
</Button> </Button>
<Button <Button

View File

@@ -1,7 +1,7 @@
'use client' 'use client'
import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
@@ -106,7 +106,7 @@ function DetailSDGSDesa() {
size="md" size="md"
disabled={sdgsState.delete.loading} disabled={sdgsState.delete.loading}
> >
<IconX size={20} /> <IconTrash size={20} />
</Button> </Button>
<Button <Button

View File

@@ -1,7 +1,7 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import { 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 { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -28,6 +28,8 @@ function SdgsDesa() {
function ListSdgsDesa({ search }: { search: string }) { function ListSdgsDesa({ search }: { search: string }) {
const listState = useProxy(sdgsDesa); const listState = useProxy(sdgsDesa);
const router = useRouter(); const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { const {
data, data,
@@ -38,8 +40,8 @@ function ListSdgsDesa({ search }: { search: string }) {
} = listState.findMany; } = listState.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search); load(page, 10, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
const filteredData = data || []; const filteredData = data || [];
@@ -63,7 +65,7 @@ function ListSdgsDesa({ search }: { search: string }) {
</Title> </Title>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color={colors['blue-button']} color='blue'
variant="light" variant="light"
onClick={() => router.push('/admin/landing-page/SDGs/create')} onClick={() => router.push('/admin/landing-page/SDGs/create')}
> >
@@ -147,12 +149,18 @@ function ListSdgsDesa({ search }: { search: string }) {
{filteredData.map((item) => ( {filteredData.map((item) => (
<Paper key={item.id} withBorder p="md" radius="md"> <Paper key={item.id} withBorder p="md" radius="md">
<Stack gap={4}> <Stack gap={4}>
<Text fz="sm" fw={600} lh={1.4}> <Box>
{item.name} <Text fz="sm" fw={600} lh={1.4}>Nama SDGs Desa</Text>
</Text> <Text fz="sm" fw={500} lh={1.4}>
<Text fz="xs" c="dark.6" lh={1.4}> {item.name}
Jumlah: {item.jumlah || '0'} </Text>
</Text> </Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Jumlah</Text>
<Text fz="xs" c="dark.6" lh={1.4}>
{item.jumlah || '0'}
</Text>
</Box>
<Group justify="flex-end" mt="xs"> <Group justify="flex-end" mt="xs">
<Button <Button
size="xs" size="xs"

View File

@@ -368,6 +368,13 @@ function EditAPBDes() {
{ value: '2', label: 'Level 2 (Sub-kelompok)' }, { value: '2', label: 'Level 2 (Sub-kelompok)' },
{ value: '3', label: 'Level 3 (Detail)' }, { value: '3', label: 'Level 3 (Detail)' },
]} ]}
styles={{
option: {
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
},
}}
value={String(newItem.level)} value={String(newItem.level)}
onChange={(val) => setNewItem({ ...newItem, level: Number(val) || 1 })} onChange={(val) => setNewItem({ ...newItem, level: Number(val) || 1 })}
/> />
@@ -378,6 +385,13 @@ function EditAPBDes() {
{ value: 'belanja', label: 'Belanja' }, { value: 'belanja', label: 'Belanja' },
{ value: 'pembiayaan', label: 'Pembiayaan' }, { value: 'pembiayaan', label: 'Pembiayaan' },
]} ]}
styles={{
option: {
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
},
}}
value={newItem.tipe} value={newItem.tipe}
onChange={(val) => setNewItem({ ...newItem, tipe: (val as any) || 'pendapatan' })} onChange={(val) => setNewItem({ ...newItem, tipe: (val as any) || 'pendapatan' })}
/> />

View File

@@ -353,6 +353,13 @@ function CreateAPBDes() {
{ value: '2', label: 'Level 2 (Sub-kelompok)' }, { value: '2', label: 'Level 2 (Sub-kelompok)' },
{ value: '3', label: 'Level 3 (Detail)' }, { value: '3', label: 'Level 3 (Detail)' },
]} ]}
styles={{
option: {
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
},
}}
value={String(newItem.level)} value={String(newItem.level)}
onChange={(val) => setNewItem({ ...newItem, level: Number(val) || 1 })} onChange={(val) => setNewItem({ ...newItem, level: Number(val) || 1 })}
/> />
@@ -363,6 +370,13 @@ function CreateAPBDes() {
{ value: 'belanja', label: 'Belanja' }, { value: 'belanja', label: 'Belanja' },
{ value: 'pembiayaan', label: 'Pembiayaan' }, { value: 'pembiayaan', label: 'Pembiayaan' },
]} ]}
styles={{
option: {
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
},
}}
value={newItem.level === 1 ? null : newItem.tipe} value={newItem.level === 1 ? null : newItem.tipe}
onChange={(val) => setNewItem({ ...newItem, tipe: val as any })} onChange={(val) => setNewItem({ ...newItem, tipe: val as any })}
disabled={newItem.level === 1} disabled={newItem.level === 1}

View File

@@ -18,7 +18,7 @@ import {
Text, Text,
Title, Title,
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconFile, IconPlus, IconSearch } from '@tabler/icons-react'; import { IconDeviceImacCog, IconFile, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -45,12 +45,13 @@ function APBDes() {
function ListAPBDes({ search }: { search: string }) { function ListAPBDes({ search }: { search: string }) {
const listState = useProxy(apbdes); const listState = useProxy(apbdes);
const router = useRouter(); const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { data, page, totalPages, loading, load } = listState.findMany; const { data, page, totalPages, loading, load } = listState.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search); load(page, 10, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
const filteredData = data || []; const filteredData = data || [];
@@ -197,17 +198,17 @@ function ListAPBDes({ search }: { search: string }) {
<Text fz="sm" fw={600} lh={1.4}> <Text fz="sm" fw={600} lh={1.4}>
APBDes {item.tahun} APBDes {item.tahun}
</Text> </Text>
<Group justify="space-between" wrap="nowrap"> <Box>
<Text fz="sm" c="dimmed" lh={1.4}> <Text fz="sm"fw={600} lh={1.4}>
Tahun Tahun
</Text> </Text>
<Text fz="sm" fw={500} lh={1.4}> <Text fz="sm" fw={500} lh={1.4}>
{item.tahun || '-'} {item.tahun || '-'}
</Text> </Text>
</Group> </Box>
<Group justify="space-between" wrap="nowrap"> <Box>
<Text fz="sm" c="dimmed" lh={1.4}> <Text fz="sm"fw={600} lh={1.4}>
Dokumen Dokumen
</Text> </Text>
{item.file?.link ? ( {item.file?.link ? (
@@ -230,7 +231,7 @@ function ListAPBDes({ search }: { search: string }) {
Tidak ada Tidak ada
</Text> </Text>
)} )}
</Group> </Box>
<Button <Button
size="xs" size="xs"

View File

@@ -151,7 +151,7 @@ function ListKategoriKegiatan({ search }: { search: string }) {
{item.name} {item.name}
</Text> </Text>
</TableTd> </TableTd>
<TableTd w={60}> <TableTd w={120}>
<Button <Button
variant="light" variant="light"
color="green" color="green"
@@ -161,7 +161,7 @@ function ListKategoriKegiatan({ search }: { search: string }) {
<IconEdit size={18} /> <IconEdit size={18} />
</Button> </Button>
</TableTd> </TableTd>
<TableTd w={60}> <TableTd w={120}>
<Button <Button
variant="light" variant="light"
color="red" color="red"
@@ -191,7 +191,7 @@ function ListKategoriKegiatan({ search }: { search: string }) {
); );
return ( return (
<Box py={{ base: 'xl', md: 'xl' }}> <Box py={{ base: 20, md: 20 }}>
<Paper bg={colors['white-1']} p={{ base: 'md', md: 'xl' }} shadow="md" radius="md"> <Paper bg={colors['white-1']} p={{ base: 'md', md: 'xl' }} shadow="md" radius="md">
<Group justify="space-between" mb={{ base: 'md', md: 'lg' }}> <Group justify="space-between" mb={{ base: 'md', md: 'lg' }}>
<Title order={2} lh={1.2}> <Title order={2} lh={1.2}>

View File

@@ -150,12 +150,18 @@ function ListDesaAntiKorupsi({ search }: { search: string }) {
filteredData.map((item) => ( filteredData.map((item) => (
<Paper key={item.id} p="sm" radius="md" withBorder shadow="xs"> <Paper key={item.id} p="sm" radius="md" withBorder shadow="xs">
<Stack gap="xs"> <Stack gap="xs">
<Text fw={500} fz="sm" lh={1.5} lineClamp={1}> <Box>
<Text fz="sm" fw={600} lh={1.4}>Nama Program</Text>
<Text fw={500} fz="sm" lh={1.5} lineClamp={1}>
{item.name || '-'} {item.name || '-'}
</Text> </Text>
<Text fz="xs" c="dimmed" lh={1.5} lineClamp={1}> </Box>
Kategori: {item.kategori?.name || '-'} <Box>
<Text fz="sm" fw={600} lh={1.4}>Kategori</Text>
<Text fz="xs" c="dimmed" lh={1.5} lineClamp={1}>
{item.kategori?.name || '-'}
</Text> </Text>
</Box>
<Group justify="flex-end"> <Group justify="flex-end">
<Button <Button
size="xs" size="xs"

View File

@@ -1,7 +1,7 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import { 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 { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -32,6 +32,7 @@ function ListKategoriPrestasi({ 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 handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
@@ -50,8 +51,8 @@ function ListKategoriPrestasi({ search }: { search: string }) {
} = stateKategori.findMany } = stateKategori.findMany
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search) load(page, 10, debouncedSearch)
}, [page, search]) }, [page, debouncedSearch])
const filteredData = data || [] const filteredData = data || []

View File

@@ -1,7 +1,7 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { useMediaQuery, useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useMediaQuery, useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -29,6 +29,7 @@ function ListPrestasi({ search }: { search: string }) {
const listState = useProxy(prestasiState.prestasiDesa) const listState = useProxy(prestasiState.prestasiDesa)
const router = useRouter(); const router = useRouter();
const isMobile = useMediaQuery('(max-width: 768px)'); const isMobile = useMediaQuery('(max-width: 768px)');
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const { const {
data, data,
@@ -39,8 +40,8 @@ function ListPrestasi({ search }: { search: string }) {
} = listState.findMany } = listState.findMany
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search); load(page, 10, debouncedSearch);
}, []); }, [page, debouncedSearch]);
const filteredData = data || [] const filteredData = data || []
@@ -71,44 +72,46 @@ function ListPrestasi({ search }: { search: string }) {
</Group> </Group>
{/* Desktop Table */} {/* Desktop Table */}
<Box visibleFrom="md"> <Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table highlightOnHover miw={0}> <Table highlightOnHover striped verticalSpacing="sm" miw={800}>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh w="25%">Nama Prestasi</TableTh> <TableTh>Nama Prestasi</TableTh>
<TableTh w="25%">Deskripsi</TableTh> <TableTh>Deskripsi</TableTh>
<TableTh w="25%">Kategori</TableTh> <TableTh>Kategori</TableTh>
<TableTh w="25%" ta="center">Aksi</TableTh> <TableTh ta="center">Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.length > 0 ? ( {filteredData.length > 0 ? (
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd w="25%"> <TableTd style={{ maxWidth: 250 }}>
<Text truncate="end" fz="md" lh={1.5}> <Text truncate="end" fz="md" lh={1.5}>
{item.name} {item.name}
</Text> </Text>
</TableTd> </TableTd>
<TableTd w="25%"> <TableTd style={{ maxWidth: 250 }}>
<Text lineClamp={1} fz="md" c="dimmed" lh={1.5} dangerouslySetInnerHTML={{ __html: item.deskripsi }} /> <Text lineClamp={1} fz="md" c="dimmed" lh={1.5} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd> </TableTd>
<TableTd w="25%"> <TableTd>
<Text truncate="end" fz="md" lh={1.5}> <Text truncate="end" fz="md" lh={1.5}>
{item.kategori?.name || 'Tidak ada kategori'} {item.kategori?.name || 'Tidak ada kategori'}
</Text> </Text>
</TableTd> </TableTd>
<TableTd w="25%" ta="center"> <TableTd ta="center">
<Button <Center>
size="xs" <Button
radius="md" size="sm"
variant="light" radius="md"
color="blue" variant="light"
leftSection={<IconDeviceImacCog size={16} />} color="blue"
onClick={() => router.push(`/admin/landing-page/prestasi-desa/list-prestasi-desa/${item.id}`)} leftSection={<IconDeviceImacCog size={16} />}
> onClick={() => router.push(`/admin/landing-page/prestasi-desa/list-prestasi-desa/${item.id}`)}
Detail >
</Button> Detail
</Button>
</Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
)) ))
@@ -127,39 +130,48 @@ function ListPrestasi({ search }: { search: string }) {
{/* Mobile Cards */} {/* Mobile Cards */}
<Stack hiddenFrom="md" gap="xs"> <Stack hiddenFrom="md" gap="xs">
{filteredData.length > 0 ? ( {filteredData.length > 0 ? (
filteredData.map((item) => ( filteredData.map((item) => (
<Paper key={item.id} withBorder p="sm" radius="sm"> <Paper key={item.id} withBorder p="sm" radius="sm">
<Stack gap={4}> <Stack gap={"xs"}>
<Text fz="sm" fw={600} lh={1.4}> <Box>
<Text fz="sm" fw={600} lh={1.4}>Nama Prestasi</Text>
<Text fz="sm" fw={500} lh={1.4} lineClamp={2}>
{item.name} {item.name}
</Text> </Text>
<Text fz="xs" c="dimmed" lh={1.5} lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} /> </Box>
<Text fz="xs" c="dimmed" lh={1.4}> <Box>
Kategori: {item.kategori?.name || 'Tidak ada kategori'} <Text fz="sm" fw={600} lh={1.4}>Deskripsi</Text>
<Text fz="sm" fw={500} lh={1.5} lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Kategori</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.kategori?.name || 'Tidak ada kategori'}
</Text> </Text>
<Group justify="flex-end" mt="xs"> </Box>
<Button <Group justify="flex-end" mt="xs">
size="xs" <Button
radius="md" size="xs"
variant="light" radius="md"
color="blue" variant="light"
leftSection={<IconDeviceImacCog size={14} />} color="blue"
onClick={() => router.push(`/admin/landing-page/prestasi-desa/list-prestasi-desa/${item.id}`)} leftSection={<IconDeviceImacCog size={14} />}
> onClick={() => router.push(`/admin/landing-page/prestasi-desa/list-prestasi-desa/${item.id}`)}
Detail >
</Button> Detail
</Group> </Button>
</Stack> </Group>
</Paper> </Stack>
)) </Paper>
) : ( ))
<Center py="md"> ) : (
<Text c="dimmed" fz="sm" lh={1.4}> <Center py="md">
Tidak ada data prestasi <Text c="dimmed" fz="sm" lh={1.4}>
</Text> Tidak ada data prestasi
</Center> </Text>
)} </Center>
)}
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -20,7 +20,7 @@ import {
Text, Text,
Title, Title,
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -48,6 +48,7 @@ function MediaSosial() {
function ListMediaSosial({ search }: { search: string }) { function ListMediaSosial({ search }: { search: string }) {
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial); const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial);
const router = useRouter(); const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const getIconSource = (item: any) => { const getIconSource = (item: any) => {
if (item.image?.link) return item.image.link; if (item.image?.link) return item.image.link;
@@ -66,8 +67,8 @@ function ListMediaSosial({ search }: { search: string }) {
} = stateMediaSosial.findMany; } = stateMediaSosial.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search); load(page, 10, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
const filteredData = data || []; const filteredData = data || [];
@@ -195,12 +196,15 @@ function ListMediaSosial({ search }: { search: string }) {
{filteredData.length > 0 ? ( {filteredData.length > 0 ? (
filteredData.map((item) => ( filteredData.map((item) => (
<Paper key={item.id} withBorder p="sm" radius="md"> <Paper key={item.id} withBorder p="sm" radius="md">
<Group justify="space-between" wrap="nowrap" align='center'>
<Box> <Box>
<Text fw={600} fz="sm" lh={1.45}> <Text fz="sm" fw={600} lh={1.4}>Nama Media Sosial / Kontak</Text>
<Text fw={500} fz="sm" lh={1.45}>
{item.name} {item.name}
</Text> </Text>
</Box> </Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Gambar</Text>
</Box>
<Box w={40} h={40} style={{ borderRadius: 6, overflow: 'hidden' }}> <Box w={40} h={40} style={{ borderRadius: 6, overflow: 'hidden' }}>
{(() => { {(() => {
const src = getIconSource(item); const src = getIconSource(item);
@@ -217,8 +221,8 @@ function ListMediaSosial({ search }: { search: string }) {
return <Box bg={colors['blue-button']} w="100%" h="100%" />; return <Box bg={colors['blue-button']} w="100%" h="100%" />;
})()} })()}
</Box> </Box>
</Group>
<Box> <Box>
<Text fz="sm" fw={600} lh={1.4}>Link / No. Telepon</Text>
<a <a
href={item.link} href={item.link}
target="_blank" target="_blank"

View File

@@ -1,7 +1,7 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core'; import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -29,12 +29,13 @@ function ProgramInovasi() {
function ListProgramInovasi({ search }: { search: string }) { function ListProgramInovasi({ search }: { search: string }) {
const stateProgramInovasi = useProxy(profileLandingPageState.programInovasi); const stateProgramInovasi = useProxy(profileLandingPageState.programInovasi);
const router = useRouter(); const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const { data, page, totalPages, loading, load } = stateProgramInovasi.findMany; const { data, page, totalPages, loading, load } = stateProgramInovasi.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search); load(page, 10, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
const filteredData = data || []; const filteredData = data || [];
@@ -135,15 +136,22 @@ function ListProgramInovasi({ search }: { search: string }) {
> >
<Stack gap={6}> <Stack gap={6}>
{/* Title */} {/* Title */}
<Text fw={600}>{item.name}</Text> <Box>
<Text fz="sm" fw={600} lh={1.4}>Nama Program</Text>
<Text fw={500} lh={1.4}>{item.name}</Text>
</Box>
{/* Description */} {/* Description */}
<Text fz="sm" c="gray.7" lineClamp={2}> <Box>
{item.description || '-'} <Text fz="sm" fw={600} lh={1.4}>Deskripsi</Text>
<Text fz="sm" c="gray.7" lineClamp={2}>
{item.description || '-'}
</Text> </Text>
</Box>
{/* Link */} {/* Link */}
<Box> <Box>
<Text fz="sm" fw={600} lh={1.4}>Link</Text>
<a <a
href={item.link} href={item.link}
target="_blank" target="_blank"

View File

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

View File

@@ -52,7 +52,7 @@ function DetailDaftarInformasiPublik() {
<Paper <Paper
withBorder withBorder
w={{ base: '100%', md: '60%' }} w={{ base: '100%', md: '70%' }}
bg={colors['white-1']} bg={colors['white-1']}
p="lg" p="lg"
radius="md" radius="md"
@@ -83,39 +83,39 @@ function DetailDaftarInformasiPublik() {
<Box> <Box>
<Text fz="lg" fw="bold" mb={4}>Deskripsi</Text> <Text fz="lg" fw="bold" mb={4}>Deskripsi</Text>
<Box <Text
px={"xs"}
fz="md" fz="md"
c="dimmed" c="dimmed"
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }} dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
className="prose max-w-none"
style={{ wordBreak: "break-word", whiteSpace: "normal" }} style={{ wordBreak: "break-word", whiteSpace: "normal" }}
/> />
</Box> </Box>
<Group gap="sm" mt="md"> <Group gap="sm" mt="md">
<Button <Button
variant="light" variant="light"
color="green" color="green"
leftSection={<IconEdit size={18} />} leftSection={<IconEdit size={18} />}
onClick={() => router.push(`/admin/ppid/daftar-informasi-publik/${data.id}/edit`)} onClick={() => router.push(`/admin/ppid/daftar-informasi-publik/${data.id}/edit`)}
disabled={!data} disabled={!data}
> >
Edit Edit
</Button> </Button>
<Button <Button
variant="light" variant="light"
color="red" color="red"
leftSection={<IconTrash size={18} />} leftSection={<IconTrash size={18} />}
onClick={() => { onClick={() => {
setSelectedId(data.id); setSelectedId(data.id);
setModalHapus(true); setModalHapus(true);
}} }}
disabled={stateDaftarInformasi.delete.loading || !data} disabled={stateDaftarInformasi.delete.loading || !data}
loading={stateDaftarInformasi.delete.loading} loading={stateDaftarInformasi.delete.loading}
> >
Hapus Hapus
</Button> </Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>

View File

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

View File

@@ -1,7 +1,24 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import {
import { useShallowEffect, useViewportSize } from '@mantine/hooks'; Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
} from '@mantine/core';
import { useDebouncedValue, useShallowEffect, useViewportSize } from '@mantine/hooks';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -10,7 +27,7 @@ import HeaderSearch from '../../_com/header';
import daftarInformasiPublik from '../../_state/ppid/daftar_informasi_publik/daftarInformasiPublik'; import daftarInformasiPublik from '../../_state/ppid/daftar_informasi_publik/daftarInformasiPublik';
function DaftarInformasiPublik() { function DaftarInformasiPublik() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState('');
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
@@ -26,102 +43,158 @@ function DaftarInformasiPublik() {
} }
function ListDaftarInformasi({ search }: { search: string }) { function ListDaftarInformasi({ search }: { search: string }) {
const listData = useProxy(daftarInformasiPublik) const listData = useProxy(daftarInformasiPublik);
const router = useRouter() const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { data, page, totalPages, loading, load } = listData.findMany const { data, page, totalPages, loading, load } = listData.findMany;
const { width } = useViewportSize() const { width } = useViewportSize();
const isMobile = width < 768 const isMobile = width < 768;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search) load(page, 10, debouncedSearch);
}, [page, search]) }, [page, debouncedSearch]);
const filteredData = data || [] const filteredData = data || [];
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py="md">
<Skeleton height={600} radius="md" /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
) );
} }
return ( return (
<Box py="md"> <Box py={{ base: 'md', md: 'lg' }}>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p={{ base: 'sm', md: 'lg' }} shadow="md" radius="md">
<Group justify="space-between" mb="md"> <Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
<Title order={4}>List Daftar Informasi Publik</Title> <Title order={2} lh={1.2}>
<Button List Daftar Informasi Publik
leftSection={<IconPlus size={18} />} </Title>
color="blue" <Button
variant="light" leftSection={<IconPlus size={18} />}
onClick={() => router.push('/admin/ppid/daftar-informasi-publik/create')} color="blue"
> variant="light"
{isMobile ? 'Tambah' : 'Tambah Baru'} onClick={() => router.push('/admin/ppid/daftar-informasi-publik/create')}
</Button> >
{isMobile ? 'Tambah' : 'Tambah Baru'}
</Button>
</Group> </Group>
{filteredData.length === 0 ? ( {filteredData.length === 0 ? (
<Stack align="center" py="xl"> <Stack align="center" py="xl">
<IconDeviceImacCog size={40} stroke={1.5} color={colors['blue-button']} /> <IconDeviceImacCog size={40} stroke={1.5} color={colors['blue-button']} />
<Text fw={500} c="dimmed">Belum ada informasi publik yang tersedia</Text> <Text fz={{ base: 'sm', md: 'md' }} fw={500} c="dimmed" lh={1.5}>
Belum ada informasi publik yang tersedia
</Text>
</Stack> </Stack>
) : ( ) : (
<Box style={{ overflowX: 'auto' }}> <>
<Table {/* Desktop Table */}
highlightOnHover <Box visibleFrom="md" style={{ overflowX: 'auto' }}>
striped <Table
stickyHeader highlightOnHover
style={{ minWidth: '700px' }} striped
> stickyHeader
<TableThead> style={{ minWidth: '700px' }}
<TableTr> >
<TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh> <TableThead>
<TableTh style={{ width: '25%' }}>Jenis Informasi</TableTh> <TableTr>
<TableTh style={{ width: '40%' }}>Deskripsi</TableTh> <TableTh w="25%">
<TableTh style={{ width: '20%', textAlign: 'center' }}>Aksi</TableTh> <Text fw={600} lh={1.4}>
</TableTr> Jenis Informasi
</TableThead> </Text>
<TableTbody> </TableTh>
{filteredData.map((item, index) => ( <TableTh w="40%">
<TableTr key={item.id}> <Text fw={600} lh={1.4}>
<TableTd style={{ textAlign: 'center' }}> Deskripsi
<Text fz="sm">{(page - 1) * 10 + index + 1}</Text> </Text>
</TableTd> </TableTh>
<TableTd> <TableTh ta="center" w="20%">
<Box w={200}> <Text fw={600} lh={1.4}>
<Text fw={500} lineClamp={1}>{item.jenisInformasi}</Text> Aksi
</Box> </Text>
</TableTd> </TableTh>
<TableTd> </TableTr>
<Box w={200}> </TableThead>
<Text lineClamp={1} fz="sm" c="dimmed"> <TableTbody>
{item.deskripsi?.replace(/<[^>]*>?/gm, '').substring(0, 80) + '...'} {filteredData.map((item) => (
</Text> <TableTr key={item.id}>
</Box> <TableTd>
<Text fz="sm" fw={600} lh={1.5} lineClamp={1}>
</TableTd> {item.jenisInformasi}
<TableTd style={{ textAlign: 'center' }}> </Text>
</TableTd>
<TableTd>
<Text fz="sm" lh={1.5} c="dimmed" lineClamp={1}>
{item.deskripsi?.replace(/<[^>]*>?/gm, '').substring(0, 80)}...
</Text>
</TableTd>
<TableTd ta="center">
<Button <Button
size="xs" size="xs"
radius="md" radius="md"
variant="light" variant="light"
color="blue" color="blue"
leftSection={<IconDeviceImacCog size={16} />} leftSection={<IconDeviceImacCog size={16} />}
onClick={() => router.push(`/admin/ppid/daftar-informasi-publik/${item.id}`)} onClick={() =>
router.push(`/admin/ppid/daftar-informasi-publik/${item.id}`)
}
> >
Detail Detail
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} ))}
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
{/* Mobile Card List */}
<Stack hiddenFrom="md" gap="sm">
{filteredData.map((item) => (
<Paper key={item.id} withBorder p="sm" radius="md">
<Stack gap={4}>
<Box>
<Text fw={600} lh={1.4}>
Jenis Informasi
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.jenisInformasi}
</Text>
</Box>
<Box>
<Text fw={600} lh={1.4}>
Deskripsi
</Text>
<Text fz="sm" fw={500} lh={1.5} c="dimmed">
{item.deskripsi?.replace(/<[^>]*>?/gm, '').substring(0, 80)}...
</Text>
</Box>
<Box>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() =>
router.push(`/admin/ppid/daftar-informasi-publik/${item.id}`)
}
>
Detail
</Button>
</Box>
</Stack>
</Paper>
))}
</Stack>
</>
)} )}
</Paper> </Paper>
<Center mt="lg">
<Center mt={{ base: 'lg', md: 'xl' }}>
<Pagination <Pagination
value={page} value={page}
onChange={(newPage) => { onChange={(newPage) => {
@@ -129,14 +202,12 @@ function ListDaftarInformasi({ search }: { search: string }) {
window.scrollTo({ top: 0, behavior: 'smooth' }); window.scrollTo({ top: 0, behavior: 'smooth' });
}} }}
total={totalPages} total={totalPages}
mt="md"
mb="md"
color="blue" color="blue"
radius="md" radius="md"
/> />
</Center> </Center>
</Box> </Box>
) );
} }
export default DaftarInformasiPublik; export default DaftarInformasiPublik;

View File

@@ -8,11 +8,11 @@ import { useProxy } from 'valtio/utils';
import stateDasarHukumPPID from '../../_state/ppid/dasar_hukum/dasarHukum'; import stateDasarHukumPPID from '../../_state/ppid/dasar_hukum/dasarHukum';
function Page() { function Page() {
const router = useRouter() const router = useRouter();
const listDasarHukum = useProxy(stateDasarHukumPPID) const listDasarHukum = useProxy(stateDasarHukumPPID);
useShallowEffect(() => { useShallowEffect(() => {
listDasarHukum.findById.load('1') listDasarHukum.findById.load('1');
}, []) }, []);
if (listDasarHukum.findById.loading) { if (listDasarHukum.findById.loading) {
return ( return (
@@ -40,15 +40,16 @@ function Page() {
<Title order={3} c={colors['blue-button']}>Preview Dasar Hukum PPID</Title> <Title order={3} c={colors['blue-button']}>Preview Dasar Hukum PPID</Title>
</GridCol> </GridCol>
<GridCol span={{ base: 12, md: 1 }}> <GridCol span={{ base: 12, md: 1 }}>
<Button <Button
c="green" w={{ base: '100%', md: "110%" }}
variant="light" c="green"
leftSection={<IconEdit size={18} stroke={2} />} variant="light"
radius="md" leftSection={<IconEdit size={18} stroke={2} />}
onClick={() => router.push('/admin/ppid/dasar-hukum/edit')} radius="md"
> onClick={() => router.push('/admin/ppid/dasar-hukum/edit')}
Edit >
</Button> Edit
</Button>
</GridCol> </GridCol>
</Grid> </Grid>
@@ -57,33 +58,39 @@ function Page() {
<Grid> <Grid>
<GridCol span={12}> <GridCol span={12}>
<Center> <Center>
<Image loading='lazy' src="/darmasaba-icon.png" w={{ base: 100, md: 150 }} alt="Logo PPID" /> <Image loading="lazy" src="/darmasaba-icon.png" w={{ base: 100, md: 150 }} alt="Logo PPID" />
</Center> </Center>
</GridCol> </GridCol>
<GridCol span={12}> <GridCol span={12}>
<Text <Title
order={3}
ta="center" ta="center"
fz={{ base: '1.5rem', md: '2rem' }} lh={{ base: 1.15, md: 1.1 }}
fw="bold" fw="bold"
c={colors['blue-button']} c={colors['blue-button']}
dangerouslySetInnerHTML={{ __html: listDasarHukum.findById.data.judul }} dangerouslySetInnerHTML={{ __html: listDasarHukum.findById.data.judul }}
style={{ wordBreak: "break-word", whiteSpace: "normal" }} style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
/> />
</GridCol> </GridCol>
</Grid> </Grid>
<Divider my="xl" color={colors['blue-button']} /> <Divider my="xl" color={colors['blue-button']} />
<Box <Text
className="prose max-w-none"
dangerouslySetInnerHTML={{ __html: listDasarHukum.findById.data.content }} dangerouslySetInnerHTML={{ __html: listDasarHukum.findById.data.content }}
style={{ wordBreak: "break-word", whiteSpace: "normal", fontSize: '1.1rem', lineHeight: 1.7, textAlign: 'justify' }} style={{
wordBreak: 'break-word',
whiteSpace: 'normal',
fontSize: '1rem',
lineHeight: 1.55,
textAlign: 'justify',
}}
/> />
</Box> </Box>
</Paper> </Paper>
</Stack> </Stack>
</Paper> </Paper>
) );
} }
export default Page; export default Page;

View File

@@ -1,7 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client'; 'use client';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core'; import { Box, ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title} from '@mantine/core';
import { IconChartBar, IconUsers } from '@tabler/icons-react'; import { IconChartBar, IconUsers } from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
@@ -56,42 +56,77 @@ function LayoutTabsIKM({ children }: { children: React.ReactNode }) {
radius="lg" radius="lg"
keepMounted={false} keepMounted={false}
> >
<ScrollArea type="auto" offsetScrollbars> {/* ✅ Scroll horizontal wrapper */}
<TabsList <Box visibleFrom='md' pb={10}>
p="sm" <ScrollArea type="auto" offsetScrollbars>
style={{ <TabsList
background: "linear-gradient(135deg, #e7ebf7, #f9faff)", p="sm"
borderRadius: "1rem", style={{
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)", background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
display: "flex", borderRadius: "1rem",
flexWrap: "nowrap", boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
gap: "0.5rem", display: "flex",
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi flexWrap: "nowrap",
}} gap: "0.5rem",
> paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
{tabs.map((e, i) => ( }}
<Tooltip >
key={i} {tabs.map((tab, i) => (
label={e.description}
withArrow
position="bottom"
transitionProps={{ transition: 'pop', duration: 200 }}
>
<TabsTab <TabsTab
value={e.value} key={i}
leftSection={e.icon} value={tab.value}
leftSection={tab.icon}
style={{ style={{
fontWeight: 500, fontWeight: 600,
fontSize: "0.9rem", fontSize: "0.9rem",
transition: "all 0.2s ease", transition: "all 0.2s ease",
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
}} }}
> >
{e.label} {tab.label}
</TabsTab> </TabsTab>
</Tooltip> ))}
))} </TabsList>
</TabsList> </ScrollArea>
</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((e, i) => ( {tabs.map((e, i) => (
<TabsPanel key={i} value={e.value} mt="md"> <TabsPanel key={i} value={e.value} mt="md">
{/* Konten dummy, bisa diganti tergantung routing */} {/* Konten dummy, bisa diganti tergantung routing */}

View File

@@ -85,11 +85,11 @@ function EditResponden() {
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 0, md: 'lg' }} py="xs">
<Group mb="md"> <Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md"> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
<Title order={4} ml="sm" c="dark"> <Title order={4} ml="sm" c="dark">
Edit Responden Edit Responden
</Title> </Title>

View File

@@ -38,7 +38,7 @@ export default function DetailResponden() {
) )
} }
return ( return (
<Box py={10}> <Box px={{ base: 0, md: 'xs' }} py="xs">
<Button <Button
variant="subtle" variant="subtle"
onClick={() => router.back()} onClick={() => router.back()}

View File

@@ -51,7 +51,7 @@ function RespondenCreate() {
} }
} }
return ( return (
<Box> <Box px={{ base: 0, md: 'lg' }} py="xs">
<Box mb={10}> <Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}> <Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack size={20} /> <IconArrowBack size={20} />
@@ -96,24 +96,24 @@ function RespondenCreate() {
} }
disabled={indeksKepuasanState.jenisKelaminResponden.findMany.loading} disabled={indeksKepuasanState.jenisKelaminResponden.findMany.loading}
/> />
<Select <Select
key={"rating_responden"} key={"rating_responden"}
label={"Rating"} label={"Rating"}
placeholder={indeksKepuasanState.pilihanRatingResponden.findMany.loading ? 'Memuat...' : 'Pilih rating'} placeholder={indeksKepuasanState.pilihanRatingResponden.findMany.loading ? 'Memuat...' : 'Pilih rating'}
value={stategrafikBerdasarkanResponden.create.form.ratingId || ""} value={stategrafikBerdasarkanResponden.create.form.ratingId || ""}
onChange={(val) => { onChange={(val) => {
stategrafikBerdasarkanResponden.create.form.ratingId = val ?? ""; stategrafikBerdasarkanResponden.create.form.ratingId = val ?? "";
}} }}
data={ data={
(indeksKepuasanState.pilihanRatingResponden.findMany.data || []) (indeksKepuasanState.pilihanRatingResponden.findMany.data || [])
.filter(Boolean) // Hapus null, undefined, dll .filter(Boolean) // Hapus null, undefined, dll
.map((item) => ({ .map((item) => ({
value: item.id, value: item.id,
label: item.name || 'Tanpa Nama', label: item.name || 'Tanpa Nama',
})) }))
} }
disabled={indeksKepuasanState.pilihanRatingResponden.findMany.loading} disabled={indeksKepuasanState.pilihanRatingResponden.findMany.loading}
/> />
<Select <Select
key={"kelompokUmur"} key={"kelompokUmur"}
label={"Kelompok Umur"} label={"Kelompok Umur"}

View File

@@ -1,5 +1,21 @@
'use client'; 'use client';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import {
Box,
Button,
Center,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
@@ -9,11 +25,11 @@ import HeaderSearch from '../../../_com/header';
import indeksKepuasanState from '../../../_state/landing-page/indeks-kepuasan'; import indeksKepuasanState from '../../../_state/landing-page/indeks-kepuasan';
function Responden() { function Responden() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState('');
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
title="Data Responden" title="Responden"
placeholder="Cari nama responden..." placeholder="Cari nama responden..."
searchIcon={<IconSearch size={18} />} searchIcon={<IconSearch size={18} />}
value={search} value={search}
@@ -33,17 +49,13 @@ function ListResponden({ search }: ListRespondenProps) {
const router = useRouter(); const router = useRouter();
const { data, page, totalPages, loading, load } = state.findMany; const { data, page, totalPages, loading, load } = state.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10) load(page, 10);
}, [page]); }, [page]);
const filteredData = (data || []).filter((item) => {
const filteredData = (data || []).filter(item => {
const keyword = search.toLowerCase(); const keyword = search.toLowerCase();
return ( return item.name.toLowerCase().includes(keyword);
item.name.toLowerCase().includes(keyword)
);
}); });
if (loading || !data) { if (loading || !data) {
@@ -56,21 +68,25 @@ function ListResponden({ search }: ListRespondenProps) {
if (data.length === 0) { if (data.length === 0) {
return ( return (
<Paper withBorder bg="white" p="lg" radius="md" shadow="sm"> <Paper withBorder bg="white" p={{ base: 'md', sm: 'lg' }} radius="md" shadow="sm">
<Stack gap="md"> <Stack gap="md">
<Title order={3}>Data Responden</Title> <Title order={2} lh={1.2}>
<Table striped withRowBorders> Data Responden
<TableThead> </Title>
<TableTr> <Box visibleFrom="md">
<TableTh style={{ textAlign: 'center' }}>No</TableTh> <Table striped withRowBorders>
<TableTh>Nama</TableTh> <TableThead>
<TableTh>Tanggal</TableTh> <TableTr>
<TableTh>Jenis Kelamin</TableTh> <TableTh ta="center">No</TableTh>
<TableTh style={{ textAlign: 'center' }}>Aksi</TableTh> <TableTh>Nama</TableTh>
</TableTr> <TableTh>Tanggal</TableTh>
</TableThead> <TableTh>Jenis Kelamin</TableTh>
</Table> <TableTh ta="center">Aksi</TableTh>
<Text c="dimmed" ta="center" py="md"> </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 Belum ada data responden yang tersedia
</Text> </Text>
</Stack> </Stack>
@@ -79,54 +95,133 @@ function ListResponden({ search }: ListRespondenProps) {
} }
return ( return (
<Paper withBorder bg="white" p="lg" radius="md" shadow="sm"> <Paper withBorder bg="white" p={{ base: 'md', sm: 'lg' }} radius="md" shadow="sm">
<Stack gap="md"> <Stack gap="md">
<Title order={3}>Data Responden</Title> <Title order={2} lh={1.2}>
<Table striped withRowBorders> Data Responden
<TableThead> </Title>
<TableTr>
<TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh> {/* Desktop Table */}
<TableTh style={{ width: '25%' }}>Nama</TableTh> <Box visibleFrom="md">
<TableTh style={{ width: '25%' }}>Tanggal</TableTh> <Table striped withRowBorders>
<TableTh style={{ width: '20%' }}>Jenis Kelamin</TableTh> <TableThead>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length === 0 ? (
<TableTr> <TableTr>
<TableTd colSpan={5}> <TableTh w="5%" ta="center">
<Text c="dimmed" ta="center" py="md"> No
Tidak ada data yang cocok dengan pencarian </TableTh>
</Text> <TableTh w="25%">Nama</TableTh>
</TableTd> <TableTh w="25%">Tanggal</TableTh>
<TableTh w="20%">Jenis Kelamin</TableTh>
<TableTh w="15%" ta="center">
Aksi
</TableTh>
</TableTr> </TableTr>
) : ( </TableThead>
filteredData.map((item, index) => ( <TableTbody>
<TableTr key={item.id}> {filteredData.length === 0 ? (
<TableTd style={{ width: '5%', textAlign: 'center' }}>{index + 1}</TableTd> <TableTr>
<TableTd style={{ width: '25%' }}>{item.name}</TableTd> <TableTd colSpan={5}>
<TableTd style={{ width: '25%' }}> <Text c="dimmed" ta="center" py="md" fz="sm" lh={1.4}>
{item.tanggal ? new Date(item.tanggal).toLocaleDateString('id-ID') : '-'} Tidak ada data yang cocok dengan pencarian
</TableTd> </Text>
<TableTd style={{ width: '20%' }}>{item.jenisKelamin.name}</TableTd>
<TableTd style={{ width: '15%', textAlign: '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 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>
</TableTd>
</TableTr>
))
)}
</TableTbody>
</Table>
</Box>
{/* Mobile Card View */}
<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={4}>
<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>
</Stack>
</Paper>
))}
</Stack>
)}
</Box>
{filteredData.length > 0 && ( {filteredData.length > 0 && (
<Center> <Center>
<Pagination <Pagination
@@ -138,7 +233,6 @@ function ListResponden({ search }: ListRespondenProps) {
}} }}
size="md" size="md"
radius="md" radius="md"
mt="md"
/> />
</Center> </Center>
)} )}
@@ -148,5 +242,3 @@ function ListResponden({ search }: ListRespondenProps) {
} }
export default Responden; export default Responden;

View File

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

View File

@@ -1,102 +1,274 @@
/* eslint-disable react-hooks/exhaustive-deps */
'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 {
import { useShallowEffect } from '@mantine/hooks' Box,
import { IconDeviceImacCog, IconId, IconInfoCircle, IconPhone, IconUser } from '@tabler/icons-react' Button,
Center,
Grid,
GridCol,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
TextInput,
Title,
} from '@mantine/core'
import {
IconDeviceImacCog,
IconId,
IconInfoCircle,
IconPhone,
IconSearch,
IconUser,
} from '@tabler/icons-react'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import { useEffect, useState } from 'react'
import { useProxy } from 'valtio/utils' import { useProxy } from 'valtio/utils'
import statepermohonanInformasiPublikForm from '../../_state/ppid/permohonan_informasi_publik/permohonanInformasiPublik' import statepermohonanInformasiPublikForm from '../../_state/ppid/permohonan_informasi_publik/permohonanInformasiPublik'
import { useDebouncedValue } from '@mantine/hooks'
function Page() { function Page() {
const permohonanInformasiPublikState = useProxy(statepermohonanInformasiPublikForm) const permohonanInformasiPublikState = useProxy(statepermohonanInformasiPublikForm)
const router = useRouter() const router = useRouter()
const { data, page, totalPages, loading, load } = permohonanInformasiPublikState.statepermohonanInformasiPublik.findMany
const [search, setSearch] = useState('')
const [debouncedSearch] = useDebouncedValue(search, 1000);
useShallowEffect(() => { useEffect(() => {
permohonanInformasiPublikState.statepermohonanInformasiPublik.findMany.load() load(page, 10, debouncedSearch);
}, []) }, [page, debouncedSearch]);
if (!permohonanInformasiPublikState.statepermohonanInformasiPublik.findMany.data) { if (loading) {
return ( return (
<Stack pos="relative" bg={colors.Bg} p="lg" align="center"> <Stack pos="relative" p="lg" align="center">
<Skeleton radius="md" h={40} w="60%" />
<Skeleton radius="md" h={200} w="100%" /> <Skeleton radius="md" h={200} w="100%" />
</Stack> </Stack>
) )
} }
if (!data || data.length === 0) {
return (
<Box py={{ base: 'md', md: 'lg' }}>
<Paper bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} radius="xl" shadow="sm" withBorder>
<Stack gap={'sm'}>
<Grid mb={10}>
<GridCol span={{ base: 12, md: 9 }}>
<Title order={2} lh={1.2} c="dark">
Daftar Permohonan Informasi Publik
</Title>
</GridCol>
<GridCol span={{ base: 12, md: 3 }}>
<Paper radius="lg" bg={colors['white-1']}>
<TextInput
radius="lg"
placeholder={"Cari nama..."}
leftSection={<IconSearch size={16} />}
w="100%"
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
</Paper>
</GridCol>
</Grid>
<Stack align="center" py="xl" ta="center">
<IconInfoCircle size={40} stroke={1.5} color={colors['blue-button']} />
<Text fz={{ base: 'sm', md: 'md' }} fw={500} c="dimmed" lh={1.5}>
{search
? 'Tidak ditemukan data yang sesuai dengan pencarian'
: 'Belum ada permohonan yang tercatat'
}
</Text>
</Stack>
</Stack>
</Paper>
</Box>
)
}
const data = permohonanInformasiPublikState.statepermohonanInformasiPublik.findMany.data
return ( return (
<Box py="md"> <Box py={{ base: 'sm', md: 'md' }}>
<Paper bg={colors['white-1']} p="lg" radius="xl" shadow="sm" withBorder> <Paper bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} radius="xl" shadow="sm" withBorder>
<Stack gap="md"> <Stack gap={'sm'}>
<Group justify="space-between"> <Grid mb={10}>
<Title order={2} c="dark">Daftar Permohonan Informasi Publik</Title> <GridCol span={{ base: 12, md: 9 }}>
<IconInfoCircle size={20} stroke={1.5} /> <Title order={2} lh={1.2} c="dark">
</Group> Daftar Permohonan Informasi Publik
</Title>
</GridCol>
<GridCol span={{ base: 12, md: 3 }}>
<Paper radius="lg" bg={colors['white-1']}>
<TextInput
radius="lg"
placeholder={"Cari nama..."}
leftSection={<IconSearch size={16} />}
w="100%"
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
</Paper>
</GridCol>
</Grid>
{data.length === 0 ? ( {data.length === 0 ? (
<Stack align="center" py="xl"> <Stack align="center" py={{ base: 'xl', md: 'xl' }}>
<IconInfoCircle size={40} stroke={1.5} color={colors['blue-button']} /> <IconInfoCircle size={40} stroke={1.5} color={colors['blue-button']} />
<Text fw={500} c="dimmed">Belum ada permohonan informasi yang tercatat</Text> <Text fz={{ base: 'sm', md: 'md' }} fw={500} c="dimmed" lh={1.5}>
Belum ada permohonan informasi yang tercatat
</Text>
</Stack> </Stack>
) : ( ) : (
<Box style={{ overflowX: 'auto' }}> <>
<Table {/* Desktop Table */}
highlightOnHover <Box visibleFrom="md">
withRowBorders <Table highlightOnHover>
withColumnBorders <TableThead>
withTableBorder <TableTr>
striped <TableTh fz="sm" fw={600} ta="center" w={60}>
stickyHeader No
> </TableTh>
<TableThead> <TableTh fz="sm" fw={600}>
<TableTr> <Group gap={5}>
<TableTh>No</TableTh> <IconUser size={16} />
<TableTh><Group gap={5}><IconUser size={16} /> Nama</Group></TableTh> Nama
<TableTh><Group gap={5}><IconId size={16} /> NIK</Group></TableTh> </Group>
<TableTh><Group gap={5}><IconPhone size={16} /> Telepon</Group></TableTh> </TableTh>
<TableTh><Group gap={5}><IconInfoCircle size={16} /> Detail</Group></TableTh> <TableTh fz="sm" fw={600}>
</TableTr> <Group gap={5}>
</TableThead> <IconId size={16} />
<TableTbody> NIK
{data.map((item, index) => ( </Group>
<TableTr key={item.id}> </TableTh>
<TableTd>{index + 1}</TableTd> <TableTh fz="sm" fw={600}>
<TableTd> <Group gap={5}>
<Box w={200}> <IconPhone size={16} />
<Text lineClamp={1} fw={500}>{item.name}</Text> Telepon
</Box> </Group>
</TableTd> </TableTh>
<TableTd> <TableTh fz="sm" fw={600} w={140}>
<Box w={200}> <Group gap={5}>
{item.nik} <IconInfoCircle size={16} />
</Box> Detail
</TableTd> </Group>
<TableTd> </TableTh>
<Box w={200}> </TableTr>
{item.notelp} </TableThead>
</Box> <TableTbody>
</TableTd> {data.map((item, index) => (
<TableTd> <TableTr key={item.id}>
<TableTd ta="center" fz="sm" lh={1.5}>
{index + 1}
</TableTd>
<TableTd>
<Text fz="sm" fw={500} lh={1.5} lineClamp={1}>
{item.name}
</Text>
</TableTd>
<TableTd>
<Text fz="sm" lh={1.5}>{item.nik}</Text>
</TableTd>
<TableTd>
<Text fz="sm" lh={1.5}>{item.notelp}</Text>
</TableTd>
<TableTd>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() =>
router.push(`/admin/ppid/permohonan-informasi-publik/${item.id}`)
}
>
Detail
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
{/* Mobile Cards */}
<Stack hiddenFrom="md" gap="xs">
{data.map((item, index) => (
<Paper key={item.id} p="sm" radius="md" withBorder bg="white">
<Stack gap={4}>
<Box>
<Text fz="xs" fw={600} lh={1.4} c="dark">
No
</Text>
<Text fz="sm" fw={500} lh={1.5} c="dark">
{index + 1}
</Text>
</Box>
<Box>
<Text fz="xs" fw={600} lh={1.4} c="dark">
Nama
</Text>
<Text fz="sm" fw={500} lh={1.5} c="dark" lineClamp={1}>
{item.name}
</Text>
</Box>
<Box>
<Text fz="xs" fw={600} lh={1.4} c="dark">
NIK
</Text>
<Text fz="sm" lh={1.5} c="dark">{item.nik}</Text>
</Box>
<Box>
<Text fz="xs" fw={600} lh={1.4} c="dark">
Telepon
</Text>
<Text fz="sm" lh={1.5} c="dark">{item.notelp}</Text>
</Box>
<Box>
<Text fz="xs" fw={600} lh={1.4} c="dark">
Detail
</Text>
<Button <Button
size="xs" size="xs"
radius="md" radius="md"
variant="light" variant="light"
color="blue" color="blue"
leftSection={<IconDeviceImacCog size={16} />} leftSection={<IconDeviceImacCog size={16} />}
onClick={() => router.push(`/admin/ppid/permohonan-informasi-publik/${item.id}`)} onClick={() =>
router.push(`/admin/ppid/permohonan-informasi-publik/${item.id}`)
}
mt={2}
> >
Detail Lihat Detail
</Button> </Button>
</TableTd> </Box>
</TableTr> </Stack>
))} </Paper>
</TableTbody> ))}
</Table> </Stack>
</Box> </>
)} )}
</Stack> </Stack>
<Center mt="xl">
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
}}
total={totalPages}
withEdges
withControls
radius="md"
/>
</Center>
</Paper> </Paper>
</Box> </Box>
) )

View File

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

View File

@@ -1,97 +1,285 @@
/* eslint-disable react-hooks/exhaustive-deps */
'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 {
import { useShallowEffect } from '@mantine/hooks' Box,
import { IconDeviceImacCog, IconInfoCircle, IconMail, IconPhone, IconUser } from '@tabler/icons-react' Button,
Center,
Grid,
GridCol,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
TextInput,
Title,
} from '@mantine/core'
import {
IconDeviceImacCog,
IconInfoCircle,
IconMail,
IconPhone,
IconSearch,
IconUser,
} from '@tabler/icons-react'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import { useProxy } from 'valtio/utils' import { useProxy } from 'valtio/utils'
import statePermohonanKeberatan from '../../_state/ppid/permohonan_keberatan_informasi_publik/permohonanKeberatanInformasi' import statePermohonanKeberatan from '../../_state/ppid/permohonan_keberatan_informasi_publik/permohonanKeberatanInformasi'
import { useEffect, useState } from 'react'
import { useDebouncedValue } from '@mantine/hooks'
function Page() { function Page() {
const listState = useProxy(statePermohonanKeberatan)
const router = useRouter() const router = useRouter()
useShallowEffect(() => { const listState = useProxy(statePermohonanKeberatan)
listState.findMany.load() const [search, setSearch] = useState("");
}, []) const [debouncedSearch] = useDebouncedValue(search, 1000);
const { data, page, totalPages, loading, load } = listState.findMany
if (!listState.findMany.data) { useEffect(() => {
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
if (loading) {
return ( return (
<Stack pos="relative" bg={colors.Bg} p="lg" align="center"> <Stack pos="relative" p="lg" align="center">
<Skeleton radius="md" h={40} w="60%" />
<Skeleton radius="md" h={200} w="100%" /> <Skeleton radius="md" h={200} w="100%" />
</Stack> </Stack>
) )
} }
if (!data || data.length === 0) {
const data = listState.findMany.data return (
<Box py={{ base: 'md', md: 'lg' }}>
<Paper bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} radius="xl" shadow="sm" withBorder>
<Stack gap={'sm'}>
<Grid mb={10}>
<GridCol span={{ base: 12, md: 9 }}>
<Title order={2} lh={1.2} c="dark">
Daftar Permohonan Keberatan Informasi Publik
</Title>
</GridCol>
<GridCol span={{ base: 12, md: 3 }}>
<Paper radius="lg" bg={colors['white-1']}>
<TextInput
radius="lg"
placeholder={"Cari nama..."}
leftSection={<IconSearch size={16} />}
w="100%"
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
</Paper>
</GridCol>
</Grid>
<Stack align="center" py="xl" ta="center">
<IconInfoCircle size={40} stroke={1.5} color={colors['blue-button']} />
<Text fz={{ base: 'sm', md: 'md' }} fw={500} c="dimmed" lh={1.5}>
{search
? 'Tidak ditemukan data yang sesuai dengan pencarian'
: 'Belum ada permohonan keberatan yang tercatat'
}
</Text>
</Stack>
</Stack>
</Paper>
</Box>
)
}
return ( return (
<Box py="md"> <Box py={{ base: 'md', md: 'lg' }}>
<Paper bg={colors['white-1']} p="lg" radius="xl" shadow="sm" withBorder> <Paper bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} radius="xl" shadow="sm" withBorder>
<Stack gap="md"> <Stack gap={'sm'}>
<Group justify="space-between"> <Grid mb={10}>
<Title order={2} c="dark">Daftar Permohonan Keberatan Informasi Publik</Title> <GridCol span={{ base: 12, md: 9 }}>
<IconInfoCircle size={20} stroke={1.5} /> <Title order={2} lh={1.2} c="dark">
</Group> Daftar Permohonan Keberatan Informasi Publik
</Title>
</GridCol>
<GridCol span={{ base: 12, md: 3 }}>
<Paper radius="lg" bg={colors['white-1']}>
<TextInput
radius="lg"
placeholder={"Cari nama..."}
leftSection={<IconSearch size={16} />}
w="100%"
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
</Paper>
</GridCol>
</Grid>
{data.length === 0 ? ( {data.length === 0 ? (
<Stack align="center" py="xl"> <Stack align="center" py="xl" ta="center">
<IconInfoCircle size={40} stroke={1.5} color={colors['blue-button']} /> <IconInfoCircle size={40} stroke={1.5} color={colors['blue-button']} />
<Text fw={500} c="dimmed">Belum ada permohonan keberatan yang tercatat</Text> <Text fz={{ base: 'sm', md: 'md' }} fw={500} c="dimmed" lh={1.5}>
Belum ada permohonan keberatan yang tercatat
</Text>
</Stack> </Stack>
) : ( ) : (
<Box style={{ overflowX: 'auto' }}> <>
<Table {/* Desktop Table */}
highlightOnHover <Box visibleFrom="md">
withRowBorders <Table highlightOnHover>
withColumnBorders <TableThead>
withTableBorder <TableTr>
striped <TableTh fz="sm" fw={600} lh={1.4} ta="center">
stickyHeader No
> </TableTh>
<TableThead> <TableTh fz="sm" fw={600} lh={1.4}>
<TableTr> <Group gap={5}>
<TableTh>No</TableTh> <IconUser size={16} />
<TableTh><Group gap={5}><IconUser size={16} /> Nama</Group></TableTh> Nama
<TableTh><Group gap={5}><IconMail size={16} /> Email</Group></TableTh> </Group>
<TableTh><Group gap={5}><IconPhone size={16} /> Telepon</Group></TableTh> </TableTh>
<TableTh><Group gap={5}><IconInfoCircle size={16} /> Detail</Group></TableTh> <TableTh fz="sm" fw={600} lh={1.4}>
</TableTr> <Group gap={5}>
</TableThead> <IconMail size={16} />
<TableTbody> Email
{data.map((item, index) => ( </Group>
<TableTr key={item.id}> </TableTh>
<TableTd>{index + 1}</TableTd> <TableTh fz="sm" fw={600} lh={1.4}>
<TableTd> <Group gap={5}>
<Text lineClamp={1} fw={500}>{item.name}</Text> <IconPhone size={16} />
</TableTd> Telepon
<TableTd> </Group>
<Text size="sm">{item.email || '-'}</Text> </TableTh>
</TableTd> <TableTh fz="sm" fw={600} lh={1.4} ta="center">
<TableTd> <Group gap={5}>
<Text>{item.notelp || '-'}</Text> <IconInfoCircle size={16} />
</TableTd> Detail
<TableTd> </Group>
</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{data.map((item, index) => (
<TableTr key={item.id}>
<TableTd ta="center" fz="sm" lh={1.5}>
{index + 1}
</TableTd>
<TableTd>
<Text fz="sm" fw={500} lh={1.5} lineClamp={1}>
{item.name}
</Text>
</TableTd>
<TableTd>
<Text fz="sm" lh={1.5}>
{item.email || '-'}
</Text>
</TableTd>
<TableTd>
<Text fz="sm" lh={1.5}>
{item.notelp || '-'}
</Text>
</TableTd>
<TableTd>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() =>
router.push(
`/admin/ppid/permohonan-keberatan-informasi-publik/${item.id}`
)
}
>
Detail
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
{/* Mobile Cards */}
<Stack hiddenFrom="md" gap="xs">
{data.map((item, index) => (
<Paper key={item.id} p="sm" radius="md" withBorder bg="white">
<Stack gap={4}>
<Box>
<Text fz="xs" fw={600} lh={1.4} c="dimmed">
No
</Text>
<Text fz="sm" fw={600} lh={1.5}>
{index + 1}
</Text>
</Box>
<Box>
<Text fz="xs" fw={600} lh={1.4} c="dimmed">
Nama
</Text>
<Text fz="sm" fw={600} lh={1.5}>
{item.name}
</Text>
</Box>
<Box>
<Text fz="xs" fw={600} lh={1.4} c="dimmed">
Email
</Text>
<Text fz="sm" lh={1.5}>
{item.email || '-'}
</Text>
</Box>
<Box>
<Text fz="xs" fw={600} lh={1.4} c="dimmed">
Telepon
</Text>
<Text fz="sm" lh={1.5}>
{item.notelp || '-'}
</Text>
</Box>
<Box>
<Button <Button
size="xs" size="xs"
radius="md" radius="md"
variant="light" variant="light"
color="blue" color="blue"
leftSection={<IconDeviceImacCog size={16} />} leftSection={<IconDeviceImacCog size={16} />}
onClick={() => router.push(`/admin/ppid/permohonan-keberatan-informasi-publik/${item.id}`)} onClick={() =>
router.push(
`/admin/ppid/permohonan-keberatan-informasi-publik/${item.id}`
)
}
mt={4}
> >
Detail Detail
</Button> </Button>
</TableTd> </Box>
</TableTr> </Stack>
))} </Paper>
</TableTbody> ))}
</Table> </Stack>
</Box> </>
)} )}
</Stack> </Stack>
<Center mt="xl">
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
}}
total={totalPages}
withEdges
withControls
radius="md"
/>
</Center>
</Paper> </Paper>
</Box> </Box>
); )
} }
export default Page;
export default Page

View File

@@ -138,7 +138,7 @@ function EditProfilePPID() {
} }
return ( return (
<Box p="md"> <Box px={{ base: 0, md: 'lg' }} py="xs">
<Stack gap="md"> <Stack gap="md">
<Group mb="md"> <Group mb="md">
<Button variant="subtle" onClick={handleBack} p="xs" radius="md"> <Button variant="subtle" onClick={handleBack} p="xs" radius="md">

View File

@@ -7,13 +7,13 @@ import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import stateProfilePPID from '../../_state/ppid/profile_ppid/profile_PPID'; import stateProfilePPID from '../../_state/ppid/profile_ppid/profile_PPID';
function Page() { function Page() {
const router = useRouter() const router = useRouter();
const allList = useProxy(stateProfilePPID) const allList = useProxy(stateProfilePPID);
useShallowEffect(() => { useShallowEffect(() => {
allList.profile.load("edit") // Assuming "1" is your default ID, adjust as needed allList.profile.load("edit");
}, []) }, []);
if (!allList.profile.data) { if (!allList.profile.data) {
return ( return (
@@ -32,19 +32,19 @@ function Page() {
<Stack gap="md"> <Stack gap="md">
<Grid> <Grid>
<GridCol span={{ base: 12, md: 11 }}> <GridCol span={{ base: 12, md: 11 }}>
<Title order={3} c={colors['blue-button']}>Preview Profil PPID</Title> <Title order={3} c={colors['blue-button']} lh={1.2}>Preview Profil PPID</Title>
</GridCol> </GridCol>
<GridCol span={{ base: 12, md: 1 }}> <GridCol span={{ base: 12, md: 1 }}>
<Button <Button
w={{base: '100%', md: "110%"}} w={{ base: '100%', md: "110%" }}
c="green" c="green"
variant="light" variant="light"
leftSection={<IconEdit size={18} stroke={2} />} leftSection={<IconEdit size={18} stroke={2} />}
radius="md" radius="md"
onClick={() => router.push(`/admin/ppid/profil-ppid/${allList.profile.data?.id}`)} onClick={() => router.push(`/admin/ppid/profil-ppid/${allList.profile.data?.id}`)}
> >
Edit Edit
</Button> </Button>
</GridCol> </GridCol>
</Grid> </Grid>
{dataArray.map((item) => ( {dataArray.map((item) => (
@@ -57,9 +57,14 @@ function Page() {
</Center> </Center>
</GridCol> </GridCol>
<GridCol span={12}> <GridCol span={12}>
<Text ta="center" fz={{ base: "1.2rem", md: "1.8rem" }} fw="bold" c={colors['blue-button']}> <Title
order={2}
c={colors['blue-button']}
ta="center"
lh={1.15}
>
PROFIL PIMPINAN BADAN PUBLIK DESA DARMASABA PROFIL PIMPINAN BADAN PUBLIK DESA DARMASABA
</Text> </Title>
</GridCol> </GridCol>
</Grid> </Grid>
</Box> </Box>
@@ -87,34 +92,77 @@ function Page() {
className="glass3" className="glass3"
style={{ mt: -30, boxShadow: '0 4px 20px rgba(0,0,0,0.15)' }} style={{ mt: -30, boxShadow: '0 4px 20px rgba(0,0,0,0.15)' }}
> >
<Text ta="center" c={colors['white-1']} fw="bolder" fz={{ base: "1.2rem", md: "1.6rem" }}> <Title
order={3}
c={colors['white-1']}
ta="center"
lh={1.2}
>
{item.name} {item.name}
</Text> </Title>
</Paper> </Paper>
</Stack> </Stack>
</Paper> </Paper>
<Box mt="lg"> <Box mt="lg">
<Text fz={{ base: "1.125rem", md: "1.5rem" }} fw="bold" mb={4}>Biodata</Text> <Title order={3} lh={1.2} mb={4}>
<Text fz={{ base: "1rem", md: "1.4rem" }} ta="justify" c={colors['blue-button']} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: item.biodata }} /> Biodata
</Title>
<Text
fz={{ base: 'sm', md: 'md' }}
ta="justify"
c={colors['blue-button']}
lh={1.5}
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
dangerouslySetInnerHTML={{ __html: item.biodata }}
/>
</Box> </Box>
<Box mt="xl"> <Box mt="xl">
<Text fz={{ base: "1.125rem", md: "1.5rem" }} fw="bold" mb={4}>Riwayat Karir</Text> <Title order={3} lh={1.2} mb={4}>
<Text fz={{ base: "1rem", md: "1.4rem" }} ta="justify" c={colors['blue-button']} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: item.riwayat }} /> Riwayat Karir
</Box> </Title>
<Box mt="xl">
<Text fz={{ base: "1.125rem", md: "1.5rem" }} fw="bold" mb={4}>Pengalaman Organisasi</Text>
<Box px={20}> <Box px={20}>
<Text fz={{ base: "1rem", md: "1.4rem" }} ta="justify" c={colors['blue-button']} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: item.pengalaman }} /> <Text
fz={{ base: 'sm', md: 'md' }}
ta="justify"
c={colors['blue-button']}
lh={1.5}
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
dangerouslySetInnerHTML={{ __html: item.riwayat }}
/>
</Box>
</Box>
<Box mt="xl">
<Title order={3} lh={1.2} mb={4}>
Pengalaman Organisasi
</Title>
<Box px={20}>
<Text
fz={{ base: 'sm', md: 'md' }}
ta="justify"
c={colors['blue-button']}
lh={1.5}
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
dangerouslySetInnerHTML={{ __html: item.pengalaman }}
/>
</Box> </Box>
</Box> </Box>
<Box mt="xl" mb="lg"> <Box mt="xl" mb="lg">
<Text fz={{ base: "1.125rem", md: "1.5rem" }} fw="bold" mb={4}>Program Kerja Unggulan</Text> <Title order={3} lh={1.2} mb={4}>
Program Kerja Unggulan
</Title>
<Box px={20}> <Box px={20}>
<Text fz={{ base: "1rem", md: "1.4rem" }} ta="justify" c={colors['blue-button']} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: item.unggulan }} /> <Text
fz={{ base: 'sm', md: 'md' }}
ta="justify"
c={colors['blue-button']}
lh={1.5}
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
dangerouslySetInnerHTML={{ __html: item.unggulan }}
/>
</Box> </Box>
</Box> </Box>
</Box> </Box>
@@ -122,9 +170,7 @@ function Page() {
))} ))}
</Stack> </Stack>
</Paper> </Paper>
) );
} }
export default Page; export default Page;

View File

@@ -1,7 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core'; import { Box, ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import { IconBuildingCommunity, IconHierarchy2, IconUsers } from '@tabler/icons-react'; import { IconBuildingCommunity, IconHierarchy2, IconUsers } from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
@@ -63,51 +63,92 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
keepMounted={false} keepMounted={false}
> >
{/* ✅ Scroll horizontal wrapper */} {/* ✅ Scroll horizontal wrapper */}
<ScrollArea type="auto" offsetScrollbars> <Box visibleFrom='md' pb={10}>
<TabsList <ScrollArea type="auto" offsetScrollbars>
p="sm" <TabsList
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={{ style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)", padding: "1.5rem",
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
borderRadius: "1rem", borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)", boxShadow: "0 4px 16px 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) => ( {children}
<TabsTab </TabsPanel>
key={i} ))}
value={tab.value} </Tabs>
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
))}
</TabsList>
</ScrollArea>
{tabs.map((tab, i) => (
<TabsPanel
key={i}
value={tab.value}
style={{
padding: "1.5rem",
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
borderRadius: "1rem",
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
}}
>
{children}
</TabsPanel>
))}
</Tabs>
</Stack > </Stack >
); );
} }

View File

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

View File

@@ -51,7 +51,7 @@ function DetailPegawai() {
const data = statePegawai.findUnique.data; const data = statePegawai.findUnique.data;
return ( return (
<Box> <Box px={{ base: 0, md: 'xs' }} py="xs">
<Box mb={10}> <Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}> <Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} /> <IconArrowBack color={colors['blue-button']} size={25} />
@@ -59,7 +59,7 @@ function DetailPegawai() {
</Box> </Box>
<Paper <Paper
withBorder withBorder
w={{ base: "100%", md: "60%" }} w={{ base: "100%", md: "70%" }}
bg={colors['white-1']} bg={colors['white-1']}
p="lg" p="lg"
radius="md" radius="md"

View File

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

View File

@@ -1,13 +1,32 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Badge, Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, ThemeIcon, Title } from '@mantine/core'; import {
import { IconCheck, IconDeviceImacCog, IconPlus, IconSearch, IconX } from '@tabler/icons-react'; Badge,
Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title
} from '@mantine/core';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import stateStrukturPPID from '../../../_state/ppid/struktur_ppid/struktur_PPID'; import stateStrukturPPID from '../../../_state/ppid/struktur_ppid/struktur_PPID';
import { useDebouncedValue } from '@mantine/hooks';
function PegawaiPPID() { function PegawaiPPID() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
@@ -28,6 +47,7 @@ function PegawaiPPID() {
function ListPegawaiPPID({ search }: { search: string }) { function ListPegawaiPPID({ search }: { search: string }) {
const stateOrganisasi = useProxy(stateStrukturPPID.pegawai); const stateOrganisasi = useProxy(stateStrukturPPID.pegawai);
const router = useRouter(); const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { const {
data, data,
@@ -38,47 +58,28 @@ function ListPegawaiPPID({ search }: { search: string }) {
} = stateOrganisasi.findMany; } = stateOrganisasi.findMany;
useEffect(() => { useEffect(() => {
load(page, 10, search); load(page, 10, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
const filteredData = data || [] const filteredData = data || [];
// Handle loading state // Handle loading state
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py="xl">
<Skeleton height={300} /> <Skeleton height={300} radius="md" />
</Stack> </Stack>
); );
} }
if (data.length === 0) { if (data.length === 0) {
return ( return (
<Box py={10}> <Box py="xl">
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md"> <Group justify="space-between" mb="md">
<Title order={4}>Daftar Pegawai PPID</Title> <Title order={2} lh={1.2}>
<Button Daftar Pegawai PPID
leftSection={<IconPlus size={18} />} </Title>
color="blue"
variant="light"
onClick={() => router.push('/admin/ppid/struktur-ppid/pegawai/create')}
>
Tambah Baru
</Button>
</Group>
<Center py="xl">
<Text c="dimmed">Tidak ada data pegawai yang ditemukan</Text>
</Center>
</Paper>
</Box>
);
}
return (
<Box py={10}>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Pegawai PPID</Title>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
@@ -87,53 +88,70 @@ function ListPegawaiPPID({ search }: { search: string }) {
> >
Tambah Baru Tambah Baru
</Button> </Button>
</Group>
<Center py="xl">
<Text c="dimmed" fz={{ base: 'sm', md: 'md' }} lh={1.5}>
Tidak ada data pegawai yang ditemukan
</Text>
</Center>
</Paper>
</Box>
);
}
return (
<Box py="xl">
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={2} lh={1.2}>
Daftar Pegawai PPID
</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/ppid/struktur-ppid/pegawai/create')}
>
Tambah Baru
</Button>
</Group> </Group>
<Box style={{ overflowX: "auto" }}>
<Table highlightOnHover> {/* Desktop: Table */}
<Box visibleFrom="md">
<Table highlightOnHover miw={0}>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh style={{ width: '25%' }}>Nama Lengkap</TableTh> <TableTh fz="sm" fw={600} lh={1.4}>
<TableTh style={{ width: '20%' }}>Posisi</TableTh> Nama Lengkap
<TableTh style={{ width: '10%' }}>Status</TableTh> </TableTh>
<TableTh style={{ width: '10%' }}>Aksi</TableTh> <TableTh fz="sm" fw={600} lh={1.4}>
Posisi
</TableTh>
<TableTh fz="sm" fw={600} lh={1.4}>
Status
</TableTh>
<TableTh fz="sm" fw={600} lh={1.4}>
Aksi
</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.map((item) => ( {filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Box w={150}> <Text fz="md" fw={500} lh={1.5} truncate="end">
<Text fw={500} truncate="end" lineClamp={1}> {item.namaLengkap}
{item.namaLengkap} </Text>
</Text>
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={150}> <Badge variant="light" color="blue" fz="sm" lh={1.4}>
<Badge variant="light" color="blue"> {item.posisi?.nama || 'Belum diatur'}
{item.posisi?.nama || 'Belum diatur'} </Badge>
</Badge>
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Group gap="xs" wrap="nowrap"> <Badge color={item.isActive ? "green" : "red"} fz="sm" lh={1.4}>
<Box visibleFrom="sm"> {item.isActive ? "Aktif" : "Tidak Aktif"}
<Badge color={item.isActive ? "green" : "red"}> </Badge>
{item.isActive ? "Aktif" : "Tidak Aktif"}
</Badge>
</Box>
<Box hiddenFrom="sm">
{item.isActive ? (
<ThemeIcon color="green" variant="light" size="sm">
<IconCheck size={16} />
</ThemeIcon>
) : (
<ThemeIcon color="red" variant="light" size="sm">
<IconX size={16} />
</ThemeIcon>
)}
</Box>
</Group>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button <Button
@@ -143,6 +161,7 @@ function ListPegawaiPPID({ search }: { search: string }) {
color="blue" color="blue"
leftSection={<IconDeviceImacCog size={16} />} leftSection={<IconDeviceImacCog size={16} />}
onClick={() => router.push(`/admin/ppid/struktur-ppid/pegawai/${item.id}`)} onClick={() => router.push(`/admin/ppid/struktur-ppid/pegawai/${item.id}`)}
fz="sm"
> >
Detail Detail
</Button> </Button>
@@ -152,7 +171,47 @@ function ListPegawaiPPID({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
<Center mt="lg">
{/* Mobile: Card List */}
<Stack hiddenFrom="md" gap="sm" mt="md">
{filteredData.map((item) => (
<Paper key={item.id} withBorder p="md" radius="md">
<Stack gap={"xs"}>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Nama Lengkap</Text>
<Text fz="md" fw={500} lh={1.4}>
{item.namaLengkap}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Posisi</Text>
<Badge variant="light" color="blue" fz="xs" lh={1.4}>
{item.posisi?.nama || 'Belum diatur'}
</Badge>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Status</Text>
<Badge color={item.isActive ? "green" : "red"} fz="xs" lh={1.4}>
{item.isActive ? "Aktif" : "Tidak Aktif"}
</Badge>
</Box>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() => router.push(`/admin/ppid/struktur-ppid/pegawai/${item.id}`)}
fz="xs"
>
Detail
</Button>
</Stack>
</Paper>
))}
</Stack>
<Center mt="xl">
<Pagination <Pagination
value={page} value={page}
onChange={(newPage) => { onChange={(newPage) => {

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import { 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 { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -31,6 +31,7 @@ function ListPosisiOrganisasiPPID({ search }: { search: string }) {
const router = useRouter(); const router = useRouter();
const [modalHapus, setModalHapus] = useState(false) const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null)
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { const {
data, data,
@@ -41,8 +42,8 @@ function ListPosisiOrganisasiPPID({ search }: { search: string }) {
} = stateOrganisasi.findMany; } = stateOrganisasi.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search); load(page, 10, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
const handleHapus = async () => { const handleHapus = async () => {
if (selectedId) { if (selectedId) {
@@ -56,63 +57,63 @@ function ListPosisiOrganisasiPPID({ search }: { search: string }) {
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={{ base: 'sm', md: 'md' }}>
<Skeleton height={600} radius="md" /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
); );
} }
return ( return (
<Box py={10}> <Box py={{ base: 'sm', md: 'md' }}>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
<Group justify="space-between" mb="md"> <Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
<Title order={4}>Daftar Posisi Organisasi PPID</Title> <Title order={2}>Daftar Posisi Organisasi PPID</Title>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
variant="light" variant="light"
onClick={() => router.push('/admin/ppid/struktur-ppid/posisi-organisasi/create')} onClick={() => router.push('/admin/ppid/struktur-ppid/posisi-organisasi/create')}
> >
Tambah Baru Tambah Baru
</Button> </Button>
</Group> </Group>
<Box style={{ overflowX: 'auto' }}>
{/* Desktop Table */}
<Box visibleFrom="md">
<Table highlightOnHover> <Table highlightOnHover>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh style={{ width: '20%' }}>Nama Posisi</TableTh> <TableTh fz="sm" fw={600} lh={1.4}>Nama Posisi</TableTh>
<TableTh style={{ width: '20%' }}>Deskripsi</TableTh> <TableTh fz="sm" fw={600} lh={1.4}>Deskripsi</TableTh>
<TableTh style={{ width: '20%' }}>Hierarki</TableTh> <TableTh fz="sm" fw={600} lh={1.4}>Hierarki</TableTh>
<TableTh style={{ width: '20%' }}>Edit</TableTh> <TableTh fz="sm" fw={600} lh={1.4}>Edit</TableTh>
<TableTh style={{ width: '20%' }}>Hapus</TableTh> <TableTh fz="sm" fw={600} lh={1.4}>Hapus</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.length > 0 ? ( {filteredData.length > 0 ? (
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd style={{ width: '20%' }}> <TableTd>
<Text fw={500} truncate="end" lineClamp={1}>{item.nama}</Text> <Text fz="md" fw={600} lh={1.5} truncate="end" lineClamp={1}>{item.nama}</Text>
</TableTd> </TableTd>
<TableTd style={{ width: '20%' }}> <TableTd w={200}>
<Box w={200}> <Text fz="sm" lh={1.5} c="dimmed" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi || '-' }} />
<Text lineClamp={1} fz="sm" c="dimmed" dangerouslySetInnerHTML={{ __html: item.deskripsi || '-' }} />
</Box>
</TableTd> </TableTd>
<TableTd style={{ width: '20%' }}> <TableTd>
<Text>{item.hierarki || '-'}</Text> <Text fz="md" lh={1.5}>{item.hierarki || '-'}</Text>
</TableTd> </TableTd>
<TableTd style={{ width: '20%' }}> <TableTd>
<Button <Button
variant="light" variant="light"
color="green" color="green"
size="sm" size="sm"
onClick={() => router.push(`/admin/ppid/struktur-ppid/posisi-organisasi/${item.id}`)} onClick={() => router.push(`/admin/ppid/struktur-ppid/posisi-organisasi/${item.id}`)}
> >
<IconEdit size={18} /> <IconEdit size={18} />
</Button> </Button>
</TableTd> </TableTd>
<TableTd style={{ width: '20%' }}> <TableTd>
<Button <Button
variant="light" variant="light"
color="red" color="red"
@@ -129,9 +130,11 @@ function ListPosisiOrganisasiPPID({ search }: { search: string }) {
)) ))
) : ( ) : (
<TableTr> <TableTr>
<TableTd colSpan={4}> <TableTd colSpan={5}>
<Center py={20}> <Center py={{ base: 'sm', md: 'md' }}>
<Text color="dimmed">Tidak ada data posisi organisasi yang cocok</Text> <Text fz="sm" c="dimmed" ta="center" lh={1.5}>
Tidak ada data posisi organisasi yang cocok
</Text>
</Center> </Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -139,7 +142,59 @@ function ListPosisiOrganisasiPPID({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
{/* Mobile Card View */}
<Stack gap="xs" hiddenFrom="md">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} withBorder p="sm" radius="sm">
<Stack gap={4}>
<Box>
<Text fz="xs" fw={600} lh={1.4}>Nama Posisi</Text>
<Text fz="sm" fw={600} lh={1.5}>{item.nama}</Text>
</Box>
<Box>
<Text fz="xs" fw={600} lh={1.4}>Deskripsi</Text>
<Text fz="sm" lh={1.5} dangerouslySetInnerHTML={{ __html: item.deskripsi || '-' }} />
</Box>
<Box>
<Text fz="xs" fw={600} lh={1.4}>Hierarki</Text>
<Text fz="sm" lh={1.5}>{item.hierarki || '-'}</Text>
</Box>
<Group justify="flex-end" gap="xs">
<Button
variant="light"
color="green"
size="xs"
onClick={() => router.push(`/admin/ppid/struktur-ppid/posisi-organisasi/${item.id}`)}
>
<IconEdit size={16} />
</Button>
<Button
variant="light"
color="red"
size="xs"
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={16} />
</Button>
</Group>
</Stack>
</Paper>
))
) : (
<Center py="sm">
<Text fz="sm" c="dimmed" ta="center" lh={1.5}>
Tidak ada data posisi organisasi yang cocok
</Text>
</Center>
)}
</Stack>
</Paper> </Paper>
<Center> <Center>
<Pagination <Pagination
value={page} value={page}
@@ -154,6 +209,7 @@ function ListPosisiOrganisasiPPID({ search }: { search: string }) {
radius="md" radius="md"
/> />
</Center> </Center>
{/* Modal Hapus */} {/* Modal Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}

View File

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

View File

@@ -7,9 +7,8 @@ import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import stateVisiMisiPPID from '../../_state/ppid/visi_misi_ppid/visimisiPPID'; import stateVisiMisiPPID from '../../_state/ppid/visi_misi_ppid/visimisiPPID';
function VisiMisiPPIDList() { function VisiMisiPPIDList() {
const router = useRouter() const router = useRouter();
const listVisiMisi = useProxy(stateVisiMisiPPID); const listVisiMisi = useProxy(stateVisiMisiPPID);
useShallowEffect(() => { useShallowEffect(() => {
listVisiMisi.findById.load('1'); listVisiMisi.findById.load('1');
@@ -41,15 +40,16 @@ function VisiMisiPPIDList() {
<Title order={3} c={colors['blue-button']}>Preview Visi Misi PPID</Title> <Title order={3} c={colors['blue-button']}>Preview Visi Misi PPID</Title>
</GridCol> </GridCol>
<GridCol span={{ base: 12, md: 1 }}> <GridCol span={{ base: 12, md: 1 }}>
<Button <Button
c="green" w={{ base: '100%', md: "110%" }}
variant="light" c="green"
leftSection={<IconEdit size={18} stroke={2} />} variant="light"
radius="md" leftSection={<IconEdit size={18} stroke={2} />}
onClick={() => router.push('/admin/ppid/visi-misi-ppid/edit')} radius="md"
> onClick={() => router.push('/admin/ppid/visi-misi-ppid/edit')}
Edit >
</Button> Edit
</Button>
</GridCol> </GridCol>
</Grid> </Grid>
@@ -58,14 +58,25 @@ function VisiMisiPPIDList() {
<Grid> <Grid>
<GridCol span={12}> <GridCol span={12}>
<Center> <Center>
<Image loading='lazy' src="/darmasaba-icon.png" w={{ base: 100, md: 150 }} alt="Logo PPID" /> <Image loading="lazy" src="/darmasaba-icon.png" w={{ base: 100, md: 150 }} alt="Logo PPID" />
</Center> </Center>
</GridCol> </GridCol>
<GridCol span={12}> <GridCol span={12}>
<Text ta="center" fz={{ base: '1.2rem', md: '1.8rem' }} fw="bold" c={colors['blue-button']}> <Title
order={2}
ta="center"
c={colors['blue-button']}
style={{ lineHeight: 1.15 }}
>
MOTO PPID DESA DARMASABA MOTO PPID DESA DARMASABA
</Text> </Title>
<Text ta="center" fz={{ base: '1rem', md: '1.2rem' }} mt="sm"> <Text
ta="center"
fz={{ base: 'sm', md: 'md' }}
lh={{ base: 1.5, md: 1.5 }}
mt="sm"
c="black"
>
MEMBERIKAN INFORMASI YANG CEPAT, MUDAH, TEPAT DAN TRANSPARAN MEMBERIKAN INFORMASI YANG CEPAT, MUDAH, TEPAT DAN TRANSPARAN
</Text> </Text>
</GridCol> </GridCol>
@@ -74,26 +85,50 @@ function VisiMisiPPIDList() {
<Divider my="xl" color={colors['blue-button']} /> <Divider my="xl" color={colors['blue-button']} />
<Box> <Box>
<Text fz={{ base: '1.5rem', md: '1.75rem' }} fw="bold" ta="center" mb="lg" c={colors['blue-button']}> <Title
order={2}
ta="center"
mb="lg"
c={colors['blue-button']}
style={{ lineHeight: 1.15 }}
>
VISI PPID VISI PPID
</Text> </Title>
<Box <Text
className="prose max-w-none" ta={{ base: "center", md: "justify" }}
dangerouslySetInnerHTML={{ __html: listVisiMisi.findById.data.visi }} dangerouslySetInnerHTML={{ __html: listVisiMisi.findById.data.visi }}
style={{wordBreak: "break-word", whiteSpace: "normal", textAlign: "justify"}} style={{
wordBreak: 'break-word',
whiteSpace: 'normal',
fontSize: '1rem',
lineHeight: 1.55,
color: 'black',
}}
/> />
</Box> </Box>
<Divider my="xl" color={colors['blue-button']} /> <Divider my="xl" color={colors['blue-button']} />
<Box mt="xl"> <Box mt="xl">
<Text fz={{ base: '1.5rem', md: '1.75rem' }} fw="bold" ta="center" mb="lg" c={colors['blue-button']}> <Title
order={2}
ta="center"
mb="lg"
c={colors['blue-button']}
style={{ lineHeight: 1.15 }}
>
MISI PPID MISI PPID
</Text> </Title>
<Box <Text
className="prose max-w-none" ta={"justify"}
dangerouslySetInnerHTML={{ __html: listVisiMisi.findById.data.misi }} dangerouslySetInnerHTML={{ __html: listVisiMisi.findById.data.misi }}
style={{wordBreak: "break-word", whiteSpace: "normal", textAlign: "justify"}} style={{
wordBreak: 'break-word',
whiteSpace: 'normal',
fontSize: '1rem',
lineHeight: 1.55,
color: 'black',
}}
/> />
</Box> </Box>
</Box> </Box>

View File

@@ -1,14 +1,56 @@
/* 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 permohonanInformasiPublikFindMany() { export default async function permohonanInformasiPublikFindMany(
const res = await prisma.permohonanInformasiPublik.findMany({ context: Context
include: { ) {
jenisInformasiDiminta: true, const page = Number(context.query.page) || 1;
caraMemperolehInformasi: true, const limit = Number(context.query.limit) || 10;
caraMemperolehSalinanInformasi: true, const search = (context.query.search as string) || "";
} const skip = (page - 1) * limit;
});
return { const where: any = { isActive: true };
data: res,
}; // Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ name: { contains: search, mode: "insensitive" } },
{ email: { contains: search, mode: "insensitive" } },
];
}
try {
const [data, total] = await Promise.all([
prisma.permohonanInformasiPublik.findMany({
where,
skip,
include: {
jenisInformasiDiminta: true,
caraMemperolehInformasi: true,
caraMemperolehSalinanInformasi: true,
},
take: limit,
orderBy: { name: "asc" }, // opsional, kalau mau urut berdasarkan waktu
}),
prisma.permohonanInformasiPublik.count({
where: { isActive: true },
}),
]);
return {
success: true,
message: "Success fetch formulir permohonan keberatan 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 formulir permohonan keberatan with pagination",
};
}
} }

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 permohonanKeberatanInformasiPublikFindMany() { export default async function permohonanKeberatanInformasiPublikFindMany(context: Context) {
const res = await prisma.formulirPermohonanKeberatan.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' } },
{email: { contains: search, mode: 'insensitive' } },
];
}
try {
const [data, total] = await Promise.all([
prisma.formulirPermohonanKeberatan.findMany({
where,
skip,
take: limit,
orderBy: { name: "asc" }, // opsional, kalau mau urut berdasarkan waktu
}),
prisma.formulirPermohonanKeberatan.count({
where: { isActive: true },
}),
]);
return {
success: true,
message: "Success fetch formulir permohonan keberatan 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 formulir permohonan keberatan with pagination",
};
}
} }

View File

@@ -216,105 +216,282 @@ export default function ModernNewsNotification({
{/* Widget Panel */} {/* Widget Panel */}
<Transition mounted={widgetOpen} transition="slide-up" duration={300}> <Transition mounted={widgetOpen} transition="slide-up" duration={300}>
{(styles) => ( {(styles) => (
<Paper <Box>
style={{ <Box hiddenFrom="md">
...styles, <Paper
position: "fixed", style={{
bottom: "100px", ...styles,
left: "24px", position: "fixed",
width: "90vw", bottom: "100px",
maxWidth: 380, left: "24px",
maxHeight: "500px", width: "90vw",
boxShadow: "0 8px 32px rgba(0,0,0,0.12)", maxWidth: 380,
borderRadius: "16px", maxHeight: "500px",
overflow: "hidden", boxShadow: "0 8px 32px rgba(0,0,0,0.12)",
}} borderRadius: "16px",
> overflow: "hidden",
<Box }}
style={{ >
background: "linear-gradient(135deg, #1e5a7e 0%, #2c7da0 100%)", <Box
padding: "16px 20px", style={{
color: "white", background: "linear-gradient(135deg, #1e5a7e 0%, #2c7da0 100%)",
}} padding: "16px 20px",
> color: "white",
<Group justify="space-between">
<Group gap="xs">
<IconBell size={20} />
<Text c="white" fw={600} size="md">
Berita & Pengumuman
</Text>
</Group>
<CloseButton
onClick={() => {
setWidgetOpen(false);
onSeen?.();
}} }}
variant="transparent" >
c="white" <Group justify="space-between">
/> <Group gap="xs">
</Group> <IconBell size={20} />
</Box> <Text c="white" fw={600} size="md">
Berita & Pengumuman
<Box style={{ maxHeight: "400px", overflowY: "auto", padding: "12px" }}> </Text>
{news.length === 0 ? ( </Group>
<Box p="xl" style={{ textAlign: "center" }}> <CloseButton
<Text c="dimmed" size="sm"> onClick={() => {
Tidak ada berita terbaru setWidgetOpen(false);
</Text> onSeen?.();
}}
variant="transparent"
c="white"
/>
</Group>
</Box> </Box>
) : (
<Stack gap="xs"> <Box style={{ maxHeight: "400px", overflowY: "auto", padding: "12px" }}>
{news.map((item) => ( {news.length === 0 ? (
<Paper <Box p="xl" style={{ textAlign: "center" }}>
key={item.id} <Text c="dimmed" size="sm">
p="md" Tidak ada berita terbaru
radius="md" </Text>
style={{ </Box>
border: "1px solid #e9ecef", ) : (
cursor: "pointer", <Stack gap="xs">
transition: "all 0.2s ease", {news.map((item) => (
}} <Paper
onMouseEnter={(e) => { key={item.id}
e.currentTarget.style.borderColor = "#1e5a7e"; p="md"
e.currentTarget.style.transform = "translateY(-2px)"; radius="md"
e.currentTarget.style.boxShadow = "0 4px 12px rgba(0,0,0,0.08)"; style={{
}} border: "1px solid #e9ecef",
onMouseLeave={(e) => { cursor: "pointer",
e.currentTarget.style.borderColor = "#e9ecef"; transition: "all 0.2s ease",
e.currentTarget.style.transform = "translateY(0)"; }}
e.currentTarget.style.boxShadow = "none"; onMouseEnter={(e) => {
}} e.currentTarget.style.borderColor = "#1e5a7e";
onClick={() => handleNotificationClick(item)} e.currentTarget.style.transform = "translateY(-2px)";
> e.currentTarget.style.boxShadow = "0 4px 12px rgba(0,0,0,0.08)";
<Group justify="space-between" mb="xs"> }}
<Badge onMouseLeave={(e) => {
size="sm" e.currentTarget.style.borderColor = "#e9ecef";
color={item.type === "berita" ? "blue" : "orange"} e.currentTarget.style.transform = "translateY(0)";
variant="light" e.currentTarget.style.boxShadow = "none";
}}
onClick={() => handleNotificationClick(item)}
> >
{item.type === "berita" ? "Berita" : "Pengumuman"} <Group justify="space-between" mb="xs">
</Badge> <Badge
<IconChevronRight size={16} color="#adb5bd" /> size="sm"
</Group> color={item.type === "berita" ? "blue" : "orange"}
<Text fw={600} size="sm" mb={4} lineClamp={2}> variant="light"
{item.title || "Tanpa Judul"} >
</Text> {item.type === "berita" ? "Berita" : "Pengumuman"}
<Text size="xs" c="dimmed" lineClamp={2}> </Badge>
{stripHtml(item.content).substring(0, 100)}... <IconChevronRight size={16} color="#adb5bd" />
</Text> </Group>
</Paper> <Text fw={600} size="sm" mb={4} lineClamp={2}>
))} {item.title || "Tanpa Judul"}
</Stack> </Text>
)} <Text size="xs" c="dimmed" lineClamp={2}>
{stripHtml(item.content).substring(0, 100)}...
</Text>
</Paper>
))}
</Stack>
)}
</Box>
</Paper>
</Box> </Box>
</Paper> <Box visibleFrom="md">
<Paper
style={{
...styles,
position: "fixed",
bottom: "100px",
right: "24px",
width: "90vw",
maxWidth: 380,
maxHeight: "500px",
boxShadow: "0 8px 32px rgba(0,0,0,0.12)",
borderRadius: "16px",
overflow: "hidden",
}}
>
<Box
style={{
background: "linear-gradient(135deg, #1e5a7e 0%, #2c7da0 100%)",
padding: "16px 20px",
color: "white",
}}
>
<Group justify="space-between">
<Group gap="xs">
<IconBell size={20} />
<Text c="white" fw={600} size="md">
Berita & Pengumuman
</Text>
</Group>
<CloseButton
onClick={() => {
setWidgetOpen(false);
onSeen?.();
}}
variant="transparent"
c="white"
/>
</Group>
</Box>
<Box style={{ maxHeight: "400px", overflowY: "auto", padding: "12px" }}>
{news.length === 0 ? (
<Box p="xl" style={{ textAlign: "center" }}>
<Text c="dimmed" size="sm">
Tidak ada berita terbaru
</Text>
</Box>
) : (
<Stack gap="xs">
{news.map((item) => (
<Paper
key={item.id}
p="md"
radius="md"
style={{
border: "1px solid #e9ecef",
cursor: "pointer",
transition: "all 0.2s ease",
}}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = "#1e5a7e";
e.currentTarget.style.transform = "translateY(-2px)";
e.currentTarget.style.boxShadow = "0 4px 12px rgba(0,0,0,0.08)";
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = "#e9ecef";
e.currentTarget.style.transform = "translateY(0)";
e.currentTarget.style.boxShadow = "none";
}}
onClick={() => handleNotificationClick(item)}
>
<Group justify="space-between" mb="xs">
<Badge
size="sm"
color={item.type === "berita" ? "blue" : "orange"}
variant="light"
>
{item.type === "berita" ? "Berita" : "Pengumuman"}
</Badge>
<IconChevronRight size={16} color="#adb5bd" />
</Group>
<Text fw={600} size="sm" mb={4} lineClamp={2}>
{item.title || "Tanpa Judul"}
</Text>
<Text size="xs" c="dimmed" lineClamp={2}>
{stripHtml(item.content).substring(0, 100)}...
</Text>
</Paper>
))}
</Stack>
)}
</Box>
</Paper>
</Box>
</Box>
)} )}
</Transition> </Transition>
{/* Toast Notification */} {/* Toast Notification */}
<Transition <Box>
<Box hiddenFrom="md">
<Transition
mounted={toastVisible && !!currentNews}
transition="slide-left"
duration={300}
>
{(styles) => (
<Paper
style={{
...styles,
position: "fixed",
bottom: "100px",
left: "24px",
width: "90vw",
maxWidth: 380,
boxShadow: "0 8px 32px rgba(0,0,0,0.15)",
borderRadius: "12px",
overflow: "hidden",
border: "1px solid #e9ecef",
}}
onClick={handleLihatSemua}
>
<Box
style={{
height: "3px",
background: "#1e5a7e",
animation: "shrink 8s linear forwards",
}}
/>
<style>{`
@keyframes shrink {
from { width: 100%; }
to { width: 0%; }
}
`}</style>
<Box p="md">
<Group justify="space-between" mb="xs">
<Badge
size="md"
color={currentNews?.type === "berita" ? "blue" : "orange"}
variant="light"
>
{currentNews?.type === "berita"
? "Berita Terbaru"
: "Pengumuman"}
</Badge>
<CloseButton onClick={handleDismissToast} size="sm" />
</Group>
<Text fw={600} size="sm" mb={6}>
{currentNews?.title || "Informasi Terbaru"}
</Text>
<Text size="xs" c="dimmed" lineClamp={3}>
{stripHtml(currentNews?.content || "")}
</Text>
<Group justify="space-between" mt="md">
<Text size="xs" c="dimmed">
{news.length > 1 ? `${news.length} berita tersedia` : "1 berita"}
</Text>
<Text
size="xs"
fw={500}
c="#1e5a7e"
style={{ cursor: "pointer" }}
onClick={handleLihatSemua}
>
Lihat Semua
</Text>
</Group>
</Box>
</Paper>
)}
</Transition>
</Box>
<Box visibleFrom="md">
<Transition
mounted={toastVisible && !!currentNews} mounted={toastVisible && !!currentNews}
transition="slide-left" transition="slide-right"
duration={300} duration={300}
> >
{(styles) => ( {(styles) => (
@@ -323,7 +500,7 @@ export default function ModernNewsNotification({
...styles, ...styles,
position: "fixed", position: "fixed",
bottom: "100px", bottom: "100px",
left: "24px", right: "24px",
width: "90vw", width: "90vw",
maxWidth: 380, maxWidth: 380,
boxShadow: "0 8px 32px rgba(0,0,0,0.15)", boxShadow: "0 8px 32px rgba(0,0,0,0.15)",
@@ -387,6 +564,8 @@ export default function ModernNewsNotification({
</Paper> </Paper>
)} )}
</Transition> </Transition>
</Box>
</Box>
</> </>
); );
} }

View File

@@ -3,18 +3,17 @@
import colors from "@/con/colors" import colors from "@/con/colors"
import stateNav from "@/state/state-nav" import stateNav from "@/state/state-nav"
import { ActionIcon, Button, Container, Flex, Image, Menu, MenuTarget, Stack, Tooltip } from "@mantine/core" import { ActionIcon, Button, Container, Flex, Image, Menu, MenuTarget, Stack, Tooltip } from "@mantine/core"
import { IconSearch, IconUser } from "@tabler/icons-react" import { IconSearch, IconUserCog } from "@tabler/icons-react"
import { useTransitionRouter } from 'next-view-transitions' import { useTransitionRouter } from 'next-view-transitions'
import { usePathname, useRouter } from "next/navigation" import { usePathname, useRouter } from "next/navigation"
import { useSnapshot } from "valtio" import { useSnapshot } from "valtio"
import { MenuItem } from "../../../../types/menu-item" import { MenuItem } from "../../../../types/menu-item"
import { NavbarSearch } from "./NavBarSearch" import { NavbarSearch } from "./NavBarSearch"
import { NavbarSubMenu } from "./NavbarSubMenu" import { NavbarSubMenu } from "./NavbarSubMenu"
import { authStore } from "@/store/authStore";
// contoh state auth (dummy aja dulu, bisa diganti sesuai sistem auth kamu) // contoh state auth (dummy aja dulu, bisa diganti sesuai sistem auth kamu)
const stateAuth = { const isAdmin = authStore.user?.roleId === 0 || authStore.user?.roleId === 1 || authStore.user?.roleId === 2 || authStore.user?.roleId === 3 || authStore.user?.roleId === 4;
role: "admin", // coba ubah ke "user" buat test
}
export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) { export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
const { item, isSearch } = useSnapshot(stateNav) const { item, isSearch } = useSnapshot(stateNav)
@@ -70,8 +69,8 @@ export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
</Tooltip> </Tooltip>
{/* hanya tampil kalau role = admin */} {/* hanya tampil kalau role = admin */}
{stateAuth.role === "admin" && ( {isAdmin && (
<Tooltip label="Profil Saya" position="bottom" withArrow> <Tooltip label="Kembali ke Admin" position="bottom" withArrow>
<ActionIcon <ActionIcon
onClick={() => { onClick={() => {
next.push("/admin/landing-page/profil/program-inovasi") next.push("/admin/landing-page/profil/program-inovasi")
@@ -80,7 +79,7 @@ export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
radius="xl" radius="xl"
variant="light" variant="light"
> >
<IconUser size={22} /> <IconUserCog size={22} />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
)} )}